Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 ImmutableArgsTooLarge();

/**
* @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 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.
*/
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}).
*
* 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
assembly {
// reserve space
result := mload(0x40)
mstore(0x40, add(result, add(0x20, argsLength)))
// load
mstore(result, argsLength)
extcodecopy(instance, add(result, 0x20), 0x2d, argsLength)
}
}

/**
* @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
* 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();
return
abi.encodePacked(
hex"3d61",
uint16(initCodeLength),
hex"80600b3d3981f3363d3d373d3d3d363d73",
implementation,
hex"5af43d82803e903d91602b57fd5bf3",
args
);
}
}
202 changes: 130 additions & 72 deletions test/proxy/Clones.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,47 @@ async function fixture() {
const factory = await ethers.deployContract('$Clones');
const implementation = await ethers.deployContract('DummyImplementation');

const newClone = async (opts = {}) => {
const clone = await factory.$clone.staticCall(implementation).then(address => implementation.attach(address));
const tx = await (opts.deployValue
? factory.$clone(implementation, ethers.Typed.uint256(opts.deployValue))
: factory.$clone(implementation));
if (opts.initData || opts.initValue) {
await deployer.sendTransaction({ to: clone, value: opts.initValue ?? 0n, data: opts.initData ?? '0x' });
}
return Object.assign(clone, { deploymentTransaction: () => tx });
};

const newCloneDeterministic = async (opts = {}) => {
const salt = opts.salt ?? ethers.randomBytes(32);
const clone = await factory.$cloneDeterministic
.staticCall(implementation, salt)
.then(address => implementation.attach(address));
const tx = await (opts.deployValue
? factory.$cloneDeterministic(implementation, salt, ethers.Typed.uint256(opts.deployValue))
: factory.$cloneDeterministic(implementation, salt));
if (opts.initData || opts.initValue) {
await deployer.sendTransaction({ to: clone, value: opts.initValue ?? 0n, data: opts.initData ?? '0x' });
}
return Object.assign(clone, { deploymentTransaction: () => tx });
};
const newClone =
args =>
async (opts = {}) => {
const clone = await factory.$clone.staticCall(implementation).then(address => implementation.attach(address));
const tx = await (opts.deployValue
? args
? factory.$cloneWithImmutableArgs(implementation, args, ethers.Typed.uint256(opts.deployValue))
: factory.$clone(implementation, ethers.Typed.uint256(opts.deployValue))
: args
? factory.$cloneWithImmutableArgs(implementation, args)
: factory.$clone(implementation));
if (opts.initData || opts.initValue) {
await deployer.sendTransaction({ to: clone, value: opts.initValue ?? 0n, data: opts.initData ?? '0x' });
}
return Object.assign(clone, { deploymentTransaction: () => tx });
};

const newCloneDeterministic =
args =>
async (opts = {}) => {
const salt = opts.salt ?? ethers.randomBytes(32);
const clone = await factory.$cloneDeterministic
.staticCall(implementation, salt)
.then(address => implementation.attach(address));
const tx = await (opts.deployValue
? args
? factory.$cloneWithImmutableArgsDeterministic(
implementation,
args,
salt,
ethers.Typed.uint256(opts.deployValue),
)
: factory.$cloneDeterministic(implementation, salt, ethers.Typed.uint256(opts.deployValue))
: args
? factory.$cloneWithImmutableArgsDeterministic(implementation, args, salt)
: factory.$cloneDeterministic(implementation, salt));
if (opts.initData || opts.initValue) {
await deployer.sendTransaction({ to: clone, value: opts.initValue ?? 0n, data: opts.initData ?? '0x' });
}
return Object.assign(clone, { deploymentTransaction: () => tx });
};

return { deployer, factory, implementation, newClone, newCloneDeterministic };
}
Expand All @@ -43,53 +60,94 @@ describe('Clones', function () {
Object.assign(this, await loadFixture(fixture));
});

describe('clone', function () {
beforeEach(async function () {
this.createClone = this.newClone;
for (const args of [undefined, '0x', '0x11223344']) {
describe(args ? `with immutable args: ${args}` : 'without immutable args', function () {
describe('clone', function () {
beforeEach(async function () {
this.createClone = this.newClone(args);
});

shouldBehaveLikeClone();

it('get immutable arguments', async function () {
const instance = await this.createClone();
expect(await this.factory.$fetchCloneArgs(instance)).to.equal(args ?? '0x');
});
});

describe('cloneDeterministic', function () {
beforeEach(async function () {
this.createClone = this.newCloneDeterministic(undefined);
});

shouldBehaveLikeClone();

it('revert if address already used', async function () {
const salt = ethers.randomBytes(32);

const deployClone = () =>
args
? this.factory.$cloneWithImmutableArgsDeterministic(this.implementation, args, salt)
: this.factory.$cloneDeterministic(this.implementation, salt);

// deploy once
await expect(deployClone()).to.not.be.reverted;

// deploy twice
await expect(deployClone()).to.be.revertedWithCustomError(this.factory, 'FailedDeployment');
});

it('address prediction', async function () {
const salt = ethers.randomBytes(32);

if (args) {
const expected = ethers.getCreate2Address(
this.factory.target,
salt,
ethers.keccak256(
ethers.concat([
'0x3d61',
ethers.toBeHex(0x2d + ethers.getBytes(args).length, 2),
'0x80600b3d3981f3363d3d373d3d3d363d73',
this.implementation.target,
'0x5af43d82803e903d91602b57fd5bf3',
args,
]),
),
);

const predicted = await this.factory.$predictWithImmutableArgsDeterministicAddress(
this.implementation,
args,
salt,
);
expect(predicted).to.equal(expected);

await expect(this.factory.$cloneWithImmutableArgsDeterministic(this.implementation, args, salt))
.to.emit(this.factory, 'return$cloneWithImmutableArgsDeterministic_address_bytes_bytes32')
.withArgs(predicted);
} else {
const expected = ethers.getCreate2Address(
this.factory.target,
salt,
ethers.keccak256(
ethers.concat([
'0x3d602d80600a3d3981f3363d3d373d3d3d363d73',
this.implementation.target,
'0x5af43d82803e903d91602b57fd5bf3',
]),
),
);

const predicted = await this.factory.$predictDeterministicAddress(this.implementation, salt);
expect(predicted).to.equal(expected);

await expect(this.factory.$cloneDeterministic(this.implementation, salt))
.to.emit(this.factory, 'return$cloneDeterministic_address_bytes32')
.withArgs(predicted);
}
});
});
});

shouldBehaveLikeClone();
});

describe('cloneDeterministic', function () {
beforeEach(async function () {
this.createClone = this.newCloneDeterministic;
});

shouldBehaveLikeClone();

it('revert if address already used', async function () {
const salt = ethers.randomBytes(32);

// deploy once
await expect(this.factory.$cloneDeterministic(this.implementation, salt)).to.emit(
this.factory,
'return$cloneDeterministic_address_bytes32',
);

// deploy twice
await expect(this.factory.$cloneDeterministic(this.implementation, salt)).to.be.revertedWithCustomError(
this.factory,
'FailedDeployment',
);
});

it('address prediction', async function () {
const salt = ethers.randomBytes(32);

const creationCode = ethers.concat([
'0x3d602d80600a3d3981f3363d3d373d3d3d363d73',
this.implementation.target,
'0x5af43d82803e903d91602b57fd5bf3',
]);

const predicted = await this.factory.$predictDeterministicAddress(this.implementation, salt);
const expected = ethers.getCreate2Address(this.factory.target, salt, ethers.keccak256(creationCode));
expect(predicted).to.equal(expected);

await expect(this.factory.$cloneDeterministic(this.implementation, salt))
.to.emit(this.factory, 'return$cloneDeterministic_address_bytes32')
.withArgs(predicted);
});
});
}
});