diff --git a/contracts/src/CollectionsProtocolHandler.sol b/contracts/src/CollectionsProtocolHandler.sol index 0500f17..073337e 100644 --- a/contracts/src/CollectionsProtocolHandler.sol +++ b/contracts/src/CollectionsProtocolHandler.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; -import "@openzeppelin/contracts/proxy/Clones.sol"; +import "@openzeppelin/contracts/utils/Create2.sol"; import {LibString} from "solady/utils/LibString.sol"; import "./CollectionsERC721.sol"; +import "./libraries/Proxy.sol"; import "./Ethscriptions.sol"; import "./libraries/Predeploys.sol"; import "./interfaces/IProtocolHandler.sol"; contract CollectionsProtocolHandler is IProtocolHandler { - using Clones for address; using LibString for string; // Standard NFT attribute structure @@ -96,7 +96,7 @@ contract CollectionsProtocolHandler is IProtocolHandler { // Note: Similar to ItemData but without ethscriptionId (can't change) } - address public constant collectionsTemplate = Predeploys.COLLECTIONS_TEMPLATE_IMPLEMENTATION; + address public constant collectionsImplementation = Predeploys.COLLECTIONS_TEMPLATE_IMPLEMENTATION; address public constant ethscriptions = Predeploys.ETHSCRIPTIONS; // Track deployed collections by ID @@ -156,19 +156,23 @@ contract CollectionsProtocolHandler is IProtocolHandler { // Get totalSupply from metadata uint256 totalSupply = metadata.totalSupply; - // Deploy ERC721 clone with CREATE2 using collectionId as salt for deterministic address - address collectionContract = collectionsTemplate.cloneDeterministic(collectionId); + // Deploy ERC721 proxy with CREATE2 using collectionId as salt for deterministic address + Proxy collectionProxy = new Proxy{salt: collectionId}(address(this)); - // Initialize the clone with basic info - CollectionsERC721(collectionContract).initialize( + // Initialize implementation via proxy + bytes memory initCalldata = abi.encodeWithSelector( + CollectionsERC721.initialize.selector, metadata.name, metadata.symbol, collectionId ); + collectionProxy.upgradeToAndCall(collectionsImplementation, initCalldata); + // Hand over admin to global ProxyAdmin + collectionProxy.changeAdmin(Predeploys.PROXY_ADMIN); // Store collection state collectionState[collectionId] = CollectionState({ - collectionContract: collectionContract, + collectionContract: address(collectionProxy), createEthscriptionId: txHash, currentSize: 0, locked: false @@ -179,7 +183,7 @@ contract CollectionsProtocolHandler is IProtocolHandler { collectionIds.push(collectionId); - emit CollectionCreated(collectionId, collectionContract, metadata.name, metadata.symbol, totalSupply); + emit CollectionCreated(collectionId, address(collectionProxy), metadata.name, metadata.symbol, totalSupply); emit ProtocolHandlerSuccess(txHash, protocolName()); } @@ -459,7 +463,8 @@ contract CollectionsProtocolHandler is IProtocolHandler { } // Predict using CREATE2 - return Clones.predictDeterministicAddress(collectionsTemplate, collectionId, address(this)); + bytes memory creationCode = abi.encodePacked(type(Proxy).creationCode, abi.encode(address(this))); + return Create2.computeAddress(collectionId, keccak256(creationCode), address(this)); } function getAllCollections() external view returns (bytes32[] memory) { diff --git a/contracts/src/FixedFungibleProtocolHandler.sol b/contracts/src/FixedFungibleProtocolHandler.sol index be58752..152afd8 100644 --- a/contracts/src/FixedFungibleProtocolHandler.sol +++ b/contracts/src/FixedFungibleProtocolHandler.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; -import "@openzeppelin/contracts/proxy/Clones.sol"; +import "@openzeppelin/contracts/utils/Create2.sol"; import {LibString} from "solady/utils/LibString.sol"; import "./FixedFungibleERC20.sol"; +import "./libraries/Proxy.sol"; import "./Ethscriptions.sol"; import "./libraries/Predeploys.sol"; import "./interfaces/IProtocolHandler.sol"; @@ -12,7 +13,6 @@ import "./interfaces/IProtocolHandler.sol"; /// @notice Implements the fixed-fungible token protocol enforced via Ethscriptions transfers /// @dev Deploys and controls FixedFungible ERC-20 clones; callable only by the Ethscriptions contract contract FixedFungibleProtocolHandler is IProtocolHandler { - using Clones for address; using LibString for string; // ============================================================= @@ -50,8 +50,8 @@ contract FixedFungibleProtocolHandler is IProtocolHandler { // CONSTANTS // ============================================================= - /// @dev Deterministic template contract used for clone deployments - address public constant fixedFungibleTemplate = Predeploys.FIXED_FUNGIBLE_TEMPLATE_IMPLEMENTATION; + /// @dev Implementation contract used for proxy deployments + address public constant fixedFungibleImplementation = Predeploys.FIXED_FUNGIBLE_TEMPLATE_IMPLEMENTATION; address public constant ethscriptions = Predeploys.ETHSCRIPTIONS; string public constant CANONICAL_PROTOCOL = "fixed-fungible"; string public constant PRETTY_PROTOCOL = "Fixed-Fungible"; @@ -140,25 +140,29 @@ contract FixedFungibleProtocolHandler is IProtocolHandler { if (deployOp.mintAmount == 0) revert InvalidMintAmount(); if (deployOp.maxSupply % deployOp.mintAmount != 0) revert MaxSupplyNotDivisibleByMintAmount(); - // Deploy ERC20 clone with CREATE2 using tickKey as salt for deterministic address - address tokenAddress = fixedFungibleTemplate.cloneDeterministic(tickKey); + // Deploy a Proxy via CREATE2 with this handler as temporary admin for initialization + Proxy tokenProxy = new Proxy{salt: tickKey}(address(this)); - // Initialize the clone + // Build name/symbol and initialization calldata string memory name = string.concat(PRETTY_PROTOCOL, " ", deployOp.tick); string memory symbol = LibString.upper(deployOp.tick); - // Initialize with max supply in 18 decimals // User maxSupply "1000000" means 1000000 * 10^18 smallest units - FixedFungibleERC20(tokenAddress).initialize( + bytes memory initCalldata = abi.encodeWithSelector( + FixedFungibleERC20.initialize.selector, name, symbol, deployOp.maxSupply * 10**18, ethscriptionId ); + // Set implementation and run initialize as admin + tokenProxy.upgradeToAndCall(fixedFungibleImplementation, initCalldata); + // Hand off admin to global ProxyAdmin so upgrades require system governance + tokenProxy.changeAdmin(Predeploys.PROXY_ADMIN); // Store token info tokensByTick[tickKey] = TokenInfo({ - tokenContract: tokenAddress, + tokenContract: address(tokenProxy), deployEthscriptionId: ethscriptionId, tick: deployOp.tick, maxSupply: deployOp.maxSupply, @@ -171,7 +175,7 @@ contract FixedFungibleProtocolHandler is IProtocolHandler { emit FixedFungibleTokenDeployed( ethscriptionId, - tokenAddress, + address(tokenProxy), deployOp.tick, deployOp.maxSupply, deployOp.mintAmount @@ -290,8 +294,9 @@ contract FixedFungibleProtocolHandler is IProtocolHandler { return tokensByTick[tickKey].tokenContract; } - // Predict using CREATE2 - return Clones.predictDeterministicAddress(fixedFungibleTemplate, tickKey, address(this)); + // Predict using CREATE2 for Proxy with constructor arg (admin = address(this)) + bytes memory creationCode = abi.encodePacked(type(Proxy).creationCode, abi.encode(address(this))); + return Create2.computeAddress(tickKey, keccak256(creationCode), address(this)); } /// @notice Check if an ethscription is a token item diff --git a/contracts/src/L2/ProxyAdmin.sol b/contracts/src/L2/ProxyAdmin.sol index 2c60ad5..c95c82d 100644 --- a/contracts/src/L2/ProxyAdmin.sol +++ b/contracts/src/L2/ProxyAdmin.sol @@ -27,21 +27,21 @@ contract ProxyAdmin is Ownable { /// @notice Returns the admin of the given proxy address. /// @param _proxy Address of the proxy to get the admin of. /// @return Address of the admin of the proxy. - function getProxyAdmin(address payable _proxy) external view returns (address) { + function getProxyAdmin(address _proxy) external view returns (address) { return IStaticERC1967Proxy(_proxy).admin(); } /// @notice Updates the admin of the given proxy address. /// @param _proxy Address of the proxy to update. /// @param _newAdmin Address of the new proxy admin. - function changeProxyAdmin(address payable _proxy, address _newAdmin) external onlyOwner { + function changeProxyAdmin(address _proxy, address _newAdmin) external onlyOwner { Proxy(_proxy).changeAdmin(_newAdmin); } /// @notice Changes a proxy's implementation contract. /// @param _proxy Address of the proxy to upgrade. /// @param _implementation Address of the new implementation address. - function upgrade(address payable _proxy, address _implementation) public onlyOwner { + function upgrade(address _proxy, address _implementation) public onlyOwner { Proxy(_proxy).upgradeTo(_implementation); } @@ -51,14 +51,10 @@ contract ProxyAdmin is Ownable { /// @param _implementation Address of the new implementation address. /// @param _data Data to trigger the new implementation with. function upgradeAndCall( - address payable _proxy, + address _proxy, address _implementation, bytes memory _data - ) - external - payable - onlyOwner - { - Proxy(_proxy).upgradeToAndCall{ value: msg.value }(_implementation, _data); + ) external onlyOwner { + Proxy(_proxy).upgradeToAndCall(_implementation, _data); } -} \ No newline at end of file +} diff --git a/contracts/src/libraries/Proxy.sol b/contracts/src/libraries/Proxy.sol index c1c759d..b54230a 100644 --- a/contracts/src/libraries/Proxy.sol +++ b/contracts/src/libraries/Proxy.sol @@ -63,7 +63,6 @@ contract Proxy { bytes calldata _data ) public - payable virtual proxyCallIfNotAdmin returns (bytes memory) diff --git a/contracts/test/AddressPrediction.t.sol b/contracts/test/AddressPrediction.t.sol new file mode 100644 index 0000000..c4a8f2e --- /dev/null +++ b/contracts/test/AddressPrediction.t.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import "../src/libraries/Predeploys.sol"; +import "../src/libraries/Proxy.sol"; +import "../src/FixedFungibleProtocolHandler.sol"; +import "../src/CollectionsProtocolHandler.sol"; +import "../src/Ethscriptions.sol"; +import "@openzeppelin/contracts/utils/Create2.sol"; +import "./TestSetup.sol"; + +contract AddressPredictionTest is TestSetup { + // Test predictable address for FixedFungibleProtocolHandler token proxies + function testPredictFixedFungibleTokenAddress() public { + // Arrange + string memory tick = "eths"; + bytes32 deployTxHash = keccak256("deploy-eths"); + + // Prepare deploy op data + FixedFungibleProtocolHandler.DeployOperation memory deployOp = FixedFungibleProtocolHandler.DeployOperation({ + tick: tick, + maxSupply: 1_000_000, + mintAmount: 1_000 + }); + bytes memory data = abi.encode(deployOp); + + // Prediction via contract helper + address predicted = fixedFungibleHandler.predictTokenAddressByTick(tick); + + // Act: call deploy as Ethscriptions (authorized) + vm.prank(Predeploys.ETHSCRIPTIONS); + fixedFungibleHandler.op_deploy(deployTxHash, data); + + // Assert actual matches predicted + address actual = fixedFungibleHandler.getTokenAddressByTick(tick); + assertEq(actual, predicted, "Predicted token address should match actual deployed proxy"); + } + + // Test predictable address for CollectionsProtocolHandler collection proxies + function testPredictCollectionsAddress() public { + // Arrange + bytes32 collectionId = keccak256("collection-1"); + + CollectionsProtocolHandler.CollectionMetadata memory metadata = CollectionsProtocolHandler.CollectionMetadata({ + name: "My Collection", + symbol: "MYC", + totalSupply: 1000, + description: "A test collection", + logoImageUri: "data:,logo", + bannerImageUri: "data:,banner", + backgroundColor: "#000000", + websiteLink: "https://example.com", + twitterLink: "", + discordLink: "" + }); + + // Manually compute predicted proxy address + bytes memory creationCode = abi.encodePacked(type(Proxy).creationCode, abi.encode(address(collectionsHandler))); + address predicted = Create2.computeAddress(collectionId, keccak256(creationCode), address(collectionsHandler)); + + // Act: create collection as Ethscriptions (authorized) + vm.prank(Predeploys.ETHSCRIPTIONS); + collectionsHandler.op_create_collection(collectionId, abi.encode(metadata)); + + // Assert deployed matches predicted + (address actual,,,) = collectionsHandler.collectionState(collectionId); + assertEq(actual, predicted, "Predicted collection address should match actual deployed proxy"); + } +}