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

Send value when deploying clones #4939

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/chilled-walls-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`Clones`: Add version of `clone` and `cloneDeterministic` that support sending value at creation.
5 changes: 5 additions & 0 deletions .changeset/strong-singers-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`Errors`: New library of standard custom errors.
5 changes: 3 additions & 2 deletions contracts/metatx/ERC2771Forwarder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ pragma solidity ^0.8.20;
import {ERC2771Context} from "./ERC2771Context.sol";
import {ECDSA} from "../utils/cryptography/ECDSA.sol";
import {EIP712} from "../utils/cryptography/EIP712.sol";
import {Nonces} from "../utils/Nonces.sol";
import {Address} from "../utils/Address.sol";
import {Errors} from "../utils/Errors.sol";
import {Nonces} from "../utils/Nonces.sol";

/**
* @dev A forwarder compatible with ERC-2771 contracts. See {ERC2771Context}.
Expand Down Expand Up @@ -132,7 +133,7 @@ contract ERC2771Forwarder is EIP712, Nonces {
}

if (!_execute(request, true)) {
revert Address.FailedInnerCall();
revert Errors.FailedInnerCall();
}
}

Expand Down
41 changes: 34 additions & 7 deletions contracts/proxy/Clones.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

pragma solidity ^0.8.20;

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

/**
* @dev https://eips.ethereum.org/EIPS/eip-1167[ERC-1167] is a standard for
* deploying minimal proxy contracts, also known as "clones".
Expand All @@ -16,27 +18,34 @@ pragma solidity ^0.8.20;
*/
library Clones {
/**
* @dev A clone instance deployment failed.
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create opcode, which should never revert.
*/
error ERC1167FailedCreateClone();
function clone(address implementation) internal returns (address instance) {
return clone(implementation, 0);
}

/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create opcode, which should never revert.
*/
function clone(address implementation) internal returns (address instance) {
function clone(address implementation, uint256 value) internal returns (address instance) {
if (address(this).balance < value) {
revert Errors.InsufficientBalance(address(this).balance, value);
}
/// @solidity memory-safe-assembly
assembly {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create(0, 0x09, 0x37)
instance := create(value, 0x09, 0x37)
}
if (instance == address(0)) {
revert ERC1167FailedCreateClone();
revert Errors.FailedDeployment();
}
}

Expand All @@ -48,17 +57,35 @@ library Clones {
* the clones cannot be deployed twice at the same address.
*/
function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
return cloneDeterministic(implementation, salt, 0);
}

/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* 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 cloneDeterministic(
address implementation,
bytes32 salt,
uint256 value
) internal returns (address instance) {
if (address(this).balance < value) {
revert Errors.InsufficientBalance(address(this).balance, value);
}
/// @solidity memory-safe-assembly
assembly {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create2(0, 0x09, 0x37, salt)
instance := create2(value, 0x09, 0x37, salt)
}
if (instance == address(0)) {
revert ERC1167FailedCreateClone();
revert Errors.FailedDeployment();
}
}

Expand Down
30 changes: 11 additions & 19 deletions contracts/utils/Address.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,17 @@

pragma solidity ^0.8.20;

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

/**
* @dev Collection of functions related to the address type
*/
library Address {
/**
* @dev The ETH balance of the account is not enough to perform the operation.
*/
error AddressInsufficientBalance(address account);

/**
* @dev There's no code at `target` (it is not a contract).
*/
error AddressEmptyCode(address target);

/**
* @dev A call to an address target failed. The target may have reverted.
*/
error FailedInnerCall();

/**
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to
* `recipient`, forwarding all available gas and reverting on errors.
Expand All @@ -40,12 +32,12 @@ library Address {
*/
function sendValue(address payable recipient, uint256 amount) internal {
if (address(this).balance < amount) {
revert AddressInsufficientBalance(address(this));
revert Errors.InsufficientBalance(address(this).balance, amount);
}

(bool success, ) = recipient.call{value: amount}("");
if (!success) {
revert FailedInnerCall();
revert Errors.FailedInnerCall();
}
}

Expand All @@ -57,7 +49,7 @@ library Address {
* If `target` reverts with a revert reason or custom error, it is bubbled
* up by this function (like regular Solidity function calls). However, if
* the call reverted with no returned reason, this function reverts with a
* {FailedInnerCall} error.
* {Errors.FailedInnerCall} error.
*
* Returns the raw returned data. To convert to the expected return value,
* use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
Expand All @@ -82,7 +74,7 @@ library Address {
*/
function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
if (address(this).balance < value) {
revert AddressInsufficientBalance(address(this));
revert Errors.InsufficientBalance(address(this).balance, value);
}
(bool success, bytes memory returndata) = target.call{value: value}(data);
return verifyCallResultFromTarget(target, success, returndata);
Expand All @@ -108,8 +100,8 @@ library Address {

/**
* @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target
* was not a contract or bubbling up the revert reason (falling back to {FailedInnerCall}) in case of an
* unsuccessful call.
* was not a contract or bubbling up the revert reason (falling back to {Errors.FailedInnerCall}) in case
* of an unsuccessful call.
*/
function verifyCallResultFromTarget(
address target,
Expand All @@ -130,7 +122,7 @@ library Address {

/**
* @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the
* revert reason or with a default {FailedInnerCall} error.
* revert reason or with a default {Errors.FailedInnerCall} error.
*/
function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) {
if (!success) {
Expand All @@ -141,7 +133,7 @@ library Address {
}

/**
* @dev Reverts with returndata if present. Otherwise reverts with {FailedInnerCall}.
* @dev Reverts with returndata if present. Otherwise reverts with {Errors.FailedInnerCall}.
*/
function _revert(bytes memory returndata) private pure {
// Look for revert reason and bubble it up if present
Expand All @@ -153,7 +145,7 @@ library Address {
revert(add(32, returndata), returndata_size)
}
} else {
revert FailedInnerCall();
revert Errors.FailedInnerCall();
}
}
}
16 changes: 4 additions & 12 deletions contracts/utils/Create2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

pragma solidity ^0.8.20;

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

/**
* @dev Helper to make usage of the `CREATE2` EVM opcode easier and safer.
* `CREATE2` can be used to compute in advance the address where a smart
Expand All @@ -13,21 +15,11 @@ pragma solidity ^0.8.20;
* information.
*/
library Create2 {
/**
* @dev Not enough balance for performing a CREATE2 deploy.
*/
error Create2InsufficientBalance(uint256 balance, uint256 needed);

/**
* @dev There's no code to deploy.
*/
error Create2EmptyBytecode();

/**
* @dev The deployment failed.
*/
error Create2FailedDeployment();

/**
* @dev Deploys a contract using `CREATE2`. The address where the contract
* will be deployed can be known in advance via {computeAddress}.
Expand All @@ -44,7 +36,7 @@ library Create2 {
*/
function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) internal returns (address addr) {
if (address(this).balance < amount) {
revert Create2InsufficientBalance(address(this).balance, amount);
revert Errors.InsufficientBalance(address(this).balance, amount);
}
if (bytecode.length == 0) {
revert Create2EmptyBytecode();
Expand All @@ -54,7 +46,7 @@ library Create2 {
addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)
}
if (addr == address(0)) {
revert Create2FailedDeployment();
revert Errors.FailedDeployment();
}
}

Expand Down
23 changes: 23 additions & 0 deletions contracts/utils/Errors.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

/**
* @dev Collection of standard custom error that are used in multiple contracts
*/
library Errors {
/**
* @dev The ETH balance of the account is not enough to perform the operation.
*/
error InsufficientBalance(uint256 balance, uint256 needed);

/**
* @dev A call to an address target failed. The target may have reverted.
*/
error FailedInnerCall();

/**
* @dev The deployment failed.
*/
error FailedDeployment();
}
35 changes: 27 additions & 8 deletions test/proxy/Clones.behaviour.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,25 @@ module.exports = function shouldBehaveLikeClone() {
});
};

describe('construct with value', function () {
const value = 10n;

it('factory has enough balance', async function () {
await this.deployer.sendTransaction({ to: this.factory, value });

const instance = await this.createClone({ deployValue: value });
await expect(instance.deploymentTransaction()).to.changeEtherBalances([this.factory, instance], [-value, value]);

expect(await ethers.provider.getBalance(instance)).to.equal(value);
});

it('factory does not have enough balance', async function () {
await expect(this.createClone({ deployValue: value }))
.to.be.revertedWithCustomError(this.factory, 'InsufficientBalance')
.withArgs(0n, value);
});
});

describe('initialization without parameters', function () {
describe('non payable', function () {
const expectedInitializedValue = 10n;
Expand All @@ -23,7 +42,7 @@ module.exports = function shouldBehaveLikeClone() {

describe('when not sending balance', function () {
beforeEach('creating proxy', async function () {
this.proxy = await this.createClone(this.initializeData);
this.proxy = await this.createClone({ initData: this.initializeData });
});

assertProxyInitialization({
Expand All @@ -36,7 +55,7 @@ module.exports = function shouldBehaveLikeClone() {
const value = 10n ** 6n;

it('reverts', async function () {
await expect(this.createClone(this.initializeData, { value })).to.be.reverted;
await expect(this.createClone({ initData: this.initializeData, initValue: value })).to.be.reverted;
});
});
});
Expand All @@ -50,7 +69,7 @@ module.exports = function shouldBehaveLikeClone() {

describe('when not sending balance', function () {
beforeEach('creating proxy', async function () {
this.proxy = await this.createClone(this.initializeData);
this.proxy = await this.createClone({ initData: this.initializeData });
});

assertProxyInitialization({
Expand All @@ -63,7 +82,7 @@ module.exports = function shouldBehaveLikeClone() {
const value = 10n ** 6n;

beforeEach('creating proxy', async function () {
this.proxy = await this.createClone(this.initializeData, { value });
this.proxy = await this.createClone({ initData: this.initializeData, initValue: value });
});

assertProxyInitialization({
Expand All @@ -86,7 +105,7 @@ module.exports = function shouldBehaveLikeClone() {

describe('when not sending balance', function () {
beforeEach('creating proxy', async function () {
this.proxy = await this.createClone(this.initializeData);
this.proxy = await this.createClone({ initData: this.initializeData });
});

assertProxyInitialization({
Expand All @@ -99,7 +118,7 @@ module.exports = function shouldBehaveLikeClone() {
const value = 10n ** 6n;

it('reverts', async function () {
await expect(this.createClone(this.initializeData, { value })).to.be.reverted;
await expect(this.createClone({ initData: this.initializeData, initValue: value })).to.be.reverted;
});
});
});
Expand All @@ -115,7 +134,7 @@ module.exports = function shouldBehaveLikeClone() {

describe('when not sending balance', function () {
beforeEach('creating proxy', async function () {
this.proxy = await this.createClone(this.initializeData);
this.proxy = await this.createClone({ initData: this.initializeData });
});

assertProxyInitialization({
Expand All @@ -128,7 +147,7 @@ module.exports = function shouldBehaveLikeClone() {
const value = 10n ** 6n;

beforeEach('creating proxy', async function () {
this.proxy = await this.createClone(this.initializeData, { value });
this.proxy = await this.createClone({ initData: this.initializeData, initValue: value });
});

assertProxyInitialization({
Expand Down
Loading
Loading