From 802248d00be50ae2c8a860266aeca9b13fe317c8 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Mon, 30 Oct 2023 13:37:19 +0000 Subject: [PATCH 1/3] Add support for CREATE2 clones and address prediction --- src/ClonesWithImmutableArgs.sol | 74 ++++++++++++++++++++++++++++-- src/ExampleCloneFactory.sol | 20 ++++++++ src/test/ExampleCloneFactory.t.sol | 39 ++++++++++++++++ 3 files changed, 129 insertions(+), 4 deletions(-) diff --git a/src/ClonesWithImmutableArgs.sol b/src/ClonesWithImmutableArgs.sol index 50594d0..de2f2d9 100644 --- a/src/ClonesWithImmutableArgs.sol +++ b/src/ClonesWithImmutableArgs.sol @@ -3,11 +3,17 @@ pragma solidity ^0.8.4; /// @title ClonesWithImmutableArgs -/// @author wighawag, zefram.eth +/// @author wighawag, zefram.eth, nick.eth /// @notice Enables creating clone contracts with immutable args library ClonesWithImmutableArgs { error CreateFail(); + enum CloneType { + CREATE, + CREATE2, + PREDICT_CREATE2 + } + /// @notice Creates a clone proxy of the implementation contract, with immutable args /// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length /// @param implementation The implementation contract to clone @@ -16,6 +22,44 @@ library ClonesWithImmutableArgs { function clone(address implementation, bytes memory data) internal returns (address payable instance) + { + return clone(implementation, data, CloneType.CREATE); + } + + /// @notice Creates a clone proxy of the implementation contract, with immutable args, + /// using CREATE2 + /// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length + /// @param implementation The implementation contract to clone + /// @param data Encoded immutable args + /// @return instance The address of the created clone + function clone2(address implementation, bytes memory data) + internal + returns (address payable instance) + { + return clone(implementation, data, CloneType.CREATE2); + } + + /// @notice Computes the address of a clone created using CREATE2 + /// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length + /// @param implementation The implementation contract to clone + /// @param data Encoded immutable args + /// @return instance The address of the clone + function predictAddress(address implementation, bytes memory data) + internal + returns (address payable instance) + { + return clone(implementation, data, CloneType.PREDICT_CREATE2); + } + + /// @notice Creates a clone proxy of the implementation contract, with immutable args + /// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length + /// @param implementation The implementation contract to clone + /// @param data Encoded immutable args + /// @param cloneType Whether to use CREATE or CREATE2 to create the clones + /// @return instance The address of the created clone + function clone(address implementation, bytes memory data, CloneType cloneType) + internal + returns (address payable instance) { // unrealistic for memory ptr or data length to exceed 256 bits unchecked { @@ -24,6 +68,7 @@ library ClonesWithImmutableArgs { uint256 runSize = creationSize - 10; uint256 dataPtr; uint256 ptr; + // solhint-disable-next-line no-inline-assembly assembly { ptr := mload(0x40) @@ -135,9 +180,30 @@ library ClonesWithImmutableArgs { assembly { mstore(copyPtr, shl(240, extraLength)) } - // solhint-disable-next-line no-inline-assembly - assembly { - instance := create(0, ptr, creationSize) + if(cloneType == CloneType.CREATE) { + // solhint-disable-next-line no-inline-assembly + assembly { + instance := create(0, ptr, creationSize) + } + } else if(cloneType == CloneType.CREATE2) { + // solhint-disable-next-line no-inline-assembly + assembly { + instance := create2(0, ptr, creationSize, 0) + } + } else if(cloneType == CloneType.PREDICT_CREATE2) { + bytes32 bytecodeHash; + // solhint-disable-next-line no-inline-assembly + assembly { + bytecodeHash := keccak256(ptr, creationSize) + } + instance = payable(address(uint160(uint(keccak256(abi.encodePacked( + bytes1(0xff), + address(this), + bytes32(0), + bytecodeHash + )))))); + } else { + revert CreateFail(); } if (instance == address(0)) { revert CreateFail(); diff --git a/src/ExampleCloneFactory.sol b/src/ExampleCloneFactory.sol index 43373c7..461488d 100644 --- a/src/ExampleCloneFactory.sol +++ b/src/ExampleCloneFactory.sol @@ -22,4 +22,24 @@ contract ExampleCloneFactory { bytes memory data = abi.encodePacked(param1, param2, param3, param4); clone = ExampleClone(address(implementation).clone(data)); } + + function createClone2( + address param1, + uint256 param2, + uint64 param3, + uint8 param4 + ) external returns (ExampleClone clone) { + bytes memory data = abi.encodePacked(param1, param2, param3, param4); + clone = ExampleClone(address(implementation).clone2(data)); + } + + function predictAddress( + address param1, + uint256 param2, + uint64 param3, + uint8 param4 + ) external returns (address clone) { + bytes memory data = abi.encodePacked(param1, param2, param3, param4); + clone = address(implementation).predictAddress(data); + } } diff --git a/src/test/ExampleCloneFactory.t.sol b/src/test/ExampleCloneFactory.t.sol index f056e2c..a4b938b 100644 --- a/src/test/ExampleCloneFactory.t.sol +++ b/src/test/ExampleCloneFactory.t.sol @@ -51,4 +51,43 @@ contract ExampleCloneFactoryTest is DSTest { assertEq(clone.param3(), param3); assertEq(clone.param4(), param4); } + + function testCorrectness_clone2( + address param1, + uint256 param2, + uint64 param3, + uint8 param4 + ) public { + ExampleClone clone = factory.createClone2( + param1, + param2, + param3, + param4 + ); + assertEq(clone.param1(), param1); + assertEq(clone.param2(), param2); + assertEq(clone.param3(), param3); + assertEq(clone.param4(), param4); + } + + function testCorrectness_predictAddress( + address param1, + uint256 param2, + uint64 param3, + uint8 param4 + ) public { + address predicted = factory.predictAddress( + param1, + param2, + param3, + param4 + ); + ExampleClone clone = factory.createClone2( + param1, + param2, + param3, + param4 + ); + assertEq(predicted, address(clone)); + } } From 64ad37aae5e8d6099ed5f2420923a92eaa8bafba Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Wed, 10 Jan 2024 11:42:06 +0000 Subject: [PATCH 2/3] Rename method, refactor so can be a view func --- src/ClonesWithImmutableArgs.sol | 73 ++++++++++++++---------------- src/ExampleCloneFactory.sol | 6 +-- src/test/ExampleCloneFactory.t.sol | 2 +- 3 files changed, 37 insertions(+), 44 deletions(-) diff --git a/src/ClonesWithImmutableArgs.sol b/src/ClonesWithImmutableArgs.sol index de2f2d9..00323fc 100644 --- a/src/ClonesWithImmutableArgs.sol +++ b/src/ClonesWithImmutableArgs.sol @@ -23,7 +23,14 @@ library ClonesWithImmutableArgs { internal returns (address payable instance) { - return clone(implementation, data, CloneType.CREATE); + bytes memory creationcode = getCreationBytecode(implementation, data); + // solhint-disable-next-line no-inline-assembly + assembly { + instance := create(0, add(creationcode, 0x20), mload(creationcode)) + } + if (instance == address(0)) { + revert CreateFail(); + } } /// @notice Creates a clone proxy of the implementation contract, with immutable args, @@ -36,7 +43,14 @@ library ClonesWithImmutableArgs { internal returns (address payable instance) { - return clone(implementation, data, CloneType.CREATE2); + bytes memory creationcode = getCreationBytecode(implementation, data); + // solhint-disable-next-line no-inline-assembly + assembly { + instance := create2(0, add(creationcode, 0x20), mload(creationcode), 0) + } + if (instance == address(0)) { + revert CreateFail(); + } } /// @notice Computes the address of a clone created using CREATE2 @@ -44,23 +58,27 @@ library ClonesWithImmutableArgs { /// @param implementation The implementation contract to clone /// @param data Encoded immutable args /// @return instance The address of the clone - function predictAddress(address implementation, bytes memory data) + function addressOfClone2(address implementation, bytes memory data) internal + view returns (address payable instance) { - return clone(implementation, data, CloneType.PREDICT_CREATE2); + bytes memory creationcode = getCreationBytecode(implementation, data); + bytes32 bytecodeHash = keccak256(creationcode); + instance = payable(address(uint160(uint(keccak256(abi.encodePacked( + bytes1(0xff), + address(this), + bytes32(0), + bytecodeHash + )))))); } - /// @notice Creates a clone proxy of the implementation contract, with immutable args + /// @notice Computes bytecode for a clone /// @dev data cannot exceed 65535 bytes, since 2 bytes are used to store the data length /// @param implementation The implementation contract to clone /// @param data Encoded immutable args - /// @param cloneType Whether to use CREATE or CREATE2 to create the clones - /// @return instance The address of the created clone - function clone(address implementation, bytes memory data, CloneType cloneType) - internal - returns (address payable instance) - { + /// @return ret Creation bytecode for the clone contract + function getCreationBytecode(address implementation, bytes memory data) internal pure returns (bytes memory ret) { // unrealistic for memory ptr or data length to exceed 256 bits unchecked { uint256 extraLength = data.length + 2; // +2 bytes for telling how much data there is appended to the call @@ -71,7 +89,10 @@ library ClonesWithImmutableArgs { // solhint-disable-next-line no-inline-assembly assembly { - ptr := mload(0x40) + ret := mload(0x40) + mstore(ret, creationSize) + mstore(0x40, add(ret, creationSize)) + ptr := add(ret, 0x20) // ------------------------------------------------------------------------------------------------------------- // CREATION (10 bytes) @@ -180,34 +201,6 @@ library ClonesWithImmutableArgs { assembly { mstore(copyPtr, shl(240, extraLength)) } - if(cloneType == CloneType.CREATE) { - // solhint-disable-next-line no-inline-assembly - assembly { - instance := create(0, ptr, creationSize) - } - } else if(cloneType == CloneType.CREATE2) { - // solhint-disable-next-line no-inline-assembly - assembly { - instance := create2(0, ptr, creationSize, 0) - } - } else if(cloneType == CloneType.PREDICT_CREATE2) { - bytes32 bytecodeHash; - // solhint-disable-next-line no-inline-assembly - assembly { - bytecodeHash := keccak256(ptr, creationSize) - } - instance = payable(address(uint160(uint(keccak256(abi.encodePacked( - bytes1(0xff), - address(this), - bytes32(0), - bytecodeHash - )))))); - } else { - revert CreateFail(); - } - if (instance == address(0)) { - revert CreateFail(); - } } } } diff --git a/src/ExampleCloneFactory.sol b/src/ExampleCloneFactory.sol index 461488d..3457495 100644 --- a/src/ExampleCloneFactory.sol +++ b/src/ExampleCloneFactory.sol @@ -33,13 +33,13 @@ contract ExampleCloneFactory { clone = ExampleClone(address(implementation).clone2(data)); } - function predictAddress( + function addressOfClone2( address param1, uint256 param2, uint64 param3, uint8 param4 - ) external returns (address clone) { + ) external view returns (address clone) { bytes memory data = abi.encodePacked(param1, param2, param3, param4); - clone = address(implementation).predictAddress(data); + clone = address(implementation).addressOfClone2(data); } } diff --git a/src/test/ExampleCloneFactory.t.sol b/src/test/ExampleCloneFactory.t.sol index a4b938b..85e5145 100644 --- a/src/test/ExampleCloneFactory.t.sol +++ b/src/test/ExampleCloneFactory.t.sol @@ -76,7 +76,7 @@ contract ExampleCloneFactoryTest is DSTest { uint64 param3, uint8 param4 ) public { - address predicted = factory.predictAddress( + address predicted = factory.addressOfClone2( param1, param2, param3, From 23768824cdc037f361f7065538b8f949cae9d3d1 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Wed, 10 Jan 2024 11:50:05 +0000 Subject: [PATCH 3/3] Fix merge --- src/ExampleCloneFactory.sol | 1 + src/test/ExampleCloneFactory.t.sol | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ExampleCloneFactory.sol b/src/ExampleCloneFactory.sol index d0d8a6d..eadd704 100644 --- a/src/ExampleCloneFactory.sol +++ b/src/ExampleCloneFactory.sol @@ -41,6 +41,7 @@ contract ExampleCloneFactory { ) external view returns (address clone) { bytes memory data = abi.encodePacked(param1, param2, param3, param4); clone = address(implementation).addressOfClone2(data); + } function createClone3( address param1, diff --git a/src/test/ExampleCloneFactory.t.sol b/src/test/ExampleCloneFactory.t.sol index 0303222..cc2cb8a 100644 --- a/src/test/ExampleCloneFactory.t.sol +++ b/src/test/ExampleCloneFactory.t.sol @@ -73,8 +73,14 @@ contract ExampleCloneFactoryTest is DSTest { param2, param3, param4 + ); + assertEq(clone.param1(), param1); + assertEq(clone.param2(), param2); + assertEq(clone.param3(), param3); + assertEq(clone.param4(), param4); + } -function testCorrectness_clone3( + function testCorrectness_clone3( address param1, uint256 param2, uint64 param3, @@ -92,9 +98,10 @@ function testCorrectness_clone3( assertEq(clone.param2(), param2); assertEq(clone.param3(), param3); assertEq(clone.param4(), param4); + assertEq(address(clone), factory.addressOfClone3(salt)); } - function testCorrectness_predictAddress( + function testCorrectness_addressOfClone2( address param1, uint256 param2, uint64 param3, @@ -113,7 +120,6 @@ function testCorrectness_clone3( param4 ); assertEq(predicted, address(clone)); - assertEq(address(clone), factory.addressOfClone3(salt)); } /// -----------------------------------------------------------------------