Skip to content

Commit 660b651

Browse files
committed
Use upgradeable proxies for tokens and collections
1 parent 83e4f49 commit 660b651

File tree

5 files changed

+110
-35
lines changed

5 files changed

+110
-35
lines changed

contracts/src/CollectionsProtocolHandler.sol

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity 0.8.24;
33

4-
import "@openzeppelin/contracts/proxy/Clones.sol";
4+
import "@openzeppelin/contracts/utils/Create2.sol";
55
import {LibString} from "solady/utils/LibString.sol";
66
import "./CollectionsERC721.sol";
7+
import "./libraries/Proxy.sol";
78
import "./Ethscriptions.sol";
89
import "./libraries/Predeploys.sol";
910
import "./interfaces/IProtocolHandler.sol";
1011

1112
contract CollectionsProtocolHandler is IProtocolHandler {
12-
using Clones for address;
1313
using LibString for string;
1414

1515
// Standard NFT attribute structure
@@ -96,7 +96,7 @@ contract CollectionsProtocolHandler is IProtocolHandler {
9696
// Note: Similar to ItemData but without ethscriptionId (can't change)
9797
}
9898

99-
address public constant collectionsTemplate = Predeploys.COLLECTIONS_TEMPLATE_IMPLEMENTATION;
99+
address public constant collectionsImplementation = Predeploys.COLLECTIONS_TEMPLATE_IMPLEMENTATION;
100100
address public constant ethscriptions = Predeploys.ETHSCRIPTIONS;
101101

102102
// Track deployed collections by ID
@@ -156,19 +156,23 @@ contract CollectionsProtocolHandler is IProtocolHandler {
156156
// Get totalSupply from metadata
157157
uint256 totalSupply = metadata.totalSupply;
158158

159-
// Deploy ERC721 clone with CREATE2 using collectionId as salt for deterministic address
160-
address collectionContract = collectionsTemplate.cloneDeterministic(collectionId);
159+
// Deploy ERC721 proxy with CREATE2 using collectionId as salt for deterministic address
160+
Proxy collectionProxy = new Proxy{salt: collectionId}(address(this));
161161

162-
// Initialize the clone with basic info
163-
CollectionsERC721(collectionContract).initialize(
162+
// Initialize implementation via proxy
163+
bytes memory initCalldata = abi.encodeWithSelector(
164+
CollectionsERC721.initialize.selector,
164165
metadata.name,
165166
metadata.symbol,
166167
collectionId
167168
);
169+
collectionProxy.upgradeToAndCall(collectionsImplementation, initCalldata);
170+
// Hand over admin to global ProxyAdmin
171+
collectionProxy.changeAdmin(Predeploys.PROXY_ADMIN);
168172

169173
// Store collection state
170174
collectionState[collectionId] = CollectionState({
171-
collectionContract: collectionContract,
175+
collectionContract: address(collectionProxy),
172176
createEthscriptionId: txHash,
173177
currentSize: 0,
174178
locked: false
@@ -179,7 +183,7 @@ contract CollectionsProtocolHandler is IProtocolHandler {
179183

180184
collectionIds.push(collectionId);
181185

182-
emit CollectionCreated(collectionId, collectionContract, metadata.name, metadata.symbol, totalSupply);
186+
emit CollectionCreated(collectionId, address(collectionProxy), metadata.name, metadata.symbol, totalSupply);
183187
emit ProtocolHandlerSuccess(txHash, protocolName());
184188
}
185189

@@ -459,7 +463,8 @@ contract CollectionsProtocolHandler is IProtocolHandler {
459463
}
460464

461465
// Predict using CREATE2
462-
return Clones.predictDeterministicAddress(collectionsTemplate, collectionId, address(this));
466+
bytes memory creationCode = abi.encodePacked(type(Proxy).creationCode, abi.encode(address(this)));
467+
return Create2.computeAddress(collectionId, keccak256(creationCode), address(this));
463468
}
464469

465470
function getAllCollections() external view returns (bytes32[] memory) {

contracts/src/FixedFungibleProtocolHandler.sol

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// SPDX-License-Identifier: MIT
22
pragma solidity 0.8.24;
33

4-
import "@openzeppelin/contracts/proxy/Clones.sol";
4+
import "@openzeppelin/contracts/utils/Create2.sol";
55
import {LibString} from "solady/utils/LibString.sol";
66
import "./FixedFungibleERC20.sol";
7+
import "./libraries/Proxy.sol";
78
import "./Ethscriptions.sol";
89
import "./libraries/Predeploys.sol";
910
import "./interfaces/IProtocolHandler.sol";
@@ -12,7 +13,6 @@ import "./interfaces/IProtocolHandler.sol";
1213
/// @notice Implements the fixed-fungible token protocol enforced via Ethscriptions transfers
1314
/// @dev Deploys and controls FixedFungible ERC-20 clones; callable only by the Ethscriptions contract
1415
contract FixedFungibleProtocolHandler is IProtocolHandler {
15-
using Clones for address;
1616
using LibString for string;
1717

1818
// =============================================================
@@ -50,8 +50,8 @@ contract FixedFungibleProtocolHandler is IProtocolHandler {
5050
// CONSTANTS
5151
// =============================================================
5252

53-
/// @dev Deterministic template contract used for clone deployments
54-
address public constant fixedFungibleTemplate = Predeploys.FIXED_FUNGIBLE_TEMPLATE_IMPLEMENTATION;
53+
/// @dev Implementation contract used for proxy deployments
54+
address public constant fixedFungibleImplementation = Predeploys.FIXED_FUNGIBLE_TEMPLATE_IMPLEMENTATION;
5555
address public constant ethscriptions = Predeploys.ETHSCRIPTIONS;
5656
string public constant CANONICAL_PROTOCOL = "fixed-fungible";
5757
string public constant PRETTY_PROTOCOL = "Fixed-Fungible";
@@ -140,25 +140,29 @@ contract FixedFungibleProtocolHandler is IProtocolHandler {
140140
if (deployOp.mintAmount == 0) revert InvalidMintAmount();
141141
if (deployOp.maxSupply % deployOp.mintAmount != 0) revert MaxSupplyNotDivisibleByMintAmount();
142142

143-
// Deploy ERC20 clone with CREATE2 using tickKey as salt for deterministic address
144-
address tokenAddress = fixedFungibleTemplate.cloneDeterministic(tickKey);
143+
// Deploy a Proxy via CREATE2 with this handler as temporary admin for initialization
144+
Proxy tokenProxy = new Proxy{salt: tickKey}(address(this));
145145

146-
// Initialize the clone
146+
// Build name/symbol and initialization calldata
147147
string memory name = string.concat(PRETTY_PROTOCOL, " ", deployOp.tick);
148148
string memory symbol = LibString.upper(deployOp.tick);
149-
150149
// Initialize with max supply in 18 decimals
151150
// User maxSupply "1000000" means 1000000 * 10^18 smallest units
152-
FixedFungibleERC20(tokenAddress).initialize(
151+
bytes memory initCalldata = abi.encodeWithSelector(
152+
FixedFungibleERC20.initialize.selector,
153153
name,
154154
symbol,
155155
deployOp.maxSupply * 10**18,
156156
ethscriptionId
157157
);
158+
// Set implementation and run initialize as admin
159+
tokenProxy.upgradeToAndCall(fixedFungibleImplementation, initCalldata);
160+
// Hand off admin to global ProxyAdmin so upgrades require system governance
161+
tokenProxy.changeAdmin(Predeploys.PROXY_ADMIN);
158162

159163
// Store token info
160164
tokensByTick[tickKey] = TokenInfo({
161-
tokenContract: tokenAddress,
165+
tokenContract: address(tokenProxy),
162166
deployEthscriptionId: ethscriptionId,
163167
tick: deployOp.tick,
164168
maxSupply: deployOp.maxSupply,
@@ -171,7 +175,7 @@ contract FixedFungibleProtocolHandler is IProtocolHandler {
171175

172176
emit FixedFungibleTokenDeployed(
173177
ethscriptionId,
174-
tokenAddress,
178+
address(tokenProxy),
175179
deployOp.tick,
176180
deployOp.maxSupply,
177181
deployOp.mintAmount
@@ -290,8 +294,9 @@ contract FixedFungibleProtocolHandler is IProtocolHandler {
290294
return tokensByTick[tickKey].tokenContract;
291295
}
292296

293-
// Predict using CREATE2
294-
return Clones.predictDeterministicAddress(fixedFungibleTemplate, tickKey, address(this));
297+
// Predict using CREATE2 for Proxy with constructor arg (admin = address(this))
298+
bytes memory creationCode = abi.encodePacked(type(Proxy).creationCode, abi.encode(address(this)));
299+
return Create2.computeAddress(tickKey, keccak256(creationCode), address(this));
295300
}
296301

297302
/// @notice Check if an ethscription is a token item

contracts/src/L2/ProxyAdmin.sol

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,21 @@ contract ProxyAdmin is Ownable {
2727
/// @notice Returns the admin of the given proxy address.
2828
/// @param _proxy Address of the proxy to get the admin of.
2929
/// @return Address of the admin of the proxy.
30-
function getProxyAdmin(address payable _proxy) external view returns (address) {
30+
function getProxyAdmin(address _proxy) external view returns (address) {
3131
return IStaticERC1967Proxy(_proxy).admin();
3232
}
3333

3434
/// @notice Updates the admin of the given proxy address.
3535
/// @param _proxy Address of the proxy to update.
3636
/// @param _newAdmin Address of the new proxy admin.
37-
function changeProxyAdmin(address payable _proxy, address _newAdmin) external onlyOwner {
37+
function changeProxyAdmin(address _proxy, address _newAdmin) external onlyOwner {
3838
Proxy(_proxy).changeAdmin(_newAdmin);
3939
}
4040

4141
/// @notice Changes a proxy's implementation contract.
4242
/// @param _proxy Address of the proxy to upgrade.
4343
/// @param _implementation Address of the new implementation address.
44-
function upgrade(address payable _proxy, address _implementation) public onlyOwner {
44+
function upgrade(address _proxy, address _implementation) public onlyOwner {
4545
Proxy(_proxy).upgradeTo(_implementation);
4646
}
4747

@@ -51,14 +51,10 @@ contract ProxyAdmin is Ownable {
5151
/// @param _implementation Address of the new implementation address.
5252
/// @param _data Data to trigger the new implementation with.
5353
function upgradeAndCall(
54-
address payable _proxy,
54+
address _proxy,
5555
address _implementation,
5656
bytes memory _data
57-
)
58-
external
59-
payable
60-
onlyOwner
61-
{
62-
Proxy(_proxy).upgradeToAndCall{ value: msg.value }(_implementation, _data);
57+
) external onlyOwner {
58+
Proxy(_proxy).upgradeToAndCall(_implementation, _data);
6359
}
64-
}
60+
}

contracts/src/libraries/Proxy.sol

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ contract Proxy {
6363
bytes calldata _data
6464
)
6565
public
66-
payable
6766
virtual
6867
proxyCallIfNotAdmin
6968
returns (bytes memory)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import "forge-std/Test.sol";
5+
import "../src/libraries/Predeploys.sol";
6+
import "../src/libraries/Proxy.sol";
7+
import "../src/FixedFungibleProtocolHandler.sol";
8+
import "../src/CollectionsProtocolHandler.sol";
9+
import "../src/Ethscriptions.sol";
10+
import "@openzeppelin/contracts/utils/Create2.sol";
11+
import "./TestSetup.sol";
12+
13+
contract AddressPredictionTest is TestSetup {
14+
// Test predictable address for FixedFungibleProtocolHandler token proxies
15+
function testPredictFixedFungibleTokenAddress() public {
16+
// Arrange
17+
string memory tick = "eths";
18+
bytes32 deployTxHash = keccak256("deploy-eths");
19+
20+
// Prepare deploy op data
21+
FixedFungibleProtocolHandler.DeployOperation memory deployOp = FixedFungibleProtocolHandler.DeployOperation({
22+
tick: tick,
23+
maxSupply: 1_000_000,
24+
mintAmount: 1_000
25+
});
26+
bytes memory data = abi.encode(deployOp);
27+
28+
// Prediction via contract helper
29+
address predicted = fixedFungibleHandler.predictTokenAddressByTick(tick);
30+
31+
// Act: call deploy as Ethscriptions (authorized)
32+
vm.prank(Predeploys.ETHSCRIPTIONS);
33+
fixedFungibleHandler.op_deploy(deployTxHash, data);
34+
35+
// Assert actual matches predicted
36+
address actual = fixedFungibleHandler.getTokenAddressByTick(tick);
37+
assertEq(actual, predicted, "Predicted token address should match actual deployed proxy");
38+
}
39+
40+
// Test predictable address for CollectionsProtocolHandler collection proxies
41+
function testPredictCollectionsAddress() public {
42+
// Arrange
43+
bytes32 collectionId = keccak256("collection-1");
44+
45+
CollectionsProtocolHandler.CollectionMetadata memory metadata = CollectionsProtocolHandler.CollectionMetadata({
46+
name: "My Collection",
47+
symbol: "MYC",
48+
totalSupply: 1000,
49+
description: "A test collection",
50+
logoImageUri: "data:,logo",
51+
bannerImageUri: "data:,banner",
52+
backgroundColor: "#000000",
53+
websiteLink: "https://example.com",
54+
twitterLink: "",
55+
discordLink: ""
56+
});
57+
58+
// Manually compute predicted proxy address
59+
bytes memory creationCode = abi.encodePacked(type(Proxy).creationCode, abi.encode(address(collectionsHandler)));
60+
address predicted = Create2.computeAddress(collectionId, keccak256(creationCode), address(collectionsHandler));
61+
62+
// Act: create collection as Ethscriptions (authorized)
63+
vm.prank(Predeploys.ETHSCRIPTIONS);
64+
collectionsHandler.op_create_collection(collectionId, abi.encode(metadata));
65+
66+
// Assert deployed matches predicted
67+
(address actual,,,) = collectionsHandler.collectionState(collectionId);
68+
assertEq(actual, predicted, "Predicted collection address should match actual deployed proxy");
69+
}
70+
}

0 commit comments

Comments
 (0)