diff --git a/contracts/factory/zksync/MagicDropCloneFactory.sol b/contracts/factory/zksync/MagicDropCloneFactory.sol new file mode 100644 index 00000000..15702bb0 --- /dev/null +++ b/contracts/factory/zksync/MagicDropCloneFactory.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import {Ownable} from "solady/src/auth/Ownable.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {TokenStandard} from "../../common/Structs.sol"; +import {MagicDropTokenImplRegistry} from "../../registry/MagicDropTokenImplRegistry.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 = Clones.cloneDeterministic(impl, salt); + + // 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 = Clones.clone(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 Predicts the deployment address of a deterministic clone + /// @param standard The token standard of the contract + /// @param implId The implementation ID + /// @param salt The salt used for address generation + /// @return The predicted deployment address + function predictDeploymentAddress(TokenStandard standard, uint32 implId, bytes32 salt) + external + view + returns (address) + { + address impl; + if (implId == 0) { + impl = _registry.getDefaultImplementation(standard); + } else { + impl = _registry.getImplementation(standard, implId); + } + return Clones.predictDeterministicAddress(impl, salt, address(this)); + } + + /// @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/lib/solady b/lib/solady index 362b2efd..6122858a 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit 362b2efd20f38aea7252b391e5e016633ff79641 +Subproject commit 6122858a3aed96ee9493b99f70a245237681a95f diff --git a/test/factory/MagicDropCloneFactoryTest.t.sol b/test/factory/MagicDropCloneFactoryTest.t.sol index 006e9bc6..7d601b9e 100644 --- a/test/factory/MagicDropCloneFactoryTest.t.sol +++ b/test/factory/MagicDropCloneFactoryTest.t.sol @@ -5,8 +5,6 @@ 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"; diff --git a/test/factory/MagicDropTokenImplRegistryTest.t.sol b/test/factory/MagicDropTokenImplRegistryTest.t.sol index 781d1e68..93da0ba7 100644 --- a/test/factory/MagicDropTokenImplRegistryTest.t.sol +++ b/test/factory/MagicDropTokenImplRegistryTest.t.sol @@ -5,7 +5,6 @@ import {Test} from "forge-std/Test.sol"; import {MagicDropTokenImplRegistry} from "../../contracts/registry/MagicDropTokenImplRegistry.sol"; import {TokenStandard} from "../../contracts/common/Structs.sol"; import {MockERC721} from "solady/test/utils/mocks/MockERC721.sol"; -import {LibClone} from "solady/src/utils/LibClone.sol"; import {IERC165} from "openzeppelin-contracts/contracts/interfaces/IERC165.sol"; contract MagicDropTokenImplRegistryTest is Test {