-
Notifications
You must be signed in to change notification settings - Fork 25
Use upgradeable proxies for tokens and collections #128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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)); | ||
|
Comment on lines
+143
to
+144
|
||
|
|
||
| // 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Proxy Upgrades Fail to Forward ETHThe Additional Locations (1) |
||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The handler contract temporarily assumes admin privileges during initialization. If this function can be called multiple times or reentered before admin transfer, it creates a window where the handler has elevated privileges. Consider adding reentrancy protection or documenting this assumption explicitly.