Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: onchain feature flag registry #22

Merged
merged 7 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions contracts/helpers/FeatureFlagRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";

/**
* @title FeatureFlagRegistry
* @notice A contract for managing feature flags onchain
*/
contract FeatureFlagRegistry is AccessControl {
error Immutable();

event FeatureFlagSet(bytes32 indexed featureFlagHash, address indexed user, bool status);
event FeatureFlagImmutable(bytes32 indexed featureFlagHash);

bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");

mapping(bytes32 => mapping(address => bool)) private _featureFlagStatus;
mapping(bytes32 => bool) public featureFlagImmutable;

/**
* @notice Initializes the contract with an owner who receives the DEFAULT_ADMIN_ROLE
* @param owner Address that will be granted the admin role
*/
constructor(address owner) {
_grantRole(DEFAULT_ADMIN_ROLE, owner);
}

/**
* @notice Sets the status of a feature flag for a specific user; specify zero address to set the global status
* @dev Only callable by accounts with MANAGER_ROLE
* @param featureFlag String identifier of the feature flag
* @param user Address of the user to set the status for
* @param status Boolean indicating if the feature should be enabled
* @custom:throws Immutable if the feature flag has been made immutable
*/
function setFeatureFlagStatus(string memory featureFlag, address user, bool status) public onlyRole(MANAGER_ROLE) {
bytes32 featureFlagHash = _hashFeatureFlag(featureFlag);
if (featureFlagImmutable[featureFlagHash]) {
revert Immutable();
}
_featureFlagStatus[featureFlagHash][user] = status;

emit FeatureFlagSet(featureFlagHash, user, status);
}

/**
* @notice Makes a feature flag immutable, preventing future status changes
* @dev Only callable by accounts with MANAGER_ROLE
* @param featureFlag String identifier of the feature flag
* @custom:throws Immutable if the feature flag is already immutable
*/
function setFeatureFlagImmutable(string memory featureFlag) public onlyRole(MANAGER_ROLE) {
bytes32 featureFlagHash = _hashFeatureFlag(featureFlag);
if (featureFlagImmutable[featureFlagHash]) {
revert Immutable();
}
featureFlagImmutable[featureFlagHash] = true;
}

/**
* @notice Checks if a feature flag is enabled for a specific user
* @param featureFlag String identifier of the feature flag
* @param user Address of the user to check
* @return bool True if the feature is enabled for the user
*/
function isFeatureFlagEnabled(string memory featureFlag, address user) public view returns (bool) {
bytes32 featureFlagHash = _hashFeatureFlag(featureFlag);

return isFeatureFlagEnabled(featureFlagHash, user);
}

/**
* @notice Checks if a feature flag is enabled for a specific user using the feature flag hash
* @dev Implements the logic for checking feature flag status, considering global and user-specific settings
* @param featureFlagHash Hash of the feature flag identifier
* @param user Address of the user to check
* @return bool True if the feature is enabled for the user
*/
function isFeatureFlagEnabled(bytes32 featureFlagHash, address user) public view returns (bool) {
bool globalStatus = _featureFlagStatus[featureFlagHash][address(0)];

if (featureFlagImmutable[featureFlagHash]) {
return globalStatus;
} else if (globalStatus) {
return true;
}
return _featureFlagStatus[featureFlagHash][user];
}

/**
* @notice Hashes a feature flag string identifier
* @dev Internal helper function to consistently hash feature flag strings
* @param featureFlag String identifier to hash
* @return bytes32 Keccak256 hash of the feature flag string
*/
function _hashFeatureFlag(string memory featureFlag) internal pure returns (bytes32) {
return keccak256(bytes(featureFlag));
}
}
23 changes: 23 additions & 0 deletions deploy/deploy-feature-flag-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright Clave - All Rights Reserved
* Unauthorized copying of this file, via any medium is strictly prohibited
* Proprietary and confidential
*/
import * as hre from 'hardhat';
import { Wallet } from 'zksync-ethers';
import { create2IfNotExists, getWallet } from '../deploy/utils';
let fundingWallet: Wallet;

// An example of a basic deploy script
// Do not push modifications to this file
// Just modify, interact then revert changes
export default async function (): Promise<void> {
fundingWallet = getWallet(hre);

const initialOwner = fundingWallet.address;

const implementation = await create2IfNotExists(hre, "FeatureFlagRegistry", [initialOwner]);

console.log("FeatureFlagRegistry deployed at", await implementation.getAddress());
}

49 changes: 4 additions & 45 deletions deploy/deploy-session-key-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@
* Unauthorized copying of this file, via any medium is strictly prohibited
* Proprietary and confidential
*/
import {
AbiCoder,
ZeroHash,
} from 'ethers';
import * as hre from 'hardhat';
import { Contract, Wallet, utils } from 'zksync-ethers';
import { deployContract, getProvider, getWallet } from '../deploy/utils';
import { Wallet } from 'zksync-ethers';
import { create2IfNotExists, getWallet } from '../deploy/utils';
let fundingWallet: Wallet;

