From 77186454abf79d78d11d5aafa5795e07ceecb0cd Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Thu, 23 Jan 2025 18:43:37 -0500 Subject: [PATCH 1/4] zksync factory Signed-off-by: Adam Wolf --- .gitignore | 1 + .../factory/zksync/MagicDropCloneFactory.sol | 203 ++++++++++++++++++ contracts/factory/zksync/ZKProxy.sol | 36 ++++ test/factory/MagicDropCloneFactoryTest.t.sol | 40 +--- 4 files changed, 241 insertions(+), 39 deletions(-) create mode 100644 contracts/factory/zksync/MagicDropCloneFactory.sol create mode 100644 contracts/factory/zksync/ZKProxy.sol diff --git a/.gitignore b/.gitignore index 7f3a0e5d..36313eb3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ artifacts dist .DS_Store out/ +zkout/ lcov.info lcov.info.pruned coverage/ diff --git a/contracts/factory/zksync/MagicDropCloneFactory.sol b/contracts/factory/zksync/MagicDropCloneFactory.sol new file mode 100644 index 00000000..3146ccf6 --- /dev/null +++ b/contracts/factory/zksync/MagicDropCloneFactory.sol @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {Ownable} from "solady/src/auth/Ownable.sol"; +import {TokenStandard} from "../../common/Structs.sol"; +import {MagicDropTokenImplRegistry} from "../../registry/MagicDropTokenImplRegistry.sol"; +import {ZKProxy} from "./ZKProxy.sol"; + +/// @title MagicDropCloneFactory +/// @notice A factory contract for creating and managing clones of MagicDrop contracts +/// @dev This contract uses the UUPS proxy pattern +contract MagicDropCloneFactory is Ownable { + /*============================================================== + = CONSTANTS = + ==============================================================*/ + + MagicDropTokenImplRegistry private _registry; + bytes4 private constant INITIALIZE_SELECTOR = bytes4(keccak256("initialize(string,string,address)")); + + /*============================================================== + = EVENTS = + ==============================================================*/ + + event MagicDropFactoryInitialized(); + event NewContractInitialized( + address contractAddress, address initialOwner, uint32 implId, TokenStandard standard, string name, string symbol + ); + event Withdrawal(address to, uint256 amount); + + /*============================================================== + = ERRORS = + ==============================================================*/ + + error InitializationFailed(); + error RegistryAddressCannotBeZero(); + error InsufficientDeploymentFee(); + error WithdrawalFailed(); + error InitialOwnerCannotBeZero(); + + /*============================================================== + = CONSTRUCTOR = + ==============================================================*/ + + /// @param initialOwner The address of the initial owner + /// @param registry The address of the registry contract + constructor(address initialOwner, address registry) public { + if (registry == address(0)) { + revert RegistryAddressCannotBeZero(); + } + + _registry = MagicDropTokenImplRegistry(registry); + _initializeOwner(initialOwner); + + emit MagicDropFactoryInitialized(); + } + + /*============================================================== + = PUBLIC WRITE METHODS = + ==============================================================*/ + + /// @notice Creates a new deterministic clone of a MagicDrop contract + /// @param name The name of the new contract + /// @param symbol The symbol of the new contract + /// @param standard The token standard of the new contract + /// @param initialOwner The initial owner of the new contract + /// @param implId The implementation ID + /// @param salt A unique salt for deterministic address generation + /// @return The address of the newly created contract + function createContractDeterministic( + string calldata name, + string calldata symbol, + TokenStandard standard, + address payable initialOwner, + uint32 implId, + bytes32 salt + ) external payable returns (address) { + address impl; + // Retrieve the implementation address from the registry + if (implId == 0) { + impl = _registry.getDefaultImplementation(standard); + } else { + impl = _registry.getImplementation(standard, implId); + } + + if (initialOwner == address(0)) { + revert InitialOwnerCannotBeZero(); + } + + // Retrieve the deployment fee for the implementation and ensure the caller has sent the correct amount + uint256 deploymentFee = _registry.getDeploymentFee(standard, implId); + if (msg.value < deploymentFee) { + revert InsufficientDeploymentFee(); + } + + // Create a deterministic clone of the implementation contract + address instance = address(new ZKProxy{salt: salt}(impl)); + + // Initialize the newly created contract + (bool success,) = instance.call(abi.encodeWithSelector(INITIALIZE_SELECTOR, name, symbol, initialOwner)); + if (!success) { + revert InitializationFailed(); + } + + emit NewContractInitialized({ + contractAddress: instance, + initialOwner: initialOwner, + implId: implId, + standard: standard, + name: name, + symbol: symbol + }); + + return instance; + } + + /// @notice Creates a new clone of a MagicDrop contract + /// @param name The name of the new contract + /// @param symbol The symbol of the new contract + /// @param standard The token standard of the new contract + /// @param initialOwner The initial owner of the new contract + /// @param implId The implementation ID + /// @return The address of the newly created contract + function createContract( + string calldata name, + string calldata symbol, + TokenStandard standard, + address payable initialOwner, + uint32 implId + ) external payable returns (address) { + address impl; + // Retrieve the implementation address from the registry + if (implId == 0) { + impl = _registry.getDefaultImplementation(standard); + } else { + impl = _registry.getImplementation(standard, implId); + } + + if (initialOwner == address(0)) { + revert InitialOwnerCannotBeZero(); + } + + // Retrieve the deployment fee for the implementation and ensure the caller has sent the correct amount + uint256 deploymentFee = _registry.getDeploymentFee(standard, implId); + if (msg.value < deploymentFee) { + revert InsufficientDeploymentFee(); + } + + // Create a non-deterministic clone of the implementation contract + address instance = address(new ZKProxy(impl)); + + // Initialize the newly created contract + (bool success,) = instance.call(abi.encodeWithSelector(INITIALIZE_SELECTOR, name, symbol, initialOwner)); + if (!success) { + revert InitializationFailed(); + } + + emit NewContractInitialized({ + contractAddress: instance, + initialOwner: initialOwner, + implId: implId, + standard: standard, + name: name, + symbol: symbol + }); + + return instance; + } + + /*============================================================== + = PUBLIC VIEW METHODS = + ==============================================================*/ + + /// @notice Retrieves the address of the registry contract + /// @return The address of the registry contract + function getRegistry() external view returns (address) { + return address(_registry); + } + + /*============================================================== + = ADMIN OPERATIONS = + ==============================================================*/ + + /// @notice Withdraws the contract's balance + function withdraw(address to) external onlyOwner { + (bool success,) = to.call{value: address(this).balance}(""); + if (!success) { + revert WithdrawalFailed(); + } + + emit Withdrawal(to, address(this).balance); + } + + /// @dev Overriden to prevent double-initialization of the owner. + function _guardInitializeOwner() internal pure virtual override returns (bool) { + return true; + } + + /// @notice Receives ETH + receive() external payable {} + + /// @notice Fallback function to receive ETH + fallback() external payable {} +} diff --git a/contracts/factory/zksync/ZKProxy.sol b/contracts/factory/zksync/ZKProxy.sol new file mode 100644 index 00000000..e7b57f2b --- /dev/null +++ b/contracts/factory/zksync/ZKProxy.sol @@ -0,0 +1,36 @@ +pragma solidity ^0.8.20; + +// This is a ZKSync compatible proxy and a replacement for OZ Clones +contract ZKProxy { + address immutable implementation; + + constructor(address _implementation) { + implementation = _implementation; + } + + fallback() external payable { + address impl = implementation; + assembly { + // The pointer to the free memory slot + let ptr := mload(0x40) + // Copy function signature and arguments from calldata at zero position into memory at pointer position + calldatacopy(ptr, 0, calldatasize()) + // Delegatecall method of the implementation contract returns 0 on error + let result := delegatecall(gas(), impl, ptr, calldatasize(), 0, 0) + // Get the size of the last return data + let size := returndatasize() + // Copy the size length of bytes from return data at zero position to pointer position + returndatacopy(ptr, 0, size) + // Depending on the result value + switch result + case 0 { + // End execution and revert state changes + revert(ptr, size) + } + default { + // Return data with length of size at pointers position + return(ptr, size) + } + } + } +} \ No newline at end of file diff --git a/test/factory/MagicDropCloneFactoryTest.t.sol b/test/factory/MagicDropCloneFactoryTest.t.sol index 006e9bc6..32a182bd 100644 --- a/test/factory/MagicDropCloneFactoryTest.t.sol +++ b/test/factory/MagicDropCloneFactoryTest.t.sol @@ -7,7 +7,7 @@ import {MockERC721} from "solady/test/utils/mocks/MockERC721.sol"; import {MockERC1155} from "solady/test/utils/mocks/MockERC1155.sol"; import {LibClone} from "solady/src/utils/LibClone.sol"; -import {MagicDropCloneFactory} from "../../contracts/factory/MagicDropCloneFactory.sol"; +import {MagicDropCloneFactory} from "../../contracts/factory/zksync/MagicDropCloneFactory.sol"; import {MagicDropTokenImplRegistry} from "../../contracts/registry/MagicDropTokenImplRegistry.sol"; import {TokenStandard} from "../../contracts/common/Structs.sol"; @@ -121,24 +121,6 @@ contract MagicDropCloneFactoryTest is Test { vm.stopPrank(); } - function testCreateContractWithDifferentSalts(uint256 numSalts) public { - vm.startPrank(user); - - numSalts = bound(numSalts, 10, 100); - bytes32[] memory salts = new bytes32[](numSalts); - - for (uint256 i = 0; i < numSalts; i++) { - salts[i] = keccak256(abi.encodePacked(i, block.timestamp, msg.sender)); - address predictedAddress = factory.predictDeploymentAddress(TokenStandard.ERC721, erc721ImplId, salts[i]); - address deployedAddress = factory.createContractDeterministic{value: 0.01 ether}( - "TestNFT", "TNFT", TokenStandard.ERC721, payable(user), erc721ImplId, salts[i] - ); - assertEq(predictedAddress, deployedAddress); - } - - vm.stopPrank(); - } - function testFailCreateContractWithInvalidImplementation() public { uint32 invalidImplId = 999; @@ -158,26 +140,6 @@ contract MagicDropCloneFactoryTest is Test { ); } - function testFailContractAlreadyDeployed() public { - bytes32 salt = bytes32(uint256(1)); - uint32 implId = 1; - TokenStandard standard = TokenStandard.ERC721; - address initialOwner = address(0x1); - string memory name = "TestToken"; - string memory symbol = "TT"; - - // Predict the address where the contract will be deployed - address predictedAddress = factory.predictDeploymentAddress(standard, implId, salt); - - // Deploy a dummy contract to the predicted address - vm.etch(predictedAddress, address(erc721Impl).code); - - // Try to create a contract with the same parameters - factory.createContractDeterministic{value: 0.01 ether}( - name, symbol, standard, payable(initialOwner), implId, salt - ); - } - function testInitializationFailed() public { TokenStandard standard = TokenStandard.ERC721; From d5195a2b03b514e772887d3e677d8029be5b26a5 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Fri, 24 Jan 2025 09:24:46 -0500 Subject: [PATCH 2/4] move Signed-off-by: Adam Wolf --- test/factory/{ => zksync}/MagicDropCloneFactoryTest.t.sol | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/factory/{ => zksync}/MagicDropCloneFactoryTest.t.sol (100%) diff --git a/test/factory/MagicDropCloneFactoryTest.t.sol b/test/factory/zksync/MagicDropCloneFactoryTest.t.sol similarity index 100% rename from test/factory/MagicDropCloneFactoryTest.t.sol rename to test/factory/zksync/MagicDropCloneFactoryTest.t.sol From 62f1c7b26f10b47d4f18a5b36d178d0726f04908 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Fri, 24 Jan 2025 09:25:30 -0500 Subject: [PATCH 3/4] add Signed-off-by: Adam Wolf --- test/factory/MagicDropCloneFactoryTest.t.sol | 225 +++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 test/factory/MagicDropCloneFactoryTest.t.sol diff --git a/test/factory/MagicDropCloneFactoryTest.t.sol b/test/factory/MagicDropCloneFactoryTest.t.sol new file mode 100644 index 00000000..006e9bc6 --- /dev/null +++ b/test/factory/MagicDropCloneFactoryTest.t.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {console} from "forge-std/console.sol"; +import {Test} from "forge-std/Test.sol"; +import {MockERC721} from "solady/test/utils/mocks/MockERC721.sol"; +import {MockERC1155} from "solady/test/utils/mocks/MockERC1155.sol"; +import {LibClone} from "solady/src/utils/LibClone.sol"; + +import {MagicDropCloneFactory} from "../../contracts/factory/MagicDropCloneFactory.sol"; +import {MagicDropTokenImplRegistry} from "../../contracts/registry/MagicDropTokenImplRegistry.sol"; +import {TokenStandard} from "../../contracts/common/Structs.sol"; + +contract MockERC721Initializable is MockERC721 { + function initialize(string memory, string memory, address) public {} +} + +contract MockERC1155Initializable is MockERC1155 { + function initialize(string memory, string memory, address) public {} +} + +contract InvalidImplementation is MockERC721 { + function initialize(string memory) public {} // Missing name and symbol parameters +} + +contract MagicDropCloneFactoryTest is Test { + MagicDropCloneFactory internal factory; + MagicDropTokenImplRegistry internal registry; + + MockERC721Initializable internal erc721Impl; + MockERC1155Initializable internal erc1155Impl; + address internal owner = payable(address(0x1)); + address internal user = payable(address(0x2)); + + uint32 internal erc721ImplId; + uint32 internal erc1155ImplId; + + function setUp() public { + vm.startPrank(owner); + + // Deploy and initialize registry + MagicDropTokenImplRegistry registryImpl = new MagicDropTokenImplRegistry(owner); + registry = MagicDropTokenImplRegistry(address(registryImpl)); + + // Deploy factory + MagicDropCloneFactory factoryImpl = new MagicDropCloneFactory(owner, address(registry)); + factory = MagicDropCloneFactory(payable(address(factoryImpl))); + + // Deploy implementations + erc721Impl = new MockERC721Initializable(); + erc1155Impl = new MockERC1155Initializable(); + + // Register implementations + erc721ImplId = registry.registerImplementation(TokenStandard.ERC721, address(erc721Impl), true, 0.01 ether); + erc1155ImplId = registry.registerImplementation(TokenStandard.ERC1155, address(erc1155Impl), true, 0.01 ether); + + // Fund user + vm.deal(user, 100 ether); + + vm.stopPrank(); + } + + function testCreateERC721Contract() public { + vm.startPrank(user); + + address newContract = factory.createContract{value: 0.01 ether}( + "TestNFT", "TNFT", TokenStandard.ERC721, payable(user), erc721ImplId + ); + + MockERC721Initializable nft = MockERC721Initializable(newContract); + + // Test minting + nft.mint(user, 1); + assertEq(nft.ownerOf(1), user); + + vm.stopPrank(); + } + + function testCreateERC721ContractWithDefaultImplementation() public { + vm.startPrank(user); + + address newContract = + factory.createContract{value: 0.01 ether}("TestNFT", "TNFT", TokenStandard.ERC721, payable(user), 0); + + MockERC721Initializable nft = MockERC721Initializable(newContract); + // Test minting + nft.mint(user, 1); + assertEq(nft.ownerOf(1), user); + + vm.stopPrank(); + } + + function testCreateERC1155Contract() public { + vm.startPrank(user); + + address newContract = factory.createContract{value: 0.01 ether}( + "TestMultiToken", "TMT", TokenStandard.ERC1155, payable(user), erc1155ImplId + ); + + MockERC1155Initializable nft = MockERC1155Initializable(newContract); + + // Test minting + nft.mint(user, 1, 100, ""); + assertEq(nft.balanceOf(user, 1), 100); + + vm.stopPrank(); + } + + function testCreateERC1155ContractWithDefaultImplementation() public { + vm.startPrank(user); + + address newContract = + factory.createContract{value: 0.01 ether}("TestMultiToken", "TMT", TokenStandard.ERC1155, payable(user), 0); + + MockERC1155Initializable nft = MockERC1155Initializable(newContract); + + // Test minting + nft.mint(user, 1, 100, ""); + assertEq(nft.balanceOf(user, 1), 100); + + vm.stopPrank(); + } + + function testCreateContractWithDifferentSalts(uint256 numSalts) public { + vm.startPrank(user); + + numSalts = bound(numSalts, 10, 100); + bytes32[] memory salts = new bytes32[](numSalts); + + for (uint256 i = 0; i < numSalts; i++) { + salts[i] = keccak256(abi.encodePacked(i, block.timestamp, msg.sender)); + address predictedAddress = factory.predictDeploymentAddress(TokenStandard.ERC721, erc721ImplId, salts[i]); + address deployedAddress = factory.createContractDeterministic{value: 0.01 ether}( + "TestNFT", "TNFT", TokenStandard.ERC721, payable(user), erc721ImplId, salts[i] + ); + assertEq(predictedAddress, deployedAddress); + } + + vm.stopPrank(); + } + + function testFailCreateContractWithInvalidImplementation() public { + uint32 invalidImplId = 999; + + vm.prank(user); + factory.createContract("TestNFT", "TNFT", TokenStandard.ERC721, payable(user), invalidImplId); + } + + function testFailCreateDeterministicContractWithSameSalt() public { + vm.startPrank(user); + + factory.createContractDeterministic{value: 0.01 ether}( + "TestNFT1", "TNFT1", TokenStandard.ERC721, payable(user), erc721ImplId, bytes32(0) + ); + + factory.createContractDeterministic{value: 0.01 ether}( + "TestNFT2", "TNFT2", TokenStandard.ERC721, payable(user), erc721ImplId, bytes32(0) + ); + } + + function testFailContractAlreadyDeployed() public { + bytes32 salt = bytes32(uint256(1)); + uint32 implId = 1; + TokenStandard standard = TokenStandard.ERC721; + address initialOwner = address(0x1); + string memory name = "TestToken"; + string memory symbol = "TT"; + + // Predict the address where the contract will be deployed + address predictedAddress = factory.predictDeploymentAddress(standard, implId, salt); + + // Deploy a dummy contract to the predicted address + vm.etch(predictedAddress, address(erc721Impl).code); + + // Try to create a contract with the same parameters + factory.createContractDeterministic{value: 0.01 ether}( + name, symbol, standard, payable(initialOwner), implId, salt + ); + } + + function testInitializationFailed() public { + TokenStandard standard = TokenStandard.ERC721; + + vm.startPrank(owner); + InvalidImplementation impl = new InvalidImplementation(); + uint32 implId = registry.registerImplementation(standard, address(impl), false, 0.01 ether); + vm.stopPrank(); + + vm.expectRevert(MagicDropCloneFactory.InitializationFailed.selector); + factory.createContractDeterministic{value: 0.01 ether}( + "TestNFT", "TNFT", standard, payable(user), implId, bytes32(0) + ); + } + + function testInsufficientDeploymentFee() public { + vm.startPrank(user); + vm.expectRevert(MagicDropCloneFactory.InsufficientDeploymentFee.selector); + factory.createContractDeterministic{value: 0.005 ether}( + "TestNFT", "TNFT", TokenStandard.ERC721, payable(user), erc721ImplId, bytes32(0) + ); + } + + function testGetRegistry() public view { + assertEq(factory.getRegistry(), address(registry)); + } + + function testWithdraw() public { + vm.startPrank(user); + factory.createContract{value: 0.01 ether}("TestMultiToken", "TMT", TokenStandard.ERC1155, payable(user), 0); + vm.stopPrank(); + + vm.startPrank(owner); + uint256 userBalanceBefore = user.balance; + assertEq(address(factory).balance, 0.01 ether); + factory.withdraw(user); + assertEq(address(factory).balance, 0); + assertEq(user.balance, userBalanceBefore + 0.01 ether); + vm.stopPrank(); + } + + function testFailWithdrawToNonOwner() public { + vm.startPrank(user); + factory.withdraw(user); + } +} From d1b8b1fc94fc7f8fc71a492f50ba5305df1a9121 Mon Sep 17 00:00:00 2001 From: Adam Wolf Date: Fri, 24 Jan 2025 09:37:00 -0500 Subject: [PATCH 4/4] cleanup Signed-off-by: Adam Wolf --- contracts/factory/zksync/MagicDropCloneFactory.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/factory/zksync/MagicDropCloneFactory.sol b/contracts/factory/zksync/MagicDropCloneFactory.sol index 3146ccf6..53ee9844 100644 --- a/contracts/factory/zksync/MagicDropCloneFactory.sol +++ b/contracts/factory/zksync/MagicDropCloneFactory.sol @@ -7,8 +7,7 @@ import {MagicDropTokenImplRegistry} from "../../registry/MagicDropTokenImplRegis import {ZKProxy} from "./ZKProxy.sol"; /// @title MagicDropCloneFactory -/// @notice A factory contract for creating and managing clones of MagicDrop contracts -/// @dev This contract uses the UUPS proxy pattern +/// @notice A zksync compatible factory contract for creating and managing clones of MagicDrop contracts contract MagicDropCloneFactory is Ownable { /*============================================================== = CONSTANTS =