Add ability to create clones with initial `value` in Clones.sol (#4936)

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Co-authored-by: ernestognw <ernestognw@gmail.com>
pull/4944/head
Anton Bukov 11 months ago committed by GitHub
parent 8b2f29ceb0
commit e83142944f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/chilled-walls-develop.md
  2. 5
      .changeset/strong-singers-talk.md
  3. 9
      CHANGELOG.md
  4. 2
      README.md
  5. 3
      contracts/metatx/ERC2771Forwarder.sol
  6. 47
      contracts/proxy/Clones.sol
  7. 30
      contracts/utils/Address.sol
  8. 16
      contracts/utils/Create2.sol
  9. 26
      contracts/utils/Errors.sol
  10. 16
      scripts/upgradeable/upgradeable.patch
  11. 2
      test/finance/VestingWallet.behavior.js
  12. 2
      test/governance/Governor.test.js
  13. 10
      test/governance/TimelockController.test.js
  14. 2
      test/metatx/ERC2771Forwarder.test.js
  15. 35
      test/proxy/Clones.behaviour.js
  16. 30
      test/proxy/Clones.test.js
  17. 38
      test/utils/Address.test.js
  18. 4
      test/utils/Create2.test.js
  19. 2
      test/utils/cryptography/EIP712.test.js

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`Clones`: Add version of `clone` and `cloneDeterministic` that support sending value at creation.

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`Errors`: New library of common custom errors.

@ -1,5 +1,14 @@
# Changelog # Changelog
### Custom error changes
This version comes with changes to the custom error identifiers. Contracts previously depending on the following errors should be replaced accordingly:
- Replace `Address.FailedInnerCall` with `Errors.FailedCall`
- Replace `Address.AddressInsufficientBalance` with `Errors.InsufficientBalance`
- Replace `Clones.Create2InsufficientBalance` with `Errors.InsufficientBalance`
- Replace `Clones.ERC1167FailedCreateClone` with `Errors.FailedDeployment`
- Replace `Clones.Create2FailedDeployment` with `Errors.FailedDeployment`
## 5.0.2 (2024-02-29) ## 5.0.2 (2024-02-29)

@ -41,7 +41,7 @@ $ npm install @openzeppelin/contracts
$ forge install OpenZeppelin/openzeppelin-contracts $ forge install OpenZeppelin/openzeppelin-contracts
``` ```
Add `@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/` in `remappings.txt.` Add `@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/` in `remappings.txt.`
### Usage ### Usage

@ -8,6 +8,7 @@ import {ECDSA} from "../utils/cryptography/ECDSA.sol";
import {EIP712} from "../utils/cryptography/EIP712.sol"; import {EIP712} from "../utils/cryptography/EIP712.sol";
import {Nonces} from "../utils/Nonces.sol"; import {Nonces} from "../utils/Nonces.sol";
import {Address} from "../utils/Address.sol"; import {Address} from "../utils/Address.sol";
import {Errors} from "../utils/Errors.sol";
/** /**
* @dev A forwarder compatible with ERC-2771 contracts. See {ERC2771Context}. * @dev A forwarder compatible with ERC-2771 contracts. See {ERC2771Context}.
@ -132,7 +133,7 @@ contract ERC2771Forwarder is EIP712, Nonces {
} }
if (!_execute(request, true)) { if (!_execute(request, true)) {
revert Address.FailedInnerCall(); revert Errors.FailedCall();
} }
} }

@ -3,6 +3,8 @@
pragma solidity ^0.8.20; 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 * @dev https://eips.ethereum.org/EIPS/eip-1167[ERC-1167] is a standard for
* deploying minimal proxy contracts, also known as "clones". * deploying minimal proxy contracts, also known as "clones".
@ -15,17 +17,26 @@ pragma solidity ^0.8.20;
* deterministic method. * deterministic method.
*/ */
library Clones { library Clones {
/**
* @dev A clone instance deployment failed.
*/
error ERC1167FailedCreateClone();
/** /**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`. * @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. * This function uses the create opcode, which should never revert.
*/ */
function clone(address implementation) internal returns (address instance) { function clone(address implementation) internal returns (address instance) {
return clone(implementation, 0);
}
/**
* @dev Same as {xref-Clones-clone-address-}[clone], 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 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 /// @solidity memory-safe-assembly
assembly { assembly {
// Stores the bytecode after address // Stores the bytecode after address
@ -34,10 +45,10 @@ library Clones {
mstore(0x11, implementation) mstore(0x11, implementation)
// Packs the first 3 bytes of the `implementation` address with the bytecode before the address. // Packs the first 3 bytes of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0x88, implementation), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000)) mstore(0x00, or(shr(0x88, implementation), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
instance := create(0, 0x09, 0x37) instance := create(value, 0x09, 0x37)
} }
if (instance == address(0)) { if (instance == address(0)) {
revert ERC1167FailedCreateClone(); revert Errors.FailedDeployment();
} }
} }
@ -49,6 +60,24 @@ library Clones {
* the clones cannot be deployed twice at the same address. * the clones cannot be deployed twice at the same address.
*/ */
function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) { function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
return cloneDeterministic(implementation, salt, 0);
}
/**
* @dev Same as {xref-Clones-cloneDeterministic-address-bytes32-}[cloneDeterministic], 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 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 /// @solidity memory-safe-assembly
assembly { assembly {
// Stores the bytecode after address // Stores the bytecode after address
@ -57,10 +86,10 @@ library Clones {
mstore(0x11, implementation) mstore(0x11, implementation)
// Packs the first 3 bytes of the `implementation` address with the bytecode before the address. // Packs the first 3 bytes of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0x88, implementation), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000)) mstore(0x00, or(shr(0x88, implementation), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
instance := create2(0, 0x09, 0x37, salt) instance := create2(value, 0x09, 0x37, salt)
} }
if (instance == address(0)) { if (instance == address(0)) {
revert ERC1167FailedCreateClone(); revert Errors.FailedDeployment();
} }
} }

@ -3,25 +3,17 @@
pragma solidity ^0.8.20; pragma solidity ^0.8.20;
import {Errors} from "./Errors.sol";
/** /**
* @dev Collection of functions related to the address type * @dev Collection of functions related to the address type
*/ */
library Address { 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). * @dev There's no code at `target` (it is not a contract).
*/ */
error AddressEmptyCode(address target); 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 * @dev Replacement for Solidity's `transfer`: sends `amount` wei to
* `recipient`, forwarding all available gas and reverting on errors. * `recipient`, forwarding all available gas and reverting on errors.
@ -40,12 +32,12 @@ library Address {
*/ */
function sendValue(address payable recipient, uint256 amount) internal { function sendValue(address payable recipient, uint256 amount) internal {
if (address(this).balance < amount) { if (address(this).balance < amount) {
revert AddressInsufficientBalance(address(this)); revert Errors.InsufficientBalance(address(this).balance, amount);
} }
(bool success, ) = recipient.call{value: amount}(""); (bool success, ) = recipient.call{value: amount}("");
if (!success) { if (!success) {
revert FailedInnerCall(); revert Errors.FailedCall();
} }
} }
@ -57,7 +49,7 @@ library Address {
* If `target` reverts with a revert reason or custom error, it is bubbled * If `target` reverts with a revert reason or custom error, it is bubbled
* up by this function (like regular Solidity function calls). However, if * up by this function (like regular Solidity function calls). However, if
* the call reverted with no returned reason, this function reverts with a * the call reverted with no returned reason, this function reverts with a
* {FailedInnerCall} error. * {Errors.FailedCall} error.
* *
* Returns the raw returned data. To convert to the expected return value, * 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`]. * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
@ -82,7 +74,7 @@ library Address {
*/ */
function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
if (address(this).balance < value) { 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); (bool success, bytes memory returndata) = target.call{value: value}(data);
return verifyCallResultFromTarget(target, success, returndata); return verifyCallResultFromTarget(target, success, returndata);
@ -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 * @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 * was not a contract or bubbling up the revert reason (falling back to {Errors.FailedCall}) in case
* unsuccessful call. * of an unsuccessful call.
*/ */
function verifyCallResultFromTarget( function verifyCallResultFromTarget(
address target, address target,
@ -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 * @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.FailedCall} error.
*/ */
function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) { function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) {
if (!success) { if (!success) {
@ -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.FailedCall}.
*/ */
function _revert(bytes memory returndata) private pure { function _revert(bytes memory returndata) private pure {
// Look for revert reason and bubble it up if present // Look for revert reason and bubble it up if present
@ -153,7 +145,7 @@ library Address {
revert(add(32, returndata), returndata_size) revert(add(32, returndata), returndata_size)
} }
} else { } else {
revert FailedInnerCall(); revert Errors.FailedCall();
} }
} }
} }

@ -3,6 +3,8 @@
pragma solidity ^0.8.20; pragma solidity ^0.8.20;
import {Errors} from "./Errors.sol";
/** /**
* @dev Helper to make usage of the `CREATE2` EVM opcode easier and safer. * @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 * `CREATE2` can be used to compute in advance the address where a smart
@ -13,21 +15,11 @@ pragma solidity ^0.8.20;
* information. * information.
*/ */
library Create2 { library Create2 {
/**
* @dev Not enough balance for performing a CREATE2 deploy.
*/
error Create2InsufficientBalance(uint256 balance, uint256 needed);
/** /**
* @dev There's no code to deploy. * @dev There's no code to deploy.
*/ */
error Create2EmptyBytecode(); error Create2EmptyBytecode();
/**
* @dev The deployment failed.
*/
error Create2FailedDeployment();
/** /**
* @dev Deploys a contract using `CREATE2`. The address where the contract * @dev Deploys a contract using `CREATE2`. The address where the contract
* will be deployed can be known in advance via {computeAddress}. * will be deployed can be known in advance via {computeAddress}.
@ -44,7 +36,7 @@ library Create2 {
*/ */
function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) internal returns (address addr) { function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) internal returns (address addr) {
if (address(this).balance < amount) { if (address(this).balance < amount) {
revert Create2InsufficientBalance(address(this).balance, amount); revert Errors.InsufficientBalance(address(this).balance, amount);
} }
if (bytecode.length == 0) { if (bytecode.length == 0) {
revert Create2EmptyBytecode(); revert Create2EmptyBytecode();
@ -54,7 +46,7 @@ library Create2 {
addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt) addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)
} }
if (addr == address(0)) { if (addr == address(0)) {
revert Create2FailedDeployment(); revert Errors.FailedDeployment();
} }
} }

@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @dev Collection of common custom errors used in multiple contracts
*
* IMPORTANT: Backwards compatibility is not guaranteed in future versions of the library.
* It is recommended to avoid relying on the error API for critical functionality.
*/
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 FailedCall();
/**
* @dev The deployment failed.
*/
error FailedDeployment();
}

@ -59,7 +59,7 @@ index ff596b0c3..000000000
-<!-- Make sure that you have reviewed the OpenZeppelin Contracts Contributor Guidelines. --> -<!-- Make sure that you have reviewed the OpenZeppelin Contracts Contributor Guidelines. -->
-<!-- https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/CONTRIBUTING.md --> -<!-- https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/CONTRIBUTING.md -->
diff --git a/README.md b/README.md diff --git a/README.md b/README.md
index 35083bc6e..05cf4fc27 100644 index fa7b4e31e..4799b6376 100644
--- a/README.md --- a/README.md
+++ b/README.md +++ b/README.md
@@ -19,6 +19,9 @@ @@ -19,6 +19,9 @@
@ -89,8 +89,8 @@ index 35083bc6e..05cf4fc27 100644
+$ forge install OpenZeppelin/openzeppelin-contracts-upgradeable +$ forge install OpenZeppelin/openzeppelin-contracts-upgradeable
``` ```
-Add `@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/` in `remappings.txt.` -Add `@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/` in `remappings.txt.`
+Add `@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/` in `remappings.txt.` +Add `@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/` in `remappings.txt.`
### Usage ### Usage
@ -110,7 +110,7 @@ index 35083bc6e..05cf4fc27 100644
} }
``` ```
diff --git a/contracts/package.json b/contracts/package.json diff --git a/contracts/package.json b/contracts/package.json
index 6ab89138a..ece834a44 100644 index 845e8c403..8dc181b91 100644
--- a/contracts/package.json --- a/contracts/package.json
+++ b/contracts/package.json +++ b/contracts/package.json
@@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
@ -118,7 +118,7 @@ index 6ab89138a..ece834a44 100644
- "name": "@openzeppelin/contracts", - "name": "@openzeppelin/contracts",
+ "name": "@openzeppelin/contracts-upgradeable", + "name": "@openzeppelin/contracts-upgradeable",
"description": "Secure Smart Contract library for Solidity", "description": "Secure Smart Contract library for Solidity",
"version": "5.0.1", "version": "5.0.2",
"files": [ "files": [
@@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
}, },
@ -307,7 +307,7 @@ index 77c4c8990..602467f40 100644
} }
} }
diff --git a/package.json b/package.json diff --git a/package.json b/package.json
index ec2c44ced..46eedc98f 100644 index c4b358e10..96ab2559c 100644
--- a/package.json --- a/package.json
+++ b/package.json +++ b/package.json
@@ -32,7 +32,7 @@ @@ -32,7 +32,7 @@
@ -328,7 +328,7 @@ index 304d1386a..a1cd63bee 100644
+@openzeppelin/contracts-upgradeable/=contracts/ +@openzeppelin/contracts-upgradeable/=contracts/
+@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js
index 166038b36..268e0d29d 100644 index 2b6e7fa97..268e0d29d 100644
--- a/test/utils/cryptography/EIP712.test.js --- a/test/utils/cryptography/EIP712.test.js
+++ b/test/utils/cryptography/EIP712.test.js +++ b/test/utils/cryptography/EIP712.test.js
@@ -47,27 +47,6 @@ describe('EIP712', function () { @@ -47,27 +47,6 @@ describe('EIP712', function () {
@ -346,7 +346,7 @@ index 166038b36..268e0d29d 100644
- const clone = await factory - const clone = await factory
- .$clone(this.eip712) - .$clone(this.eip712)
- .then(tx => tx.wait()) - .then(tx => tx.wait())
- .then(receipt => receipt.logs.find(ev => ev.fragment.name == 'return$clone').args.instance) - .then(receipt => receipt.logs.find(ev => ev.fragment.name == 'return$clone_address').args.instance)
- .then(address => ethers.getContractAt('$EIP712Verifier', address)); - .then(address => ethers.getContractAt('$EIP712Verifier', address));
- -
- const expectedDomain = { ...this.domain, verifyingContract: clone.target }; - const expectedDomain = { ...this.domain, verifyingContract: clone.target };

@ -12,7 +12,7 @@ async function envSetup(mock, beneficiary, token) {
const beneficiaryMock = await ethers.deployContract('EtherReceiverMock'); const beneficiaryMock = await ethers.deployContract('EtherReceiverMock');
await beneficiaryMock.setAcceptEther(false); await beneficiaryMock.setAcceptEther(false);
await mock.connect(beneficiary).transferOwnership(beneficiaryMock); await mock.connect(beneficiary).transferOwnership(beneficiaryMock);
return { args: [], error: [mock, 'FailedInnerCall'] }; return { args: [], error: [mock, 'FailedCall'] };
}, },
releasedEvent: 'EtherReleased', releasedEvent: 'EtherReleased',
args: [], args: [],

@ -445,7 +445,7 @@ describe('Governor', function () {
await this.helper.waitForSnapshot(); await this.helper.waitForSnapshot();
await this.helper.connect(this.voter1).vote({ support: VoteType.For }); await this.helper.connect(this.voter1).vote({ support: VoteType.For });
await this.helper.waitForDeadline(); await this.helper.waitForDeadline();
await expect(this.helper.execute()).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); await expect(this.helper.execute()).to.be.revertedWithCustomError(this.mock, 'FailedCall');
}); });
it('if receiver revert with reason', async function () { it('if receiver revert with reason', async function () {

@ -893,7 +893,7 @@ describe('TimelockController', function () {
operation.predecessor, operation.predecessor,
operation.salt, operation.salt,
), ),
).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); ).to.be.revertedWithCustomError(this.mock, 'FailedCall');
}); });
}); });
}); });
@ -1099,7 +1099,7 @@ describe('TimelockController', function () {
this.mock this.mock
.connect(this.executor) .connect(this.executor)
.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt), .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt),
).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); ).to.be.revertedWithCustomError(this.mock, 'FailedCall');
}); });
it('call throw', async function () { it('call throw', async function () {
@ -1146,7 +1146,7 @@ describe('TimelockController', function () {
.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, {
gasLimit: '100000', gasLimit: '100000',
}), }),
).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); ).to.be.revertedWithCustomError(this.mock, 'FailedCall');
}); });
it('call payable with eth', async function () { it('call payable with eth', async function () {
@ -1199,7 +1199,7 @@ describe('TimelockController', function () {
this.mock this.mock
.connect(this.executor) .connect(this.executor)
.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt), .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt),
).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); ).to.be.revertedWithCustomError(this.mock, 'FailedCall');
expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n); expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n);
@ -1227,7 +1227,7 @@ describe('TimelockController', function () {
this.mock this.mock
.connect(this.executor) .connect(this.executor)
.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt), .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt),
).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); ).to.be.revertedWithCustomError(this.mock, 'FailedCall');
expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n); expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n);

@ -122,7 +122,7 @@ describe('ERC2771Forwarder', function () {
data: this.receiver.interface.encodeFunctionData('mockFunctionRevertsNoReason'), data: this.receiver.interface.encodeFunctionData('mockFunctionRevertsNoReason'),
}); });
await expect(this.forwarder.execute(request)).to.be.revertedWithCustomError(this.forwarder, 'FailedInnerCall'); await expect(this.forwarder.execute(request)).to.be.revertedWithCustomError(this.forwarder, 'FailedCall');
}); });
}); });

@ -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('initialization without parameters', function () {
describe('non payable', function () { describe('non payable', function () {
const expectedInitializedValue = 10n; const expectedInitializedValue = 10n;
@ -23,7 +42,7 @@ module.exports = function shouldBehaveLikeClone() {
describe('when not sending balance', function () { describe('when not sending balance', function () {
beforeEach('creating proxy', async function () { beforeEach('creating proxy', async function () {
this.proxy = await this.createClone(this.initializeData); this.proxy = await this.createClone({ initData: this.initializeData });
}); });
assertProxyInitialization({ assertProxyInitialization({
@ -36,7 +55,7 @@ module.exports = function shouldBehaveLikeClone() {
const value = 10n ** 6n; const value = 10n ** 6n;
it('reverts', async function () { 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;
}); });
}); });
}); });
@ -50,7 +69,7 @@ module.exports = function shouldBehaveLikeClone() {
describe('when not sending balance', function () { describe('when not sending balance', function () {
beforeEach('creating proxy', async function () { beforeEach('creating proxy', async function () {
this.proxy = await this.createClone(this.initializeData); this.proxy = await this.createClone({ initData: this.initializeData });
}); });
assertProxyInitialization({ assertProxyInitialization({
@ -63,7 +82,7 @@ module.exports = function shouldBehaveLikeClone() {
const value = 10n ** 6n; const value = 10n ** 6n;
beforeEach('creating proxy', async function () { beforeEach('creating proxy', async function () {
this.proxy = await this.createClone(this.initializeData, { value }); this.proxy = await this.createClone({ initData: this.initializeData, initValue: value });
}); });
assertProxyInitialization({ assertProxyInitialization({
@ -86,7 +105,7 @@ module.exports = function shouldBehaveLikeClone() {
describe('when not sending balance', function () { describe('when not sending balance', function () {
beforeEach('creating proxy', async function () { beforeEach('creating proxy', async function () {
this.proxy = await this.createClone(this.initializeData); this.proxy = await this.createClone({ initData: this.initializeData });
}); });
assertProxyInitialization({ assertProxyInitialization({
@ -99,7 +118,7 @@ module.exports = function shouldBehaveLikeClone() {
const value = 10n ** 6n; const value = 10n ** 6n;
it('reverts', async function () { 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;
}); });
}); });
}); });
@ -115,7 +134,7 @@ module.exports = function shouldBehaveLikeClone() {
describe('when not sending balance', function () { describe('when not sending balance', function () {
beforeEach('creating proxy', async function () { beforeEach('creating proxy', async function () {
this.proxy = await this.createClone(this.initializeData); this.proxy = await this.createClone({ initData: this.initializeData });
}); });
assertProxyInitialization({ assertProxyInitialization({
@ -128,7 +147,7 @@ module.exports = function shouldBehaveLikeClone() {
const value = 10n ** 6n; const value = 10n ** 6n;
beforeEach('creating proxy', async function () { beforeEach('creating proxy', async function () {
this.proxy = await this.createClone(this.initializeData, { value }); this.proxy = await this.createClone({ initData: this.initializeData, initValue: value });
}); });
assertProxyInitialization({ assertProxyInitialization({

@ -10,21 +10,29 @@ async function fixture() {
const factory = await ethers.deployContract('$Clones'); const factory = await ethers.deployContract('$Clones');
const implementation = await ethers.deployContract('DummyImplementation'); const implementation = await ethers.deployContract('DummyImplementation');
const newClone = async (initData, opts = {}) => { const newClone = async (opts = {}) => {
const clone = await factory.$clone.staticCall(implementation).then(address => implementation.attach(address)); const clone = await factory.$clone.staticCall(implementation).then(address => implementation.attach(address));
await factory.$clone(implementation); const tx = await (opts.deployValue
await deployer.sendTransaction({ to: clone, value: opts.value ?? 0n, data: initData ?? '0x' }); ? factory.$clone(implementation, ethers.Typed.uint256(opts.deployValue))
return clone; : 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 (initData, opts = {}) => { const newCloneDeterministic = async (opts = {}) => {
const salt = opts.salt ?? ethers.randomBytes(32); const salt = opts.salt ?? ethers.randomBytes(32);
const clone = await factory.$cloneDeterministic const clone = await factory.$cloneDeterministic
.staticCall(implementation, salt) .staticCall(implementation, salt)
.then(address => implementation.attach(address)); .then(address => implementation.attach(address));
await factory.$cloneDeterministic(implementation, salt); const tx = await (opts.deployValue
await deployer.sendTransaction({ to: clone, value: opts.value ?? 0n, data: initData ?? '0x' }); ? factory.$cloneDeterministic(implementation, salt, ethers.Typed.uint256(opts.deployValue))
return clone; : 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 }; return { deployer, factory, implementation, newClone, newCloneDeterministic };
@ -56,13 +64,13 @@ describe('Clones', function () {
// deploy once // deploy once
await expect(this.factory.$cloneDeterministic(this.implementation, salt)).to.emit( await expect(this.factory.$cloneDeterministic(this.implementation, salt)).to.emit(
this.factory, this.factory,
'return$cloneDeterministic', 'return$cloneDeterministic_address_bytes32',
); );
// deploy twice // deploy twice
await expect(this.factory.$cloneDeterministic(this.implementation, salt)).to.be.revertedWithCustomError( await expect(this.factory.$cloneDeterministic(this.implementation, salt)).to.be.revertedWithCustomError(
this.factory, this.factory,
'ERC1167FailedCreateClone', 'FailedDeployment',
); );
}); });
@ -80,7 +88,7 @@ describe('Clones', function () {
expect(predicted).to.equal(expected); expect(predicted).to.equal(expected);
await expect(this.factory.$cloneDeterministic(this.implementation, salt)) await expect(this.factory.$cloneDeterministic(this.implementation, salt))
.to.emit(this.factory, 'return$cloneDeterministic') .to.emit(this.factory, 'return$cloneDeterministic_address_bytes32')
.withArgs(predicted); .withArgs(predicted);
}); });
}); });

@ -23,13 +23,13 @@ describe('Address', function () {
describe('sendValue', function () { describe('sendValue', function () {
describe('when sender contract has no funds', function () { describe('when sender contract has no funds', function () {
it('sends 0 wei', async function () { it('sends 0 wei', async function () {
await expect(this.mock.$sendValue(this.other, 0)).to.changeEtherBalance(this.recipient, 0); await expect(this.mock.$sendValue(this.other, 0n)).to.changeEtherBalance(this.recipient, 0n);
}); });
it('reverts when sending non-zero amounts', async function () { it('reverts when sending non-zero amounts', async function () {
await expect(this.mock.$sendValue(this.other, 1)) await expect(this.mock.$sendValue(this.other, 1n))
.to.be.revertedWithCustomError(this.mock, 'AddressInsufficientBalance') .to.be.revertedWithCustomError(this.mock, 'InsufficientBalance')
.withArgs(this.mock); .withArgs(0n, 1n);
}); });
}); });
@ -42,7 +42,7 @@ describe('Address', function () {
describe('with EOA recipient', function () { describe('with EOA recipient', function () {
it('sends 0 wei', async function () { it('sends 0 wei', async function () {
await expect(this.mock.$sendValue(this.recipient, 0)).to.changeEtherBalance(this.recipient, 0); await expect(this.mock.$sendValue(this.recipient, 0n)).to.changeEtherBalance(this.recipient, 0n);
}); });
it('sends non-zero amounts', async function () { it('sends non-zero amounts', async function () {
@ -59,8 +59,8 @@ describe('Address', function () {
it('reverts when sending more than the balance', async function () { it('reverts when sending more than the balance', async function () {
await expect(this.mock.$sendValue(this.recipient, funds + 1n)) await expect(this.mock.$sendValue(this.recipient, funds + 1n))
.to.be.revertedWithCustomError(this.mock, 'AddressInsufficientBalance') .to.be.revertedWithCustomError(this.mock, 'InsufficientBalance')
.withArgs(this.mock); .withArgs(funds, funds + 1n);
}); });
}); });
@ -74,7 +74,7 @@ describe('Address', function () {
await this.targetEther.setAcceptEther(false); await this.targetEther.setAcceptEther(false);
await expect(this.mock.$sendValue(this.targetEther, funds)).to.be.revertedWithCustomError( await expect(this.mock.$sendValue(this.targetEther, funds)).to.be.revertedWithCustomError(
this.mock, this.mock,
'FailedInnerCall', 'FailedCall',
); );
}); });
}); });
@ -101,10 +101,7 @@ describe('Address', function () {
it('reverts when the called function reverts with no reason', async function () { it('reverts when the called function reverts with no reason', async function () {
const call = this.target.interface.encodeFunctionData('mockFunctionRevertsNoReason'); const call = this.target.interface.encodeFunctionData('mockFunctionRevertsNoReason');
await expect(this.mock.$functionCall(this.target, call)).to.be.revertedWithCustomError( await expect(this.mock.$functionCall(this.target, call)).to.be.revertedWithCustomError(this.mock, 'FailedCall');
this.mock,
'FailedInnerCall',
);
}); });
it('reverts when the called function reverts, bubbling up the revert reason', async function () { it('reverts when the called function reverts, bubbling up the revert reason', async function () {
@ -118,7 +115,7 @@ describe('Address', function () {
await expect(this.mock.$functionCall(this.target, call, { gasLimit: 120_000n })).to.be.revertedWithCustomError( await expect(this.mock.$functionCall(this.target, call, { gasLimit: 120_000n })).to.be.revertedWithCustomError(
this.mock, this.mock,
'FailedInnerCall', 'FailedCall',
); );
}); });
@ -132,10 +129,7 @@ describe('Address', function () {
const interface = new ethers.Interface(['function mockFunctionDoesNotExist()']); const interface = new ethers.Interface(['function mockFunctionDoesNotExist()']);
const call = interface.encodeFunctionData('mockFunctionDoesNotExist'); const call = interface.encodeFunctionData('mockFunctionDoesNotExist');
await expect(this.mock.$functionCall(this.target, call)).to.be.revertedWithCustomError( await expect(this.mock.$functionCall(this.target, call)).to.be.revertedWithCustomError(this.mock, 'FailedCall');
this.mock,
'FailedInnerCall',
);
}); });
}); });
@ -155,7 +149,7 @@ describe('Address', function () {
it('calls the requested function', async function () { it('calls the requested function', async function () {
const call = this.target.interface.encodeFunctionData('mockFunction'); const call = this.target.interface.encodeFunctionData('mockFunction');
await expect(this.mock.$functionCallWithValue(this.target, call, 0)) await expect(this.mock.$functionCallWithValue(this.target, call, 0n))
.to.emit(this.target, 'MockFunctionCalled') .to.emit(this.target, 'MockFunctionCalled')
.to.emit(this.mock, 'return$functionCallWithValue') .to.emit(this.mock, 'return$functionCallWithValue')
.withArgs(coder.encode(['string'], ['0x1234'])); .withArgs(coder.encode(['string'], ['0x1234']));
@ -169,8 +163,8 @@ describe('Address', function () {
const call = this.target.interface.encodeFunctionData('mockFunction'); const call = this.target.interface.encodeFunctionData('mockFunction');
await expect(this.mock.$functionCallWithValue(this.target, call, value)) await expect(this.mock.$functionCallWithValue(this.target, call, value))
.to.be.revertedWithCustomError(this.mock, 'AddressInsufficientBalance') .to.be.revertedWithCustomError(this.mock, 'InsufficientBalance')
.withArgs(this.mock); .withArgs(0n, value);
}); });
it('calls the requested function with existing value', async function () { it('calls the requested function with existing value', async function () {
@ -207,7 +201,7 @@ describe('Address', function () {
await expect(this.mock.$functionCallWithValue(this.target, call, value)).to.be.revertedWithCustomError( await expect(this.mock.$functionCallWithValue(this.target, call, value)).to.be.revertedWithCustomError(
this.mock, this.mock,
'FailedInnerCall', 'FailedCall',
); );
}); });
}); });
@ -225,7 +219,7 @@ describe('Address', function () {
await expect(this.mock.$functionStaticCall(this.target, call)).to.be.revertedWithCustomError( await expect(this.mock.$functionStaticCall(this.target, call)).to.be.revertedWithCustomError(
this.mock, this.mock,
'FailedInnerCall', 'FailedCall',
); );
}); });

@ -114,7 +114,7 @@ describe('Create2', function () {
await expect(this.factory.$deploy(0n, saltHex, this.constructorByteCode)).to.be.revertedWithCustomError( await expect(this.factory.$deploy(0n, saltHex, this.constructorByteCode)).to.be.revertedWithCustomError(
this.factory, this.factory,
'Create2FailedDeployment', 'FailedDeployment',
); );
}); });
@ -127,7 +127,7 @@ describe('Create2', function () {
it('fails deploying a contract if factory contract does not have sufficient balance', async function () { it('fails deploying a contract if factory contract does not have sufficient balance', async function () {
await expect(this.factory.$deploy(1n, saltHex, this.constructorByteCode)) await expect(this.factory.$deploy(1n, saltHex, this.constructorByteCode))
.to.be.revertedWithCustomError(this.factory, 'Create2InsufficientBalance') .to.be.revertedWithCustomError(this.factory, 'InsufficientBalance')
.withArgs(0n, 1n); .withArgs(0n, 1n);
}); });
}); });

@ -58,7 +58,7 @@ describe('EIP712', function () {
const clone = await factory const clone = await factory
.$clone(this.eip712) .$clone(this.eip712)
.then(tx => tx.wait()) .then(tx => tx.wait())
.then(receipt => receipt.logs.find(ev => ev.fragment.name == 'return$clone').args.instance) .then(receipt => receipt.logs.find(ev => ev.fragment.name == 'return$clone_address').args.instance)
.then(address => ethers.getContractAt('$EIP712Verifier', address)); .then(address => ethers.getContractAt('$EIP712Verifier', address));
const expectedDomain = { ...this.domain, verifyingContract: clone.target }; const expectedDomain = { ...this.domain, verifyingContract: clone.target };

Loading…
Cancel
Save