diff --git a/.gitmodules b/.gitmodules index ec7e6ad..648a854 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,9 +4,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts -[submodule "lib/clones-with-immutable-args"] - path = lib/clones-with-immutable-args - url = https://github.com/wighawag/clones-with-immutable-args [submodule "lib/evmgateway"] path = lib/evmgateway url = https://github.com/ensdomains/evmgateway diff --git a/lib/clones-with-immutable-args b/lib/clones-with-immutable-args deleted file mode 160000 index 5950723..0000000 --- a/lib/clones-with-immutable-args +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5950723ffcfa047f13262e5dbd7218b54360c42e diff --git a/remappings.txt b/remappings.txt index 482c58a..1259616 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,2 @@ @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ -@ensdomains/evm-verifier/=lib/evmgateway/evm-verifier/contracts/ -clones-with-immutable-args/=lib/clones-with-immutable-args/src/ +@ensdomains/evm-verifier/=lib/evmgateway/evm-verifier/contracts/ \ No newline at end of file diff --git a/script/VerifiableFactory.s.sol b/script/VerifiableFactory.s.sol index 93cdd28..6a61e0f 100644 --- a/script/VerifiableFactory.s.sol +++ b/script/VerifiableFactory.s.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; @@ -12,12 +12,13 @@ contract CounterScript is Script { function run() public { vm.startBroadcast(); - string[] memory urls; - // initialize the ccip-read urls - urls = new string[](1); - urls[0] = ""; + // TODO complete it + VerifiableFactory.Verifiers[] memory verifiers = new VerifiableFactory.Verifiers[](3); + verifiers[0] = VerifiableFactory.Verifiers({networkId: 1, verifier: address(0)}); + verifiers[1] = VerifiableFactory.Verifiers({networkId: 42, verifier: address(0)}); + verifiers[2] = VerifiableFactory.Verifiers({networkId: 137, verifier: address(0)}); - factory = new VerifiableFactory(urls, bytes32(0)); + factory = new VerifiableFactory(verifiers); vm.stopBroadcast(); } diff --git a/src/RegistryProxy.sol b/src/RegistryProxy.sol new file mode 100644 index 0000000..d1acb7b --- /dev/null +++ b/src/RegistryProxy.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +contract RegistryProxy { + uint256 public nonce; // slot 0 + address public registry; // slot 1 + address public admin; // slot 2 + + modifier onlyAdmin() { + require(msg.sender == admin, "Caller is not the admin"); + _; + } + + function initialize(uint256 _nonce, address _admin) external { + require(nonce == 0, "Already initialized"); + nonce = _nonce; + admin = _admin; // set the admin (ProxyAdmin contract address) + } + + function updateRegistry(address newRegistry) external onlyAdmin { + registry = newRegistry; + } +} \ No newline at end of file diff --git a/src/VerifiableFactory.sol b/src/VerifiableFactory.sol index e2eb32e..51ce90c 100644 --- a/src/VerifiableFactory.sol +++ b/src/VerifiableFactory.sol @@ -5,39 +5,72 @@ import {EVMFetcher} from "@ensdomains/evm-verifier/EVMFetcher.sol"; import {EVMFetchTarget} from "@ensdomains/evm-verifier/EVMFetchTarget.sol"; import {IEVMVerifier} from "@ensdomains/evm-verifier/IEVMVerifier.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -contract ProxyFactory is EVMFetchTarget { +import { RegistryProxy } from './RegistryProxy.sol'; + +contract VerifiableFactory is EVMFetchTarget { using EVMFetcher for EVMFetcher.EVMFetchRequest; - IEVMVerifier public immutable verifier; + struct Verifiers { + uint256 networkId; + address verifier; + } + + ProxyAdmin public proxyAdmin; uint256 constant PROXY_NONCE_SLOT = 0; uint256 constant PROXY_REGISTRY_SLOT = 1; + mapping(uint256 => IEVMVerifier) public verifiers; + event ProxyDeployed(address indexed proxyAddress); - constructor(IEVMVerifier _verifier) { + constructor(Verifiers[] memory _verifiers) { require( - address(_verifier) != address(0), - "Verifier address must be set" + _verifiers.length > 0, + "At least one verifier address must be set" ); - verifier = _verifier; + + for (uint256 i = 0; i < _verifiers.length; i++) { + verifiers[_verifiers[i].networkId] = IEVMVerifier(_verifiers[i].verifier); + } + + proxyAdmin = new ProxyAdmin(address(this)); } - function deployProxy( - address implementation, - uint256 nonce - ) external returns (address) { + + /** + * @dev deploys a new `TransparentUpgradeableProxy` contract using a deterministic address derived from + * the sender's address and a nonce. + * + * The function creates a new proxy contract that is controlled by the factory's `ProxyAdmin`. + * When the proxy is deployed, it starts by using the `RegistryProxy` contract as its main implementation. + * During the deployment, the initialize function is called to set up the proxy. + * The nonce ensures that each user gets a unique proxy, even if the same user deploys multiple proxies. + * + * - The function uses a `salt` to create a deterministic address based on `msg.sender` and a provided nonce. + * - The `initialize` function of the `RegistryProxy` contract is called immediately after deployment to set up + * the proxy with the nonce and `ProxyAdmin`. + * - The proxy is managed by a `ProxyAdmin` contract, ensuring that upgrades and critical functions are restricted to the admin. + * - A custom event `ProxyDeployed` is emitted to track the deployment of the new proxy. + * + * @param nonce A unique number provided by the caller to create a unique proxy address. + * @return proxy The address of the deployed `TransparentUpgradeableProxy` contract. + */ + function deployProxy(uint256 nonce) external returns (address) { bytes32 salt = keccak256(abi.encodePacked(msg.sender, nonce)); bytes memory data = abi.encodeWithSignature( "initialize(uint256)", - nonce + nonce, + address(proxyAdmin) ); + RegistryProxy proxyInstance = new RegistryProxy(); address proxy = address( new TransparentUpgradeableProxy{salt: salt}( - implementation, - address(this), + address(proxyInstance), + address(proxyAdmin), data ) ); @@ -48,54 +81,59 @@ contract ProxyFactory is EVMFetchTarget { return proxy; } - function updateRegistry(address proxy, address newRegistry) external { - (bool success, bytes memory result) = proxy.staticcall( - abi.encodeWithSignature("nonce()") - ); - require(success, "Failed to retrieve nonce from proxy"); - uint256 proxyNonce = abi.decode(result, (uint256)); - - bytes32 salt = keccak256(abi.encodePacked(msg.sender, proxyNonce)); - address expectedProxyAddress = address( - uint160( - uint256( - keccak256( - abi.encodePacked( - bytes1(0xff), - address(this), - salt, - keccak256( - type(TransparentUpgradeableProxy).creationCode - ) - ) - ) - ) - ) - ); - require( - expectedProxyAddress == proxy, - "Only the creator can update the registry" - ); - - (success, ) = proxy.call( - abi.encodeWithSignature("updateRegistry(address)", newRegistry) - ); - require(success, "Registry update failed"); + function updateRegistry(address proxy, address newImplementation) external { + // todo } - function verifyContract(address proxy) public view { + /** + * @dev verifies a proxy contract on a specified network using the EVMGateway. + * + * The function starts the process of verifying a specific proxy contract by sending a request + * to the correct IEVMVerifier for the given network. It then retrieves fixed values from certain + * storage slots (like the proxy's nonce and registry address) to help with the verification + * + * The verification request is sent through the `EVMFetcher` to the gateway, and the result + * is processed by the `verifyCallback` function upon completion. + * + * - The function looks up the correct verifier for the network based on the provided `networkId`. + * - It retrieves static values from storage slots (`PROXY_NONCE_SLOT` and `PROXY_REGISTRY_SLOT`) to assist in the verification process. + * - The fetched data is passed to `verifyCallback` for further processing. + * + * @param proxy The address of the proxy contract to be verified. + * @param networkId The ID of the network where the verification will take place. + */ + function verifyContract(address proxy, uint256 networkId) public view { + IEVMVerifier verifier = verifiers[networkId]; + require(address(verifier) != address(0), "Verifier is not available for the given netowrk"); + EVMFetcher - .newFetchRequest(verifier, proxy) + .newFetchRequest(verifiers[networkId], proxy) .getStatic(PROXY_NONCE_SLOT) .getStatic(PROXY_REGISTRY_SLOT) .fetch(this.verifyCallback.selector, abi.encode(proxy)); } + /** + * @dev callback function that processes the response from the EVMGateway after fetching the verification data. + * + * This function decodes the fetched data and verifies the correctness of the proxy contract's address by + * calculating the expected address using a salt derived from the sender's address and the proxy's nonce. + * + * - The response data contains the nonce, registry address, and extra data (proxy address) retrieved from both verfiyContract and gateway requests. + * - The proxy address is reconstructed using the same logic used during deployment to ensure it matches the expected proxy address. + * - If the proxy address matches, the function returns `true`, indicating a successful verification. + * + * @param response The response data retrieved from the EVMGateway, containing the nonce, registry address, and extra data. + * @return bool Returns `true` if the proxy address matches the expected address, otherwise the function will revert. + */ function verifyCallback( bytes calldata response ) public view returns (bool) { - (uint256 nonce, /*address registryAddress*/, bytes memory extraData) = abi - .decode(response, (uint256, address, bytes)); + ( + uint256 nonce /*address registryAddress*/, + , + bytes memory extraData + ) = abi.decode(response, (uint256, address, bytes)); address proxy = abi.decode(extraData, (address)); bytes32 salt = keccak256(abi.encodePacked(msg.sender, nonce)); @@ -130,19 +168,3 @@ contract ProxyFactory is EVMFetchTarget { return size > 0; } } - -// RegistryProxy Contract -contract RegistryProxy { - uint256 public nonce; - address public registry; - - function updateRegistry(address newRegistry) external { - require(msg.sender == registry, "Only owner can update"); - registry = newRegistry; - } - - function initialize(uint256 _nonce) external { - require(nonce == 0, "Already initialized"); - nonce = _nonce; - } -} diff --git a/src/ChildContract.sol b/src/mock/MockRegistry.sol similarity index 90% rename from src/ChildContract.sol rename to src/mock/MockRegistry.sol index 862d6f5..51e62d3 100644 --- a/src/ChildContract.sol +++ b/src/mock/MockRegistry.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; -contract ChildContract { +contract MockRegistry { uint256 public value; address public factory; diff --git a/src/mock/VerifyCreate2Harness.sol b/src/mock/VerifyCreate2Harness.sol deleted file mode 100644 index 1d279cb..0000000 --- a/src/mock/VerifyCreate2Harness.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {VerifiableFactory} from "../VerifiableFactory.sol"; - -contract VerifyCreate2Harness is VerifiableFactory { - constructor(string[] memory _urls, bytes32 _rootHash) VerifiableFactory(_urls, _rootHash) {} - - // Deploy this contract then call this method to test `myInternalMethod`. - function verifyCreate2Harness(address createdContractAddress, uint256 _value, address user) - external - view - returns (bool) - { - return verifyCreate2(createdContractAddress, _value, user); - } -} diff --git a/test/VerifiableFactory.t.sol b/test/VerifiableFactory.t.sol index 8292593..d3ded71 100644 --- a/test/VerifiableFactory.t.sol +++ b/test/VerifiableFactory.t.sol @@ -1,76 +1,78 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; -import "forge-std/Test.sol"; -import "../src/VerifiableFactory.sol"; -import "../src/ChildContract.sol"; +import {Test} from "forge-std/Test.sol"; +import {VerifiableFactory} from "../src/VerifiableFactory.sol"; +import {IEVMVerifier} from "@ensdomains/evm-verifier/IEVMVerifier.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; -// import "../src/mock/VerifyCreate2Harness.sol"; - -contract FactoryTest is Test { - VerifiableFactory factory; - bytes32 merkleRoot; - string[] urls; +contract VerifiableFactoryTest is Test { + VerifiableFactory public factory; + address public deployer; + IEVMVerifier verifier1; + IEVMVerifier verifier2; + IEVMVerifier verifier3; function setUp() public { - // static merkle root - merkleRoot = 0x38fd43fd2274f45a44e9dcb8da9065881f7416a5ba85a5684eee8e2db0e0a1f3; + deployer = address(this); + verifier1 = IEVMVerifier(address(0)); + verifier2 = IEVMVerifier(address(0)); + verifier3 = IEVMVerifier(address(0)); - // mocking ccip-read urls - urls = new string[](1); - urls[0] = ""; + VerifiableFactory.Verifiers[] memory verifiers = new VerifiableFactory.Verifiers[](3); + verifiers[0] = VerifiableFactory.Verifiers({networkId: 1, verifier: address(verifier1)}); + verifiers[1] = VerifiableFactory.Verifiers({networkId: 42, verifier: address(verifier2)}); + verifiers[2] = VerifiableFactory.Verifiers({networkId: 137, verifier: address(verifier3)}); - factory = new VerifiableFactory(urls, merkleRoot); + factory = new VerifiableFactory(verifiers); } - function testCreateContract() public { - uint256 value = 42; - - address childContractAddress = factory.createContract(value); - - ChildContract child = ChildContract(childContractAddress); - assertEq(child.value(), value, "Child contract's value should be correct"); - assertEq(child.factory(), address(factory), "Child contract's factory should be correct"); + function testFactoryDeployment() public view { + assert(address(factory) != address(0)); } - // function testVerifyContract() public { - // uint256 value = 42; - // address user = address(this); - - // address childContractAddress = factory.createContract(value); + function testVerifierMapping() public view { + // Check if verifier addresses are correctly set + assertEq(address(factory.verifiers(1)), address(verifier1)); + assertEq(address(factory.verifiers(42)), address(verifier2)); + assertEq(address(factory.verifiers(137)), address(verifier3)); + } - // // mock a successful off-chain storage layout verification (skip OffchainLookup for simplicity) - // // in a full test, simulate the off-chain verification and invoke verifyCallback manually - // // this test only for verifying that the contract was created by the factory + function testDeployProxy() public { + uint256 nonce = 1; + address proxyAddress = factory.deployProxy(nonce); + + // Check if proxy was deployed + uint256 codeSize; + assembly { + codeSize := extcodesize(proxyAddress) + } + assert(codeSize > 0); + emit log_named_address("Deployed Proxy Address", proxyAddress); + } - // // verify the contract using factory's verifyContract function - // // normally this would trigger OffchainLookup; here we're simplifying by calling internal function directly - // VerifyCreate2Harness harness = new VerifyCreate2Harness(urls, merkleRoot); - // bool result = harness.verifyCreate2Harness(childContractAddress, value, user); - // assertTrue(result, "Verification should succeed for valid contract"); - // } + function testUpdateRegistry() public { + uint256 nonce = 1; + address proxyAddress = factory.deployProxy(nonce); + address newRegistryAddress = address(0); - function testVerifyInvalidContract() public { - // deploy a ChildContract without the factory - ChildContract rogueContract = new ChildContract(42, address(factory)); + // Update the registry of the deployed proxy + factory.updateRegistry(proxyAddress, newRegistryAddress); - // verify if it using the factory (this should fail) - vm.expectRevert(VerifiableFactory.VerificationFailed.selector); - factory.verifyContract(address(rogueContract), 42, address(this)); + // Retrieve the registry from the proxy to confirm the update + (bool success, bytes memory result) = proxyAddress.call(abi.encodeWithSignature("registry()")); + assert(success); + address updatedRegistryAddress = abi.decode(result, (address)); + assertEq(updatedRegistryAddress, newRegistryAddress); } - function testVerifyCallback() public view { - bytes32 layout = bytes32(uint256(0)); - uint256 value = 42; - - // mock merkle proof and leaf for off-chain verification - bytes32[] memory merkleProof = new bytes32[](1); - merkleProof[0] = 0xcf5d987e8c58e4cfb73aa2884be6034c1cb96def6945e12657d99532fe2c81b6; - - bytes32 leafHash = keccak256(bytes.concat(keccak256(abi.encode(layout, value)))); - console.logBytes32(leafHash); + function testVerifyContract() public { + uint256 nonce = 1; + address proxyAddress = factory.deployProxy(nonce); + factory.verifyContract(proxyAddress, 1); - bool result = factory.verifyCallback(merkleProof, leafHash); - assertTrue(result, "VerificationCallback should succeed with valid Merkle proof"); + // If no revert, verification passed + assertTrue(true); } }