// An example of a basic deploy script
Expand All @@ -20,47 +16,10 @@ export default async function (): Promise<void> {

const initialOwner = fundingWallet.address;

const implementation = await create2IfNotExists("SessionKeyPolicyRegistry", []);
const implementation = await create2IfNotExists(hre, "SessionKeyPolicyRegistry", []);

await create2IfNotExists("ERC1967Proxy", [
await create2IfNotExists(hre, "ERC1967Proxy", [
await implementation.getAddress(),
implementation.interface.encodeFunctionData("initialize", [initialOwner])
]);
}

async function create2IfNotExists(contractName: string, constructorArguments: any[]): Promise<Contract> {

const artifact = await hre.zksyncEthers.loadArtifact(contractName);
const bytecodeHash = utils.hashBytecode(artifact.bytecode);

const constructor = artifact.abi.find(abi => abi.type === "constructor");

let encodedConstructorArguments = "0x";
if (constructor) {
encodedConstructorArguments = AbiCoder.defaultAbiCoder().encode(constructor.inputs, constructorArguments);
}

const address = utils.create2Address("0x0000000000000000000000000000000000010000", bytecodeHash, ZeroHash, encodedConstructorArguments);

const provider = getProvider(hre);
const code = await provider.getCode(address);
if (code !== "0x") {
console.log(`Contract ${contractName} already deployed at ${address}`);

// enable this to verify the contracts on a subsequent run
// await verifyContract(hre, {
// address,
// contract: artifact.sourceName,
// constructorArguments: encodedConstructorArguments,
// bytecode: artifact.bytecode
// })

return new Contract(address, artifact.abi, getWallet(hre));
}

return deployContract(hre, contractName, constructorArguments, {
wallet: getWallet(hre),
silent: false,
}, 'create2');

}
56 changes: 8 additions & 48 deletions deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@
* Proprietary and confidential
*/
import {
AbiCoder,
hexlify,
keccak256,
ZeroAddress,
ZeroHash,
zeroPadValue
} from 'ethers';
import * as hre from 'hardhat';
import { Contract, Wallet, utils } from 'zksync-ethers';
import { deployContract, getProvider, getWallet, verifyContract } from '../deploy/utils';
import { create2IfNotExists, getProvider, getWallet, verifyContract } from '../deploy/utils';
import type { CallStruct } from '../typechain-types/contracts/batch/BatchCaller';
let fundingWallet: Wallet;

Expand All @@ -30,15 +28,15 @@ export default async function (): Promise<void> {

const initialOwner = fundingWallet.address;

eoaValidator = await create2IfNotExists("EOAValidator", []);
implementation = await create2IfNotExists("AGWAccount", [await eoaValidator.getAddress()]);
registry = await create2IfNotExists("AGWRegistry", [initialOwner]);
await create2IfNotExists("AccountProxy", [await implementation.getAddress()]);
eoaValidator = await create2IfNotExists(hre, "EOAValidator", []);
implementation = await create2IfNotExists(hre, "AGWAccount", [await eoaValidator.getAddress()]);
registry = await create2IfNotExists(hre, "AGWRegistry", [initialOwner]);
await create2IfNotExists(hre, "AccountProxy", [await implementation.getAddress()]);

const accountProxyArtifact = await hre.zksyncEthers.loadArtifact('AccountProxy');
const bytecodeHash = utils.hashBytecode(accountProxyArtifact.bytecode);
console.log("bytecodeHash", hexlify(bytecodeHash));
factory = await create2IfNotExists("AccountFactory", [await implementation.getAddress(), "0xb4e581f5", await registry.getAddress(), bytecodeHash, fundingWallet.address, initialOwner]);
factory = await create2IfNotExists(hre, "AccountFactory", [await implementation.getAddress(), "0xb4e581f5", await registry.getAddress(), bytecodeHash, fundingWallet.address, initialOwner]);

const factoryAddress = await factory.getAddress();
const isFactory = await registry.isFactory(factoryAddress);
Expand All @@ -47,50 +45,12 @@ export default async function (): Promise<void> {
await registry.setFactory(factoryAddress);
}

await create2IfNotExists("AAFactoryPaymaster", [await factory.getAddress()]);
await create2IfNotExists("SessionKeyValidator", []);
await create2IfNotExists(hre, "AAFactoryPaymaster", [await factory.getAddress()]);
await create2IfNotExists(hre, "SessionKeyValidator", []);

await deployAccountIfNotExists(initialOwner);
}


async function create2IfNotExists(contractName: string, constructorArguments: any[]): Promise<Contract> {

const artifact = await hre.zksyncEthers.loadArtifact(contractName);
const bytecodeHash = utils.hashBytecode(artifact.bytecode);

const constructor = artifact.abi.find(abi => abi.type === "constructor");

let encodedConstructorArguments = "0x";
if (constructor) {
encodedConstructorArguments = AbiCoder.defaultAbiCoder().encode(constructor.inputs, constructorArguments);
}

const address = utils.create2Address("0x0000000000000000000000000000000000010000", bytecodeHash, ZeroHash, encodedConstructorArguments);

const provider = getProvider(hre);
const code = await provider.getCode(address);
if (code !== "0x") {
console.log(`Contract ${contractName} already deployed at ${address}`);

// enable this to verify the contracts on a subsequent run
// await verifyContract(hre, {
// address,
// contract: artifact.sourceName,
// constructorArguments: encodedConstructorArguments,
// bytecode: artifact.bytecode
// })

return new Contract(address, artifact.abi, getWallet(hre));
}

return deployContract(hre, contractName, constructorArguments, {
wallet: getWallet(hre),
silent: false,
}, 'create2');

}

async function deployAccountIfNotExists(initialOwner: string) {


Expand Down
40 changes: 39 additions & 1 deletion deploy/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { Deployer } from '@matterlabs/hardhat-zksync';
import '@matterlabs/hardhat-zksync-node/dist/type-extensions';
import '@matterlabs/hardhat-zksync-verify/dist/src/type-extensions';
import { ethers } from 'ethers';
import { AbiCoder, ZeroHash, Contract, ethers } from 'ethers';
import type { HardhatRuntimeEnvironment } from 'hardhat/types';
import { Provider, Wallet, utils } from 'zksync-ethers';
import { DeploymentType } from 'zksync-ethers/build/types';
Expand Down Expand Up @@ -107,6 +107,7 @@ export const deployContract = async (
log(`\nStarting deployment process of "${contractArtifactName}"...`);

const wallet = options?.wallet ?? getWallet(hre);
// @ts-ignore - wallet type is overridden by hardhat-zksync
const deployer = new Deployer(hre, wallet);
const artifact = await deployer
.loadArtifact(contractArtifactName)
Expand Down Expand Up @@ -221,3 +222,40 @@ export const LOCAL_RICH_WALLETS = [
'0x3eb15da85647edd9a1159a4a13b9e7c56877c4eb33f614546d4db06a51868b1c',
},
];

export async function create2IfNotExists(hre: HardhatRuntimeEnvironment, contractName: string, constructorArguments: any[]): Promise<Contract> {

const artifact = await hre.zksyncEthers.loadArtifact(contractName);
const bytecodeHash = utils.hashBytecode(artifact.bytecode);

const constructor = artifact.abi.find(abi => abi.type === "constructor");

let encodedConstructorArguments = "0x";
if (constructor) {
encodedConstructorArguments = AbiCoder.defaultAbiCoder().encode(constructor.inputs, constructorArguments);
}

const address = utils.create2Address("0x0000000000000000000000000000000000010000", bytecodeHash, ZeroHash, encodedConstructorArguments);

const provider = getProvider(hre);
const code = await provider.getCode(address);
if (code !== "0x") {
console.log(`Contract ${contractName} already deployed at ${address}`);

// enable this to verify the contracts on a subsequent run
// await verifyContract(hre, {
// address,
// contract: artifact.sourceName,
// constructorArguments: encodedConstructorArguments,
// bytecode: artifact.bytecode
// })

return new Contract(address, artifact.abi, getWallet(hre));
}

return deployContract(hre, contractName, constructorArguments, {
wallet: getWallet(hre),
silent: false,
}, 'create2');

}
17 changes: 17 additions & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ const abstractTestnet: NetworkUserConfig = {
chainId: 11124,
}

const abstractMainnet: NetworkUserConfig = {
url: "https://api.mainnet.abs.xyz",
ethNetwork: "mainnet",
zksync: true,
verifyURL: 'https://api-explorer-verify.mainnet.abs.xyz/contract_verification',
chainId: 2741,
}

const config: HardhatUserConfig = {
zksolc: {
version: '1.5.6',
Expand All @@ -83,6 +91,14 @@ const config: HardhatUserConfig = {
browserURL: 'https://sepolia.abscan.org',
},
},
{
network: 'abstractMainnet',
chainId: 2741,
urls: {
apiURL: 'https://api.abscan.org/api',
browserURL: 'https://abscan.org',
},
},
],
},
networks: {
Expand All @@ -93,6 +109,7 @@ const config: HardhatUserConfig = {
zkSyncSepolia,
zkSyncMainnet,
abstractTestnet,
abstractMainnet,
inMemoryNode,
dockerizedNode,
},
Expand Down
Loading