diff --git a/contracts/helpers/FeatureFlagRegistry.sol b/contracts/helpers/FeatureFlagRegistry.sol new file mode 100644 index 0000000..fcdae5a --- /dev/null +++ b/contracts/helpers/FeatureFlagRegistry.sol @@ -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)); + } +} diff --git a/deploy/deploy-feature-flag-registry.ts b/deploy/deploy-feature-flag-registry.ts new file mode 100644 index 0000000..2ea3e0d --- /dev/null +++ b/deploy/deploy-feature-flag-registry.ts @@ -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 { + fundingWallet = getWallet(hre); + + const initialOwner = fundingWallet.address; + + const implementation = await create2IfNotExists(hre, "FeatureFlagRegistry", [initialOwner]); + + console.log("FeatureFlagRegistry deployed at", await implementation.getAddress()); +} + diff --git a/deploy/deploy-session-key-registry.ts b/deploy/deploy-session-key-registry.ts index 7141d5f..ea73c2c 100644 --- a/deploy/deploy-session-key-registry.ts +++ b/deploy/deploy-session-key-registry.ts @@ -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 @@ -20,47 +16,10 @@ export default async function (): Promise { 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 { - - 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'); - -} \ No newline at end of file diff --git a/deploy/deploy.ts b/deploy/deploy.ts index 92c5a16..d5bfcb7 100644 --- a/deploy/deploy.ts +++ b/deploy/deploy.ts @@ -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; @@ -30,15 +28,15 @@ export default async function (): Promise { 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); @@ -47,50 +45,12 @@ export default async function (): Promise { 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 { - - 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) { diff --git a/deploy/utils.ts b/deploy/utils.ts index 49fa5c9..219e343 100644 --- a/deploy/utils.ts +++ b/deploy/utils.ts @@ -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'; @@ -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) @@ -221,3 +222,40 @@ export const LOCAL_RICH_WALLETS = [ '0x3eb15da85647edd9a1159a4a13b9e7c56877c4eb33f614546d4db06a51868b1c', }, ]; + +export async function create2IfNotExists(hre: HardhatRuntimeEnvironment, contractName: string, constructorArguments: any[]): Promise { + + 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'); + +} \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 980150c..373a52e 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -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', @@ -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: { @@ -93,6 +109,7 @@ const config: HardhatUserConfig = { zkSyncSepolia, zkSyncMainnet, abstractTestnet, + abstractMainnet, inMemoryNode, dockerizedNode, },