Skip to content
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

Add clone variant with per-instance immutable arguments #5109

Merged
merged 18 commits into from
Sep 4, 2024
141 changes: 141 additions & 0 deletions contracts/proxy/Clones.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

pragma solidity ^0.8.20;

import {Create2} from "../utils/Create2.sol";
import {Errors} from "../utils/Errors.sol";

/**
Expand All @@ -17,6 +18,8 @@
* deterministic method.
*/
library Clones {
error ImmutableArgsTooLarge();

/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
Expand Down Expand Up @@ -121,4 +124,142 @@
) internal view returns (address predicted) {
return predictDeterministicAddress(implementation, salt, address(this));
}

/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`, with `args`
* attached to it as immutable arguments (that can be fetched using {fetchCloneArgs}).
*
* This function uses the create opcode, which should never revert.
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
*/
function cloneWithImmutableArgs(address implementation, bytes memory args) internal returns (address instance) {
return cloneWithImmutableArgs(implementation, args, 0);
}

/**
* @dev Same as {xref-Clones-cloneWithImmutableArgs-address-bytes-}[cloneWithImmutableArgs], but with a `value`
* parameter to send native currency to the new contract.
*
* NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
* to always have enough balance for new deployments. Consider exposing this function under a payable method.
*/
function cloneWithImmutableArgs(
address implementation,
bytes memory args,
uint256 value
) internal returns (address instance) {
if (address(this).balance < value) {
revert Errors.InsufficientBalance(address(this).balance, value);
}
bytes memory bytecode = _cloneWithImmutableArgsCode(implementation, args);
assembly ("memory-safe") {
instance := create(value, add(bytecode, 0x20), mload(bytecode))
}
if (instance == address(0)) {
revert Errors.FailedDeployment();
}
}

/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`, with `args`
* attached to it as immutable arguments (that can be fetched using {fetchCloneArgs}).
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
*
* This function uses the create2 opcode and a `salt` to deterministically deploy the clone. Using the same
* `implementation` and `salt` multiple time will revert, since the clones cannot be deployed twice at the same
* address.
*/
function cloneWithImmutableArgsDeterministic(
address implementation,
bytes memory args,
bytes32 salt
) internal returns (address instance) {
return cloneWithImmutableArgsDeterministic(implementation, args, salt, 0);
}

/**
* @dev Same as {xref-Clones-cloneWithImmutableArgsDeterministic-address-bytes-bytes32-}[cloneWithImmutableArgsDeterministic],
* but with a `value` parameter to send native currency to the new contract.
*
* NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
* to always have enough balance for new deployments. Consider exposing this function under a payable method.
*/
function cloneWithImmutableArgsDeterministic(
address implementation,
bytes memory args,
bytes32 salt,
uint256 value
) internal returns (address instance) {
bytes memory bytecode = _cloneWithImmutableArgsCode(implementation, args);
return Create2.deploy(value, salt, bytecode);
}

/**
* @dev Computes the address of a clone deployed using {Clones-cloneWithImmutableArgsDeterministic}.
*/
function predictWithImmutableArgsDeterministicAddress(
address implementation,
bytes memory args,
bytes32 salt,
address deployer
) internal pure returns (address predicted) {
bytes memory bytecode = _cloneWithImmutableArgsCode(implementation, args);
return Create2.computeAddress(salt, keccak256(bytecode), deployer);
}

/**
* @dev Computes the address of a clone deployed using {Clones-cloneWithImmutableArgsDeterministic}.
*/
function predictWithImmutableArgsDeterministicAddress(
address implementation,
bytes memory args,
bytes32 salt
) internal view returns (address predicted) {
return predictWithImmutableArgsDeterministicAddress(implementation, args, salt, address(this));
}

