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
5 changes: 5 additions & 0 deletions .changeset/four-chairs-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openzeppelin-solidity": minor
---

`Clones`: Add `cloneWithImmutableArgs` and `cloneDeterministicWithImmutableArgs` variants that create clones with per-instance immutable arguments. The immutable arguments can be retrieved using `fetchCloneArgs`. The corresponding `predictDeterministicWithImmutableArgs` function is also included.
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 @@ import {Errors} from "../utils/Errors.sol";
* deterministic method.
*/
library Clones {
error CloneArgumentsTooLong();

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

/**
* @dev Deploys and returns the address of a clone that mimics the behavior of `implementation` with custom
* immutable arguments. These are provided through `args` and cannot be changed after deployment. To
* access the arguments within the implementation, use {fetchCloneArgs}.
*
* This function uses the create opcode, which should never revert.
*/
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 = _cloneCodeWithImmutableArgs(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 custom
* immutable arguments. These are provided through `args` and cannot be changed after deployment. To
* access the arguments within the implementation, use {fetchCloneArgs}.
*
* 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 cloneDeterministicWithImmutableArgs(
address implementation,
bytes memory args,
bytes32 salt
) internal returns (address instance) {
return cloneDeterministicWithImmutableArgs(implementation, args, salt, 0);
}

/**
* @dev Same as {xref-Clones-cloneDeterministicWithImmutableArgs-address-bytes-bytes32-}[cloneDeterministicWithImmutableArgs],
* 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 cloneDeterministicWithImmutableArgs(
address implementation,
bytes memory args,
bytes32 salt,
uint256 value
) internal returns (address instance) {
bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
return Create2.deploy(value, salt, bytecode);
}

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

/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministicWithImmutableArgs}.
*/
function predictDeterministicAddressWithImmutableArgs(
address implementation,
bytes memory args,
bytes32 salt
) internal view returns (address predicted) {
return predictDeterministicAddressWithImmutableArgs(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
* `cloneDeterministicWithImmutableArgs`, 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) {
bytes memory result = new bytes(instance.code.length - 0x2d); // revert if length is too short
assembly ("memory-safe") {
extcodecopy(instance, add(result, 0x20), 0x2d, mload(result))
}
return result;
}

/**
* @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`. Unfortunately, that opcode is not available before cancun. A pure solidity implementation using
* abi.encodePacked is more expensive but also more portable and easier to review.
*
* NOTE: https://eips.ethereum.org/EIPS/eip-170[EIP-170] limits the length of the contract code to 24576 bytes.
* With the proxy code taking 45 bytes, that limits the length of the immutable args to 24531 bytes.
*/
function _cloneCodeWithImmutableArgs(
address implementation,
bytes memory args
) private pure returns (bytes memory) {
if (args.length > 0x5fd3) revert CloneArgumentsTooLong();
return
abi.encodePacked(
hex"61",
uint16(args.length + 0x2d),
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 testSymbolicPredictDeterministicAddressWithImmutableArgsSpillage(
address implementation,
bytes32 salt,
bytes memory args
) public {
vm.assume(args.length < 0xbfd3);

address predicted = Clones.predictDeterministicAddressWithImmutableArgs(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 < 0xbfd3);

address instance1 = Clones.cloneWithImmutableArgs(address(this), args);
address instance2 = Clones.cloneDeterministicWithImmutableArgs(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