/**
* @dev Get the immutable args attached to a clone.
*
* - If `instance` is a clone that was deployed using `clone` or `cloneDeterministic`, this
* function will return an empty array.
* - If `instance` is a clone that was deployed using `cloneWithImmutableArgs` or
* `cloneWithImmutableArgsDeterministic`, this function will return the args array used at
* creation.
* - If `instance` is NOT a clone deployed using this library, the behavior is undefined. This
* function should only be used to check addresses that are known to be clones.
*/
function fetchCloneArgs(address instance) internal view returns (bytes memory result) {
uint256 argsLength = instance.code.length - 0x2d; // revert if length is too short
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
assembly {
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
// reserve space
result := mload(0x40)
mstore(0x40, add(result, add(0x20, argsLength)))
// load
mstore(result, argsLength)
extcodecopy(instance, add(result, 0x20), 0x2d, argsLength)
}
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @dev Helper that prepares the initcode of the proxy with immutable args.
*
* An assembly variant of this function requires copying the `args` array, which can be efficiently done using
* `mcopy`. Unfortunatelly, that opcode is not available before cancun. A pure solidity implemenation using

Check failure on line 246 in contracts/proxy/Clones.sol

View workflow job for this annotation

GitHub Actions / codespell

Unfortunatelly ==> Unfortunately

Check failure on line 246 in contracts/proxy/Clones.sol

View workflow job for this annotation

GitHub Actions / codespell

implemenation ==> implementation
Amxx marked this conversation as resolved.
Show resolved Hide resolved
* abi.encodePacked is more expensive but also more portable and easier to review.
*/
function _cloneWithImmutableArgsCode(
address implementation,
bytes memory args
) private pure returns (bytes memory) {
uint256 initCodeLength = args.length + 0x2d;
if (initCodeLength > type(uint16).max) revert ImmutableArgsTooLarge();
Amxx marked this conversation as resolved.
Show resolved Hide resolved
return
abi.encodePacked(
hex"61",
uint16(initCodeLength),
hex"3d81600a3d39f3363d3d373d3d3d363d73",
implementation,
hex"5af43d82803e903d91602b57fd5bf3",
args
);
}
}
36 changes: 34 additions & 2 deletions test/proxy/Clones.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,28 @@ contract ClonesTest is Test {
assertEq(spillage, bytes32(0));
}

function testSymbolicPredictWithImmutableArgsDeterministicAddressSpillage(
address implementation,
bytes32 salt,
bytes memory args
) public {
vm.assume(args.length < 0xffd3);

address predicted = Clones.predictWithImmutableArgsDeterministicAddress(implementation, args, salt);
bytes32 spillage;
/// @solidity memory-safe-assembly
assembly {
spillage := and(predicted, 0xffffffffffffffffffffffff0000000000000000000000000000000000000000)
}
assertEq(spillage, bytes32(0));
}

function testCloneDirty() external {
address cloneClean = Clones.clone(address(this));
address cloneDirty = Clones.clone(_dirty(address(this)));

// both clones have the same code
assertEq(keccak256(cloneClean.code), keccak256(cloneDirty.code));
assertEq(cloneClean.code, cloneDirty.code);

// both clones behave as expected
assertEq(ClonesTest(cloneClean).getNumber(), this.getNumber());
Expand All @@ -37,7 +53,7 @@ contract ClonesTest is Test {
address cloneDirty = Clones.cloneDeterministic(_dirty(address(this)), ~salt);

// both clones have the same code
assertEq(keccak256(cloneClean.code), keccak256(cloneDirty.code));
assertEq(cloneClean.code, cloneDirty.code);

// both clones behave as expected
assertEq(ClonesTest(cloneClean).getNumber(), this.getNumber());
Expand All @@ -52,6 +68,22 @@ contract ClonesTest is Test {
assertEq(predictClean, predictDirty);
}

function testFetchCloneArgs(bytes memory args, bytes32 salt) external {
vm.assume(args.length < 0xffd3);

address instance1 = Clones.cloneWithImmutableArgs(address(this), args);
address instance2 = Clones.cloneWithImmutableArgsDeterministic(address(this), args, salt);

// both clones have the same code
assertEq(instance1.code, instance2.code);

// both clones behave as expected and args can be fetched
assertEq(ClonesTest(instance1).getNumber(), this.getNumber());
assertEq(ClonesTest(instance2).getNumber(), this.getNumber());
assertEq(Clones.fetchCloneArgs(instance1), args);
assertEq(Clones.fetchCloneArgs(instance2), args);
}

function _dirty(address input) private pure returns (address output) {
assembly ("memory-safe") {
output := or(input, shl(160, not(0)))
Expand Down
Loading
Loading