From 248be2fab08fc10f6da82e31a7e2cb311375a01e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 8 Nov 2023 16:18:23 +0000 Subject: [PATCH 01/44] Improve ERC4626 virtual offset notes (#4722) --- contracts/token/ERC20/extensions/ERC4626.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index ec6087231cb..dccf6900ade 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -27,14 +27,14 @@ import {Math} from "../../../utils/math/Math.sol"; * verifying the amount received is as expected, using a wrapper that performs these checks such as * https://github.com/fei-protocol/ERC4626#erc4626router-and-base[ERC4626Router]. * - * Since v4.9, this implementation uses virtual assets and shares to mitigate that risk. The `_decimalsOffset()` - * corresponds to an offset in the decimal representation between the underlying asset's decimals and the vault - * decimals. This offset also determines the rate of virtual shares to virtual assets in the vault, which itself - * determines the initial exchange rate. While not fully preventing the attack, analysis shows that the default offset - * (0) makes it non-profitable, as a result of the value being captured by the virtual shares (out of the attacker's - * donation) matching the attacker's expected gains. With a larger offset, the attack becomes orders of magnitude more - * expensive than it is profitable. More details about the underlying math can be found - * xref:erc4626.adoc#inflation-attack[here]. + * Since v4.9, this implementation introduces configurable virtual assets and shares to help developers mitigate that risk. + * The `_decimalsOffset()` corresponds to an offset in the decimal representation between the underlying asset's decimals + * and the vault decimals. This offset also determines the rate of virtual shares to virtual assets in the vault, which + * itself determines the initial exchange rate. While not fully preventing the attack, analysis shows that the default + * offset (0) makes it non-profitable even if an attacker is able to capture value from multiple user deposits, as a result + * of the value being captured by the virtual shares (out of the attacker's donation) matching the attacker's expected gains. + * With a larger offset, the attack becomes orders of magnitude more expensive than it is profitable. More details about the + * underlying math can be found xref:erc4626.adoc#inflation-attack[here]. * * The drawback of this approach is that the virtual shares do capture (a very small) part of the value being accrued * to the vault. Also, if the vault experiences losses, the users try to exit the vault, the virtual shares and assets From f1f427ddaf7c4f365782a4a7a4e4b0fec64c3f7c Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Wed, 8 Nov 2023 18:14:06 +0000 Subject: [PATCH 02/44] Migrate finance tests to ethers.js (#4723) Co-authored-by: Hadrien Croubois Co-authored-by: ernestognw --- test/finance/VestingWallet.behavior.js | 48 +++++----------- test/finance/VestingWallet.test.js | 78 +++++++++++++++----------- 2 files changed, 59 insertions(+), 67 deletions(-) diff --git a/test/finance/VestingWallet.behavior.js b/test/finance/VestingWallet.behavior.js index afd4c0495e5..c1e0f8013a5 100644 --- a/test/finance/VestingWallet.behavior.js +++ b/test/finance/VestingWallet.behavior.js @@ -1,54 +1,36 @@ -const { time } = require('@nomicfoundation/hardhat-network-helpers'); -const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { bigint: time } = require('../helpers/time'); -function releasedEvent(token, amount) { - return token ? ['ERC20Released', { token: token.address, amount }] : ['EtherReleased', { amount }]; -} - -function shouldBehaveLikeVesting(beneficiary) { +function shouldBehaveLikeVesting() { it('check vesting schedule', async function () { - const [vestedAmount, releasable, ...args] = this.token - ? ['vestedAmount(address,uint64)', 'releasable(address)', this.token.address] - : ['vestedAmount(uint64)', 'releasable()']; - for (const timestamp of this.schedule) { - await time.increaseTo(timestamp); + await time.forward.timestamp(timestamp); const vesting = this.vestingFn(timestamp); - expect(await this.mock.methods[vestedAmount](...args, timestamp)).to.be.bignumber.equal(vesting); - - expect(await this.mock.methods[releasable](...args)).to.be.bignumber.equal(vesting); + expect(await this.mock.vestedAmount(...this.args, timestamp)).to.be.equal(vesting); + expect(await this.mock.releasable(...this.args)).to.be.equal(vesting); } }); it('execute vesting schedule', async function () { - const [release, ...args] = this.token ? ['release(address)', this.token.address] : ['release()']; - - let released = web3.utils.toBN(0); - const before = await this.getBalance(beneficiary); - + let released = 0n; { - const receipt = await this.mock.methods[release](...args); - - await expectEvent.inTransaction(receipt.tx, this.mock, ...releasedEvent(this.token, '0')); - - await this.checkRelease(receipt, beneficiary, '0'); + const tx = await this.mock.release(...this.args); + await expect(tx) + .to.emit(this.mock, this.releasedEvent) + .withArgs(...this.argsVerify, 0); - expect(await this.getBalance(beneficiary)).to.be.bignumber.equal(before); + await this.checkRelease(tx, 0n); } for (const timestamp of this.schedule) { - await time.setNextBlockTimestamp(timestamp); + await time.forward.timestamp(timestamp, false); const vested = this.vestingFn(timestamp); - const receipt = await this.mock.methods[release](...args); - await expectEvent.inTransaction(receipt.tx, this.mock, ...releasedEvent(this.token, vested.sub(released))); - - await this.checkRelease(receipt, beneficiary, vested.sub(released)); - - expect(await this.getBalance(beneficiary)).to.be.bignumber.equal(before.add(vested)); + const tx = await this.mock.release(...this.args); + await expect(tx).to.emit(this.mock, this.releasedEvent); + await this.checkRelease(tx, vested - released); released = vested; } }); diff --git a/test/finance/VestingWallet.test.js b/test/finance/VestingWallet.test.js index 918e56345fb..e3739dd9071 100644 --- a/test/finance/VestingWallet.test.js +++ b/test/finance/VestingWallet.test.js @@ -1,69 +1,79 @@ -const { constants, expectEvent, time } = require('@openzeppelin/test-helpers'); -const { web3 } = require('@openzeppelin/test-helpers/src/setup'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { BNmin } = require('../helpers/math'); -const { expectRevertCustomError } = require('../helpers/customError'); - -const VestingWallet = artifacts.require('VestingWallet'); -const ERC20 = artifacts.require('$ERC20'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { bigint: time } = require('../helpers/time'); +const { min } = require('../helpers/math'); const { shouldBehaveLikeVesting } = require('./VestingWallet.behavior'); -contract('VestingWallet', function (accounts) { - const [sender, beneficiary] = accounts; +async function fixture() { + const amount = ethers.parseEther('100'); + const duration = time.duration.years(4); + const start = (await time.clock.timestamp()) + time.duration.hours(1); - const amount = web3.utils.toBN(web3.utils.toWei('100')); - const duration = web3.utils.toBN(4 * 365 * 86400); // 4 years + const [sender, beneficiary] = await ethers.getSigners(); + const mock = await ethers.deployContract('VestingWallet', [beneficiary, start, duration]); + return { mock, amount, duration, start, sender, beneficiary }; +} +describe('VestingWallet', function () { beforeEach(async function () { - this.start = (await time.latest()).addn(3600); // in 1 hour - this.mock = await VestingWallet.new(beneficiary, this.start, duration); + Object.assign(this, await loadFixture(fixture)); }); it('rejects zero address for beneficiary', async function () { - await expectRevertCustomError( - VestingWallet.new(constants.ZERO_ADDRESS, this.start, duration), - 'OwnableInvalidOwner', - [constants.ZERO_ADDRESS], - ); + await expect(ethers.deployContract('VestingWallet', [ethers.ZeroAddress, this.start, this.duration])) + .revertedWithCustomError(this.mock, 'OwnableInvalidOwner') + .withArgs(ethers.ZeroAddress); }); it('check vesting contract', async function () { - expect(await this.mock.owner()).to.be.equal(beneficiary); - expect(await this.mock.start()).to.be.bignumber.equal(this.start); - expect(await this.mock.duration()).to.be.bignumber.equal(duration); - expect(await this.mock.end()).to.be.bignumber.equal(this.start.add(duration)); + expect(await this.mock.owner()).to.be.equal(this.beneficiary.address); + expect(await this.mock.start()).to.be.equal(this.start); + expect(await this.mock.duration()).to.be.equal(this.duration); + expect(await this.mock.end()).to.be.equal(this.start + this.duration); }); describe('vesting schedule', function () { - beforeEach(async function () { + beforeEach(function () { this.schedule = Array(64) .fill() - .map((_, i) => web3.utils.toBN(i).mul(duration).divn(60).add(this.start)); - this.vestingFn = timestamp => BNmin(amount, amount.mul(timestamp.sub(this.start)).div(duration)); + .map((_, i) => (BigInt(i) * this.duration) / 60n + this.start); + this.vestingFn = timestamp => min(this.amount, (this.amount * (timestamp - this.start)) / this.duration); }); describe('Eth vesting', function () { beforeEach(async function () { - await web3.eth.sendTransaction({ from: sender, to: this.mock.address, value: amount }); - this.getBalance = account => web3.eth.getBalance(account).then(web3.utils.toBN); - this.checkRelease = () => {}; + await this.sender.sendTransaction({ to: this.mock, value: this.amount }); + + this.getBalance = signer => ethers.provider.getBalance(signer); + this.checkRelease = (tx, amount) => expect(tx).to.changeEtherBalances([this.beneficiary], [amount]); + + this.releasedEvent = 'EtherReleased'; + this.args = []; + this.argsVerify = []; }); - shouldBehaveLikeVesting(beneficiary); + shouldBehaveLikeVesting(); }); describe('ERC20 vesting', function () { beforeEach(async function () { - this.token = await ERC20.new('Name', 'Symbol'); + this.token = await ethers.deployContract('$ERC20', ['Name', 'Symbol']); + await this.token.$_mint(this.mock, this.amount); + this.getBalance = account => this.token.balanceOf(account); - this.checkRelease = (receipt, to, value) => - expectEvent.inTransaction(receipt.tx, this.token, 'Transfer', { from: this.mock.address, to, value }); + this.checkRelease = async (tx, amount) => { + await expect(tx).to.emit(this.token, 'Transfer').withArgs(this.mock.target, this.beneficiary.address, amount); + await expect(tx).to.changeTokenBalances(this.token, [this.mock, this.beneficiary], [-amount, amount]); + }; - await this.token.$_mint(this.mock.address, amount); + this.releasedEvent = 'ERC20Released'; + this.args = [ethers.Typed.address(this.token.target)]; + this.argsVerify = [this.token.target]; }); - shouldBehaveLikeVesting(beneficiary); + shouldBehaveLikeVesting(); }); }); }); From cb1ef861e57658f9334fa76e4a34d6f27c7532b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 9 Nov 2023 15:09:05 +0000 Subject: [PATCH 03/44] Add `AccessManager` guide (#4691) Co-authored-by: Hadrien Croubois Co-authored-by: Eric Lau Co-authored-by: Zack Reneau-Wedeen --- .../AccessControlERC20MintBase.sol | 25 ++ .../AccessControlERC20MintMissing.sol | 24 ++ .../AccessControlERC20MintOnlyRole.sol | 23 ++ .../AccessManagedERC20MintBase.sol | 16 ++ .../MyContractOwnable.sol | 2 +- .../ROOT/images/access-control-multiple.svg | 97 ++++++++ .../ROOT/images/access-manager-functions.svg | 47 ++++ docs/modules/ROOT/images/access-manager.svg | 99 ++++++++ docs/modules/ROOT/pages/access-control.adoc | 229 ++++++++++++------ 9 files changed, 487 insertions(+), 75 deletions(-) create mode 100644 contracts/mocks/docs/access-control/AccessControlERC20MintBase.sol create mode 100644 contracts/mocks/docs/access-control/AccessControlERC20MintMissing.sol create mode 100644 contracts/mocks/docs/access-control/AccessControlERC20MintOnlyRole.sol create mode 100644 contracts/mocks/docs/access-control/AccessManagedERC20MintBase.sol rename contracts/mocks/docs/{ => access-control}/MyContractOwnable.sol (86%) create mode 100644 docs/modules/ROOT/images/access-control-multiple.svg create mode 100644 docs/modules/ROOT/images/access-manager-functions.svg create mode 100644 docs/modules/ROOT/images/access-manager.svg diff --git a/contracts/mocks/docs/access-control/AccessControlERC20MintBase.sol b/contracts/mocks/docs/access-control/AccessControlERC20MintBase.sol new file mode 100644 index 00000000000..25139cbc478 --- /dev/null +++ b/contracts/mocks/docs/access-control/AccessControlERC20MintBase.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AccessControl} from "../../../access/AccessControl.sol"; +import {ERC20} from "../../../token/ERC20/ERC20.sol"; + +contract AccessControlERC20MintBase is ERC20, AccessControl { + // Create a new role identifier for the minter role + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + error CallerNotMinter(address caller); + + constructor(address minter) ERC20("MyToken", "TKN") { + // Grant the minter role to a specified account + _grantRole(MINTER_ROLE, minter); + } + + function mint(address to, uint256 amount) public { + // Check that the calling account has the minter role + if (!hasRole(MINTER_ROLE, msg.sender)) { + revert CallerNotMinter(msg.sender); + } + _mint(to, amount); + } +} diff --git a/contracts/mocks/docs/access-control/AccessControlERC20MintMissing.sol b/contracts/mocks/docs/access-control/AccessControlERC20MintMissing.sol new file mode 100644 index 00000000000..46002fd047f --- /dev/null +++ b/contracts/mocks/docs/access-control/AccessControlERC20MintMissing.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AccessControl} from "../../../access/AccessControl.sol"; +import {ERC20} from "../../../token/ERC20/ERC20.sol"; + +contract AccessControlERC20MintMissing is ERC20, AccessControl { + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); + + constructor() ERC20("MyToken", "TKN") { + // Grant the contract deployer the default admin role: it will be able + // to grant and revoke any roles + _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { + _mint(to, amount); + } + + function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) { + _burn(from, amount); + } +} diff --git a/contracts/mocks/docs/access-control/AccessControlERC20MintOnlyRole.sol b/contracts/mocks/docs/access-control/AccessControlERC20MintOnlyRole.sol new file mode 100644 index 00000000000..a71060ad896 --- /dev/null +++ b/contracts/mocks/docs/access-control/AccessControlERC20MintOnlyRole.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AccessControl} from "../../../access/AccessControl.sol"; +import {ERC20} from "../../../token/ERC20/ERC20.sol"; + +contract AccessControlERC20Mint is ERC20, AccessControl { + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); + + constructor(address minter, address burner) ERC20("MyToken", "TKN") { + _grantRole(MINTER_ROLE, minter); + _grantRole(BURNER_ROLE, burner); + } + + function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { + _mint(to, amount); + } + + function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) { + _burn(from, amount); + } +} diff --git a/contracts/mocks/docs/access-control/AccessManagedERC20MintBase.sol b/contracts/mocks/docs/access-control/AccessManagedERC20MintBase.sol new file mode 100644 index 00000000000..02ae00a1ae7 --- /dev/null +++ b/contracts/mocks/docs/access-control/AccessManagedERC20MintBase.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {AccessManaged} from "../../../access/manager/AccessManaged.sol"; +import {ERC20} from "../../../token/ERC20/ERC20.sol"; + +contract AccessManagedERC20Mint is ERC20, AccessManaged { + constructor(address manager) ERC20("MyToken", "TKN") AccessManaged(manager) {} + + // Minting is restricted according to the manager rules for this function. + // The function is identified by its selector: 0x40c10f19. + // Calculated with bytes4(keccak256('mint(address,uint256)')) + function mint(address to, uint256 amount) public restricted { + _mint(to, amount); + } +} diff --git a/contracts/mocks/docs/MyContractOwnable.sol b/contracts/mocks/docs/access-control/MyContractOwnable.sol similarity index 86% rename from contracts/mocks/docs/MyContractOwnable.sol rename to contracts/mocks/docs/access-control/MyContractOwnable.sol index 01847c0362e..0dfc804f256 100644 --- a/contracts/mocks/docs/MyContractOwnable.sol +++ b/contracts/mocks/docs/access-control/MyContractOwnable.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; -import {Ownable} from "../../access/Ownable.sol"; +import {Ownable} from "../../../access/Ownable.sol"; contract MyContract is Ownable { constructor(address initialOwner) Ownable(initialOwner) {} diff --git a/docs/modules/ROOT/images/access-control-multiple.svg b/docs/modules/ROOT/images/access-control-multiple.svg new file mode 100644 index 00000000000..0314e09e4eb --- /dev/null +++ b/docs/modules/ROOT/images/access-control-multiple.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/modules/ROOT/images/access-manager-functions.svg b/docs/modules/ROOT/images/access-manager-functions.svg new file mode 100644 index 00000000000..dbbf04179bd --- /dev/null +++ b/docs/modules/ROOT/images/access-manager-functions.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/modules/ROOT/images/access-manager.svg b/docs/modules/ROOT/images/access-manager.svg new file mode 100644 index 00000000000..12f91bae796 --- /dev/null +++ b/docs/modules/ROOT/images/access-manager.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/modules/ROOT/pages/access-control.adoc b/docs/modules/ROOT/pages/access-control.adoc index 5513a47f9bc..b2d6dbbfdde 100644 --- a/docs/modules/ROOT/pages/access-control.adoc +++ b/docs/modules/ROOT/pages/access-control.adoc @@ -10,10 +10,10 @@ The most common and basic form of access control is the concept of _ownership_: OpenZeppelin Contracts provides xref:api:access.adoc#Ownable[`Ownable`] for implementing ownership in your contracts. ```solidity -include::api:example$MyContractOwnable.sol[] +include::api:example$access-control/MyContractOwnable.sol[] ``` -By default, the xref:api:access.adoc#Ownable-owner--[`owner`] of an `Ownable` contract is the account that deployed it, which is usually exactly what you want. +At deployment, the xref:api:access.adoc#Ownable-owner--[`owner`] of an `Ownable` contract is set to the provided `initialOwner` parameter. Ownable also lets you: @@ -43,32 +43,11 @@ Most software uses access control systems that are role-based: some users are re OpenZeppelin Contracts provides xref:api:access.adoc#AccessControl[`AccessControl`] for implementing role-based access control. Its usage is straightforward: for each role that you want to define, you will create a new _role identifier_ that is used to grant, revoke, and check if an account has that role. -Here's a simple example of using `AccessControl` in an xref:tokens.adoc#ERC20[`ERC20` token] to define a 'minter' role, which allows accounts that have it create new tokens: +Here's a simple example of using `AccessControl` in an xref:erc20.adoc[`ERC20` token] to define a 'minter' role, which allows accounts that have it create new tokens: [source,solidity] ---- -// contracts/MyToken.sol -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -contract MyToken is ERC20, AccessControl { - // Create a new role identifier for the minter role - bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); - - constructor(address minter) ERC20("MyToken", "TKN") { - // Grant the minter role to a specified account - _grantRole(MINTER_ROLE, minter); - } - - function mint(address to, uint256 amount) public { - // Check that the calling account has the minter role - require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter"); - _mint(to, amount); - } -} +include::api:example$access-control/AccessControlERC20MintBase.sol[] ---- NOTE: Make sure you fully understand how xref:api:access.adoc#AccessControl[`AccessControl`] works before using it on your system, or copy-pasting the examples from this guide. @@ -79,30 +58,7 @@ Let's augment our ERC20 token example by also defining a 'burner' role, which le [source,solidity] ---- -// contracts/MyToken.sol -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -contract MyToken is ERC20, AccessControl { - bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); - bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); - - constructor(address minter, address burner) ERC20("MyToken", "TKN") { - _grantRole(MINTER_ROLE, minter); - _grantRole(BURNER_ROLE, burner); - } - - function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { - _mint(to, amount); - } - - function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) { - _burn(from, amount); - } -} +include::api:example$access-control/AccessControlERC20MintOnlyRole.sol[] ---- So clean! By splitting concerns this way, more granular levels of permission may be implemented than were possible with the simpler _ownership_ approach to access control. Limiting what each component of a system is able to do is known as the https://en.wikipedia.org/wiki/Principle_of_least_privilege[principle of least privilege], and is a good security practice. Note that each account may still have more than one role, if so desired. @@ -124,31 +80,7 @@ Let's take a look at the ERC20 token example, this time taking advantage of the [source,solidity] ---- -// contracts/MyToken.sol -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -contract MyToken is ERC20, AccessControl { - bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); - bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); - - constructor() ERC20("MyToken", "TKN") { - // Grant the contract deployer the default admin role: it will be able - // to grant and revoke any roles - _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); - } - - function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { - _mint(to, amount); - } - - function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) { - _burn(from, amount); - } -} +include::api:example$access-control/AccessControlERC20MintMissing.sol[] ---- Note that, unlike the previous examples, no accounts are granted the 'minter' or 'burner' roles. However, because those roles' admin role is the default admin role, and _that_ role was granted to `msg.sender`, that same account can call `grantRole` to give minting or burning permission, and `revokeRole` to remove it. @@ -204,3 +136,152 @@ TIP: A recommended configuration is to grant both roles to a secure governance c Operations executed by the xref:api:governance.adoc#TimelockController[`TimelockController`] are not subject to a fixed delay but rather a minimum delay. Some major updates might call for a longer delay. For example, if a delay of just a few days might be sufficient for users to audit a minting operation, it makes sense to use a delay of a few weeks, or even a few months, when scheduling a smart contract upgrade. The minimum delay (accessible through the xref:api:governance.adoc#TimelockController-getMinDelay--[`getMinDelay`] method) can be updated by calling the xref:api:governance.adoc#TimelockController-updateDelay-uint256-[`updateDelay`] function. Bear in mind that access to this function is only accessible by the timelock itself, meaning this maintenance operation has to go through the timelock itself. + +[[access-management]] +== Access Management + +For a system of contracts, better integrated role management can be achieved with an xref:api:access.adoc#AccessManager[`AccessManager`] instance. Instead of managing each contract's permission separately, AccessManager stores all the permissions in a single contract, making your protocol easier to audit and maintain. + +Although xref:api:access.adoc#AccessControl[`AccessControl`] offers a more dynamic solution for adding permissions to your contracts than Ownable, decentralized protocols tend to become more complex after integrating new contract instances and requires you to keep track of permissions separately in each contract. This increases the complexity of permissions management and monitoring across the system. + +image::access-control-multiple.svg[Access Control multiple] + +Protocols managing permissions in production systems often require more integrated alternatives to fragmented permissions through multiple `AccessControl` instances. + +image::access-manager.svg[AccessManager] + +The AccessManager is designed around the concept of role and target functions: + +* Roles are granted to accounts (addresses) following a many-to-many approach for flexibility. This means that each user can have one or multiple roles and multiple users can have the same role. +* Access to a restricted target function is limited to one role. A target function is defined by one https://docs.soliditylang.org/en/v0.8.20/abi-spec.html#function-selector[function selector] on one contract (called target). + +For a call to be authorized, the caller must bear the role that is assigned to the current target function (contract address + function selector). + +image::access-manager-functions.svg[AccessManager functions] + +=== Using `AccessManager` + +OpenZeppelin Contracts provides xref:api:access.adoc#AccessManager[`AccessManager`] for managing roles across any number of contracts. The `AccessManager` itself is a contract that can be deployed and used out of the box. It sets an initial admin in the constructor who will be allowed to perform management operations. + +In order to restrict access to some functions of your contract, you should inherit from the xref:api:access.adoc#AccessManaged[`AccessManaged`] contract provided along with the manager. This provides the `restricted` modifier that can be used to protect any externally facing function. Note that you will have to specify the address of the AccessManager instance (xref:api:access.adoc#AccessManaged-constructor-address-[`initialAuthority`]) in the constructor so the `restricted` modifier knows which manager to use for checking permissions. + +Here's a simple example of an xref:tokens.adoc#ERC20[`ERC20` token] that defines a `mint` function that is restricted by an xref:api:access.adoc#AccessManager[`AccessManager`]: + +```solidity +include::api:example$access-control/AccessManagedERC20MintBase.sol[] +``` + +NOTE: Make sure you fully understand how xref:api:access.adoc#AccessManager[`AccessManager`] works before using it or copy-pasting the examples from this guide. + +Once the managed contract has been deployed, it is now under the manager's control. The initial admin can then assign the minter role to an address and also allow the role to call the `mint` function. For example, this is demonstrated in the following Javascript code using Ethers.js: + +```javascript +// const target = ...; +// const user = ...; +const MINTER = 42n; // Roles are uint64 (0 is reserved for the ADMIN_ROLE) + +// Grant the minter role with no execution delay +await manager.grantRole(MINTER, user, 0); + +// Allow the minter role to call the function selector +// corresponding to the mint function +await manager.setTargetFunctionRole( + target, + ['0x40c10f19'], // bytes4(keccak256('mint(address,uint256)')) + MINTER +); + +Even though each role has its own list of function permissions, each role member (`address`) has an execution delay that will dictate how long the account should wait to execute a function that requires its role. Delayed operations must have the xref:api:access.adoc#AccessManager-schedule-address-bytes-uint48-[`schedule`] function called on them first in the AccessManager before they can be executed, either by calling to the target function or using the AccessManager's xref:api:access.adoc#AccessManager-execute-address-bytes-[`execute`] function. + +Additionally, roles can have a granting delay that prevents adding members immediately. The AccessManager admins can set this grant delay as follows: + +```javascript +const HOUR = 60 * 60; + +const GRANT_DELAY = 24 * HOUR; +const EXECUTION_DELAY = 5 * HOUR; +const ACCOUNT = "0x..."; + +await manager.connect(initialAdmin).setGrantDelay(MINTER, GRANT_DELAY); + +// The role will go into effect after the GRANT_DELAY passes +await manager.connect(initialAdmin).grantRole(MINTER, ACCOUNT, EXECUTION_DELAY); +``` + +Note that roles do not define a name. As opposed to the xref:api:access.adoc#AccessControl[`AccessControl`] case, roles are identified as numeric values instead of being hardcoded in the contract as `bytes32` values. It is still possible to allow for tooling discovery (e.g. for role exploration) using role labeling with the xref:api:access.adoc#AccessManager-labelRole-uint64-string-[`labelRole`] function. + +```javascript +await manager.labelRole(MINTER, "MINTER"); +``` + +Given the admins of the `AccessManaged` can modify all of its permissions, it's recommended to keep only a single admin address secured under a multisig or governance layer. To achieve this, it is possible for the initial admin to set up all the required permissions, targets, and functions, assign a new admin, and finally renounce its admin role. + +For improved incident response coordination, the manager includes a mode where administrators can completely close a target contract. When closed, all calls to restricted target functions in a target contract will revert. + +Closing and opening contracts don't alter any of their settings, neither permissions nor delays. Particularly, the roles required for calling specific target functions are not modified. + +This mode is useful for incident response operations that require temporarily shutting down a contract in order to evaluate emergencies and reconfigure permissions. + +```javascript +const target = await myToken.getAddress(); + +// Token's `restricted` functions closed +await manager.setTargetClosed(target, true); + +// Token's `restricted` functions open +await manager.setTargetClosed(target, false); +``` + +WARNING: Even if an `AccessManager` defines permissions for a target function, these won't be applied if the managed contract instance is not using the xref:api:access.adoc#AccessManaged-restricted--[`restricted`] modifier for that function, or if its manager is a different one. + +=== Role Admins and Guardians + +An important aspect of the AccessControl contract is that roles aren't granted nor revoked by role members. Instead, it relies on the concept of a role admin for granting and revoking. + +In the case of the `AccessManager`, the same rule applies and only the role's admins are able to call xref:api:access.adoc#AccessManager-grantRole-uint64-address-uint32-[grant] and xref:api:access.adoc#AccessManager-revokeRole-uint64-address-[revoke] functions. Note that calling these functions will be subject to the execution delay that the executing role admin has. + +Additionally, the `AccessManager` stores a _guardian_ as an extra protection for each role. This guardian has the ability to cancel operations that have been scheduled by any role member with an execution delay. Consider that a role will have its initial admin and guardian default to the `ADMIN_ROLE` (`0`). + +IMPORTANT: Be careful with the members of `ADMIN_ROLE`, since it acts as the default admin and guardian for every role. A misbehaved guardian can cancel operations at will, affecting the AccessManager's operation. + +=== Manager configuration + +The `AccessManager` provides a built-in interface for configuring permission settings that can be accessed by its `ADMIN_ROLE` members. + +This configuration interface includes the following functions: + +* Add a label to a role using the xref:api:access.adoc#AccessManager-labelRole-uint64-string-[`labelRole`] function. +* Assign the admin and guardian of a role with xref:api:access.adoc#AccessManager-setRoleAdmin-uint64-uint64-[`setRoleAdmin`] and xref:api:access.adoc#AccessManager-setRoleGuardian-uint64-uint64-[`setRoleGuardian`]. +* Set each role's grant delay via xref:api:access.adoc#AccessManager-setGrantDelay-uint64-uint32-[`setGrantDelay`]. + +As an admin, some actions will require a delay. Similar to each member's execution delay, some admin operations require waiting for execution and should follow the xref:api:access.adoc#AccessManager-schedule-address-bytes-uint48-[`schedule`] and xref:api:access.adoc#AccessManager-execute-address-bytes-[`execute`] workflow. + +More specifically, these delayed functions are those for configuring the settings of a specific target contract. The delay applied to these functions can be adjusted by the manager admins with xref:api:access.adoc#AccessManager-setTargetAdminDelay-address-uint32-[`setTargetAdminDelay`]. + +The delayed admin actions are: + +* Updating an `AccessManaged` contract xref:api:access.adoc#AccessManaged-authority--[authority] using xref:api:access.adoc#AccessManager-updateAuthority-address-address-[`updateAuthority`]. +* Closing or opening a target via xref:api:access.adoc#AccessManager-setTargetClosed-address-bool-[`setTargetClosed`]. +* Changing permissions of whether a role can call a target function with xref:api:access.adoc#AccessManager-setTargetFunctionRole-address-bytes4---uint64-[`setTargetFunctionRole`]. + +=== Using with Ownable + +Contracts already inheriting from xref:api:access.adoc#Ownable[`Ownable`] can migrate to AccessManager by transferring ownership to the manager. After that, all calls to functions with the `onlyOwner` modifier should be called through the manager's xref:api:access.adoc#AccessManager-execute-address-bytes-[`execute`] function, even if the caller doesn't require a delay. + +```javascript +await ownable.connect(owner).transferOwnership(accessManager); +``` + +=== Using with AccessControl + +For systems already using xref:api:access.adoc#AccessControl[`AccessControl`], the `DEFAULT_ADMIN_ROLE` can be granted to the `AccessManager` after revoking every other role. Subsequent calls should be made through the manager's xref:api:access.adoc#AccessManager-execute-address-bytes-[`execute`] method, similar to the Ownable case. + +```javascript +// Revoke old roles +await accessControl.connect(admin).revokeRoke(MINTER_ROLE, account); + +// Grant the admin role to the access manager +await accessControl.connect(admin).grantRole(DEFAULT_ADMIN_ROLE, accessManager); + +await accessControl.connect(admin).renounceRole(DEFAULT_ADMIN_ROLE, admin); +``` From cf6ff90b6d4d0180f0c862e16976f4dcf9847c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 9 Nov 2023 15:56:54 +0000 Subject: [PATCH 04/44] Migrate `AccessManager` tests to ethers (#4710) Co-authored-by: Hadrien Croubois --- contracts/mocks/AuthorityMock.sol | 2 +- test/access/Ownable.test.js | 6 + test/access/manager/AccessManaged.test.js | 149 +- test/access/manager/AccessManager.behavior.js | 125 +- .../access/manager/AccessManager.predicate.js | 165 +- test/access/manager/AccessManager.test.js | 2114 ++++++++--------- test/access/manager/AuthorityUtils.test.js | 69 +- test/helpers/access-manager.js | 42 +- test/helpers/constants.js | 11 +- test/helpers/namespaced-storage.js | 12 +- 10 files changed, 1244 insertions(+), 1451 deletions(-) diff --git a/contracts/mocks/AuthorityMock.sol b/contracts/mocks/AuthorityMock.sol index bf2434b0a86..4f3e1de3acc 100644 --- a/contracts/mocks/AuthorityMock.sol +++ b/contracts/mocks/AuthorityMock.sol @@ -52,7 +52,7 @@ contract AuthorityNoResponse { function canCall(address /* caller */, address /* target */, bytes4 /* selector */) external view {} } -contract AuthoritiyObserveIsConsuming { +contract AuthorityObserveIsConsuming { event ConsumeScheduledOpCalled(address caller, bytes data, bytes4 isConsuming); function canCall( diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index fabcb7d52d7..568d52b6850 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -13,6 +13,12 @@ describe('Ownable', function () { Object.assign(this, await loadFixture(fixture)); }); + it('emits ownership transfer events during construction', async function () { + await expect(await this.ownable.deploymentTransaction()) + .to.emit(this.ownable, 'OwnershipTransferred') + .withArgs(ethers.ZeroAddress, this.owner.address); + }); + it('rejects zero address for initialOwner', async function () { await expect(ethers.deployContract('$Ownable', [ethers.ZeroAddress])) .to.be.revertedWithCustomError({ interface: this.ownable.interface }, 'OwnableInvalidOwner') diff --git a/test/access/manager/AccessManaged.test.js b/test/access/manager/AccessManaged.test.js index ee7924fecb8..f3a433ebd23 100644 --- a/test/access/manager/AccessManaged.test.js +++ b/test/access/manager/AccessManaged.test.js @@ -1,142 +1,145 @@ -const { expectEvent, time, expectRevert } = require('@openzeppelin/test-helpers'); -const { selector } = require('../../helpers/methods'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const { - time: { setNextBlockTimestamp }, -} = require('@nomicfoundation/hardhat-network-helpers'); +const { bigint: time } = require('../../helpers/time'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { impersonate } = require('../../helpers/account'); +const { ethers } = require('hardhat'); -const AccessManaged = artifacts.require('$AccessManagedTarget'); -const AccessManager = artifacts.require('$AccessManager'); +async function fixture() { + const [admin, roleMember, other] = await ethers.getSigners(); -const AuthoritiyObserveIsConsuming = artifacts.require('$AuthoritiyObserveIsConsuming'); + const authority = await ethers.deployContract('$AccessManager', [admin]); + const managed = await ethers.deployContract('$AccessManagedTarget', [authority]); -contract('AccessManaged', function (accounts) { - const [admin, roleMember, other] = accounts; + const anotherAuthority = await ethers.deployContract('$AccessManager', [admin]); + const authorityObserveIsConsuming = await ethers.deployContract('$AuthorityObserveIsConsuming'); + await impersonate(authority.target); + const authorityAsSigner = await ethers.getSigner(authority.target); + + return { + roleMember, + other, + authorityAsSigner, + authority, + managed, + authorityObserveIsConsuming, + anotherAuthority, + }; +} + +describe('AccessManaged', function () { beforeEach(async function () { - this.authority = await AccessManager.new(admin); - this.managed = await AccessManaged.new(this.authority.address); + Object.assign(this, await loadFixture(fixture)); }); it('sets authority and emits AuthorityUpdated event during construction', async function () { - await expectEvent.inConstruction(this.managed, 'AuthorityUpdated', { - authority: this.authority.address, - }); - expect(await this.managed.authority()).to.eq(this.authority.address); + await expect(await this.managed.deploymentTransaction()) + .to.emit(this.managed, 'AuthorityUpdated') + .withArgs(this.authority.target); }); describe('restricted modifier', function () { - const method = 'fnRestricted()'; - beforeEach(async function () { - this.selector = selector(method); - this.role = web3.utils.toBN(42); - await this.authority.$_setTargetFunctionRole(this.managed.address, this.selector, this.role); - await this.authority.$_grantRole(this.role, roleMember, 0, 0); + this.selector = this.managed.fnRestricted.getFragment().selector; + this.role = 42n; + await this.authority.$_setTargetFunctionRole(this.managed, this.selector, this.role); + await this.authority.$_grantRole(this.role, this.roleMember, 0, 0); }); it('succeeds when role is granted without execution delay', async function () { - await this.managed.methods[method]({ from: roleMember }); + await this.managed.connect(this.roleMember)[this.selector](); }); it('reverts when role is not granted', async function () { - await expectRevertCustomError(this.managed.methods[method]({ from: other }), 'AccessManagedUnauthorized', [ - other, - ]); + await expect(this.managed.connect(this.other)[this.selector]()) + .to.be.revertedWithCustomError(this.managed, 'AccessManagedUnauthorized') + .withArgs(this.other.address); }); it('panics in short calldata', async function () { // We avoid adding the `restricted` modifier to the fallback function because other tests may depend on it // being accessible without restrictions. We check for the internal `_checkCanCall` instead. - await expectRevert.unspecified(this.managed.$_checkCanCall(other, '0x1234')); + await expect(this.managed.$_checkCanCall(this.roleMember, '0x1234')).to.be.reverted; }); describe('when role is granted with execution delay', function () { beforeEach(async function () { - const executionDelay = web3.utils.toBN(911); - await this.authority.$_grantRole(this.role, roleMember, 0, executionDelay); + const executionDelay = 911n; + await this.authority.$_grantRole(this.role, this.roleMember, 0, executionDelay); }); it('reverts if the operation is not scheduled', async function () { - const calldata = this.managed.contract.methods[method]().encodeABI(); - const opId = await this.authority.hashOperation(roleMember, this.managed.address, calldata); + const fn = this.managed.interface.getFunction(this.selector); + const calldata = this.managed.interface.encodeFunctionData(fn, []); + const opId = await this.authority.hashOperation(this.roleMember, this.managed, calldata); - await expectRevertCustomError(this.managed.methods[method]({ from: roleMember }), 'AccessManagerNotScheduled', [ - opId, - ]); + await expect(this.managed.connect(this.roleMember)[this.selector]()) + .to.be.revertedWithCustomError(this.authority, 'AccessManagerNotScheduled') + .withArgs(opId); }); it('succeeds if the operation is scheduled', async function () { // Arguments const delay = time.duration.hours(12); - const calldata = this.managed.contract.methods[method]().encodeABI(); + const fn = this.managed.interface.getFunction(this.selector); + const calldata = this.managed.interface.encodeFunctionData(fn, []); // Schedule - const timestamp = await time.latest(); - const scheduledAt = timestamp.addn(1); - const when = scheduledAt.add(delay); - await setNextBlockTimestamp(scheduledAt); - await this.authority.schedule(this.managed.address, calldata, when, { - from: roleMember, - }); + const timestamp = await time.clock.timestamp(); + const scheduledAt = timestamp + 1n; + const when = scheduledAt + delay; + await time.forward.timestamp(scheduledAt, false); + await this.authority.connect(this.roleMember).schedule(this.managed, calldata, when); // Set execution date - await setNextBlockTimestamp(when); + await time.forward.timestamp(when, false); // Shouldn't revert - await this.managed.methods[method]({ from: roleMember }); + await this.managed.connect(this.roleMember)[this.selector](); }); }); }); describe('setAuthority', function () { - beforeEach(async function () { - this.newAuthority = await AccessManager.new(admin); - }); - it('reverts if the caller is not the authority', async function () { - await expectRevertCustomError(this.managed.setAuthority(other, { from: other }), 'AccessManagedUnauthorized', [ - other, - ]); + await expect(this.managed.connect(this.other).setAuthority(this.other)) + .to.be.revertedWithCustomError(this.managed, 'AccessManagedUnauthorized') + .withArgs(this.other.address); }); it('reverts if the new authority is not a valid authority', async function () { - await impersonate(this.authority.address); - await expectRevertCustomError( - this.managed.setAuthority(other, { from: this.authority.address }), - 'AccessManagedInvalidAuthority', - [other], - ); + await expect(this.managed.connect(this.authorityAsSigner).setAuthority(this.other)) + .to.be.revertedWithCustomError(this.managed, 'AccessManagedInvalidAuthority') + .withArgs(this.other.address); }); it('sets authority and emits AuthorityUpdated event', async function () { - await impersonate(this.authority.address); - const { receipt } = await this.managed.setAuthority(this.newAuthority.address, { from: this.authority.address }); - await expectEvent(receipt, 'AuthorityUpdated', { - authority: this.newAuthority.address, - }); - expect(await this.managed.authority()).to.eq(this.newAuthority.address); + await expect(this.managed.connect(this.authorityAsSigner).setAuthority(this.anotherAuthority)) + .to.emit(this.managed, 'AuthorityUpdated') + .withArgs(this.anotherAuthority.target); + + expect(await this.managed.authority()).to.equal(this.anotherAuthority.target); }); }); describe('isConsumingScheduledOp', function () { beforeEach(async function () { - this.authority = await AuthoritiyObserveIsConsuming.new(); - this.managed = await AccessManaged.new(this.authority.address); + await this.managed.connect(this.authorityAsSigner).setAuthority(this.authorityObserveIsConsuming); }); it('returns bytes4(0) when not consuming operation', async function () { - expect(await this.managed.isConsumingScheduledOp()).to.eq('0x00000000'); + expect(await this.managed.isConsumingScheduledOp()).to.equal('0x00000000'); }); it('returns isConsumingScheduledOp selector when consuming operation', async function () { - const receipt = await this.managed.fnRestricted({ from: other }); - await expectEvent.inTransaction(receipt.tx, this.authority, 'ConsumeScheduledOpCalled', { - caller: other, - data: this.managed.contract.methods.fnRestricted().encodeABI(), - isConsuming: selector('isConsumingScheduledOp()'), - }); + const isConsumingScheduledOp = this.managed.interface.getFunction('isConsumingScheduledOp()'); + const fnRestricted = this.managed.fnRestricted.getFragment(); + await expect(this.managed.connect(this.other).fnRestricted()) + .to.emit(this.authorityObserveIsConsuming, 'ConsumeScheduledOpCalled') + .withArgs( + this.other.address, + this.managed.interface.encodeFunctionData(fnRestricted, []), + isConsumingScheduledOp.selector, + ); }); }); }); diff --git a/test/access/manager/AccessManager.behavior.js b/test/access/manager/AccessManager.behavior.js index e5c7a3ca7da..eb26b9a48ce 100644 --- a/test/access/manager/AccessManager.behavior.js +++ b/test/access/manager/AccessManager.behavior.js @@ -1,5 +1,3 @@ -const { mine } = require('@nomicfoundation/hardhat-network-helpers'); -const { expectRevertCustomError } = require('../../helpers/customError'); const { LIKE_COMMON_IS_EXECUTING, LIKE_COMMON_GET_ACCESS, @@ -18,13 +16,9 @@ const { */ function shouldBehaveLikeDelayedAdminOperation() { const getAccessPath = LIKE_COMMON_GET_ACCESS; - getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay = function () { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); - testAsDelayedOperation(); - }; + testAsDelayedOperation.mineDelay = true; + getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay = + testAsDelayedOperation; getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = function () { beforeEach('set execution delay', async function () { this.scheduleIn = this.executionDelay; // For testAsDelayedOperation @@ -42,14 +36,12 @@ function shouldBehaveLikeDelayedAdminOperation() { testAsHasRole({ publicRoleIsRequired() { it('reverts as AccessManagerUnauthorizedAccount', async function () { - await expectRevertCustomError( - web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), - 'AccessManagerUnauthorizedAccount', - [ - this.caller, + await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata })) + .to.be.revertedWithCustomError(this.target, 'AccessManagerUnauthorizedAccount') + .withArgs( + this.caller.address, this.roles.ADMIN.id, // Although PUBLIC is required, target function role doesn't apply to admin ops - ], - ); + ); }); }, specificRoleIsRequired: getAccessPath, @@ -63,19 +55,20 @@ function shouldBehaveLikeDelayedAdminOperation() { */ function shouldBehaveLikeNotDelayedAdminOperation() { const getAccessPath = LIKE_COMMON_GET_ACCESS; - getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay = function () { - beforeEach('set execution delay', async function () { - await mine(); - this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation - }); - testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE); - }; - getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = function () { - beforeEach('set execution delay', async function () { - this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation - }); - testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE); - }; + + function testScheduleOperation(mineDelay) { + return function self() { + self.mineDelay = mineDelay; + beforeEach('set execution delay', async function () { + this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation + }); + testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE); + }; + } + + getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay = + testScheduleOperation(true); + getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = testScheduleOperation(false); beforeEach('set target as manager', function () { this.target = this.manager; @@ -87,11 +80,12 @@ function shouldBehaveLikeNotDelayedAdminOperation() { testAsHasRole({ publicRoleIsRequired() { it('reverts as AccessManagerUnauthorizedAccount', async function () { - await expectRevertCustomError( - web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), - 'AccessManagerUnauthorizedAccount', - [this.caller, this.roles.ADMIN.id], // Although PUBLIC_ROLE is required, admin ops are not subject to target function roles - ); + await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata })) + .to.be.revertedWithCustomError(this.target, 'AccessManagerUnauthorizedAccount') + .withArgs( + this.caller.address, + this.roles.ADMIN.id, // Although PUBLIC_ROLE is required, admin ops are not subject to target function roles + ); }); }, specificRoleIsRequired: getAccessPath, @@ -105,19 +99,17 @@ function shouldBehaveLikeNotDelayedAdminOperation() { */ function shouldBehaveLikeRoleAdminOperation(roleAdmin) { const getAccessPath = LIKE_COMMON_GET_ACCESS; - getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay = function () { - beforeEach('set operation delay', async function () { - await mine(); - this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation - }); - testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE); - }; - getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = function () { + + function afterGrantDelay() { + afterGrantDelay.mineDelay = true; beforeEach('set execution delay', async function () { this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation }); testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE); - }; + } + + getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay = afterGrantDelay; + getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = afterGrantDelay; beforeEach('set target as manager', function () { this.target = this.manager; @@ -129,11 +121,9 @@ function shouldBehaveLikeRoleAdminOperation(roleAdmin) { testAsHasRole({ publicRoleIsRequired() { it('reverts as AccessManagerUnauthorizedAccount', async function () { - await expectRevertCustomError( - web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), - 'AccessManagerUnauthorizedAccount', - [this.caller, roleAdmin], // Role admin ops require the role's admin - ); + await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata })) + .to.be.revertedWithCustomError(this.target, 'AccessManagerUnauthorizedAccount') + .withArgs(this.caller.address, roleAdmin); }); }, specificRoleIsRequired: getAccessPath, @@ -150,11 +140,9 @@ function shouldBehaveLikeRoleAdminOperation(roleAdmin) { function shouldBehaveLikeAManagedRestrictedOperation() { function revertUnauthorized() { it('reverts as AccessManagedUnauthorized', async function () { - await expectRevertCustomError( - web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), - 'AccessManagedUnauthorized', - [this.caller], - ); + await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata })) + .to.be.revertedWithCustomError(this.target, 'AccessManagedUnauthorized') + .withArgs(this.caller.address); }); } @@ -166,20 +154,19 @@ function shouldBehaveLikeAManagedRestrictedOperation() { revertUnauthorized; getAccessPath.requiredRoleIsNotGranted = revertUnauthorized; - getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay = function () { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation - }); - testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE); - }; - getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = function () { - beforeEach('consume previously set grant delay', async function () { - this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation - }); - testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE); - }; + function testScheduleOperation(mineDelay) { + return function self() { + self.mineDelay = mineDelay; + beforeEach('sets execution delay', async function () { + this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation + }); + testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE); + }; + } + + getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay = + testScheduleOperation(true); + getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = testScheduleOperation(false); const isExecutingPath = LIKE_COMMON_IS_EXECUTING; isExecutingPath.notExecuting = revertUnauthorized; @@ -191,11 +178,11 @@ function shouldBehaveLikeAManagedRestrictedOperation() { callerIsNotTheManager: { publicRoleIsRequired() { it('succeeds called directly', async function () { - await web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }); + await this.caller.sendTransaction({ to: this.target, data: this.calldata }); }); it('succeeds via execute', async function () { - await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); + await this.manager.connect(this.caller).execute(this.target, this.calldata); }); }, specificRoleIsRequired: getAccessPath, diff --git a/test/access/manager/AccessManager.predicate.js b/test/access/manager/AccessManager.predicate.js index becbcb53413..048437e035f 100644 --- a/test/access/manager/AccessManager.predicate.js +++ b/test/access/manager/AccessManager.predicate.js @@ -1,27 +1,22 @@ const { setStorageAt } = require('@nomicfoundation/hardhat-network-helpers'); -const { - time: { setNextBlockTimestamp }, -} = require('@nomicfoundation/hardhat-network-helpers'); -const { time } = require('@openzeppelin/test-helpers'); -const { EXECUTION_ID_STORAGE_SLOT, EXPIRATION, scheduleOperation } = require('../../helpers/access-manager'); +const { EXECUTION_ID_STORAGE_SLOT, EXPIRATION, prepareOperation } = require('../../helpers/access-manager'); const { impersonate } = require('../../helpers/account'); -const { expectRevertCustomError } = require('../../helpers/customError'); +const { bigint: time } = require('../../helpers/time'); +const { ethers } = require('hardhat'); // ============ COMMON PREDICATES ============ const LIKE_COMMON_IS_EXECUTING = { executing() { it('succeeds', async function () { - await web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }); + await this.caller.sendTransaction({ to: this.target, data: this.calldata }); }); }, notExecuting() { it('reverts as AccessManagerUnauthorizedAccount', async function () { - await expectRevertCustomError( - web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), - 'AccessManagerUnauthorizedAccount', - [this.caller, this.role.id], - ); + await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata })) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedAccount') + .withArgs(this.caller.address, this.role.id); }); }, }; @@ -32,11 +27,9 @@ const LIKE_COMMON_GET_ACCESS = { callerHasAnExecutionDelay: { beforeGrantDelay() { it('reverts as AccessManagerUnauthorizedAccount', async function () { - await expectRevertCustomError( - web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), - 'AccessManagerUnauthorizedAccount', - [this.caller, this.role.id], - ); + await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata })) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedAccount') + .withArgs(this.caller.address, this.role.id); }); }, afterGrantDelay: undefined, // Diverges if there's an operation delay or not @@ -44,20 +37,18 @@ const LIKE_COMMON_GET_ACCESS = { callerHasNoExecutionDelay: { beforeGrantDelay() { it('reverts as AccessManagerUnauthorizedAccount', async function () { - await expectRevertCustomError( - web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), - 'AccessManagerUnauthorizedAccount', - [this.caller, this.role.id], - ); + await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata })) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedAccount') + .withArgs(this.caller.address, this.role.id); }); }, afterGrantDelay() { it('succeeds called directly', async function () { - await web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }); + await this.caller.sendTransaction({ to: this.target, data: this.calldata }); }); it('succeeds via execute', async function () { - await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); + await this.manager.connect(this.caller).execute(this.target, this.calldata); }); }, }, @@ -66,22 +57,20 @@ const LIKE_COMMON_GET_ACCESS = { callerHasAnExecutionDelay: undefined, // Diverges if there's an operation to schedule or not callerHasNoExecutionDelay() { it('succeeds called directly', async function () { - await web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }); + await this.caller.sendTransaction({ to: this.target, data: this.calldata }); }); it('succeeds via execute', async function () { - await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); + await this.manager.connect(this.caller).execute(this.target, this.calldata); }); }, }, }, requiredRoleIsNotGranted() { it('reverts as AccessManagerUnauthorizedAccount', async function () { - await expectRevertCustomError( - web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), - 'AccessManagerUnauthorizedAccount', - [this.caller, this.role.id], - ); + await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata })) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedAccount') + .withArgs(this.caller.address, this.role.id); }); }, }; @@ -90,39 +79,33 @@ const LIKE_COMMON_SCHEDULABLE = { scheduled: { before() { it('reverts as AccessManagerNotReady', async function () { - await expectRevertCustomError( - web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), - 'AccessManagerNotReady', - [this.operationId], - ); + await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata })) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerNotReady') + .withArgs(this.operationId); }); }, after() { it('succeeds called directly', async function () { - await web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }); + await this.caller.sendTransaction({ to: this.target, data: this.calldata }); }); it('succeeds via execute', async function () { - await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); + await this.manager.connect(this.caller).execute(this.target, this.calldata); }); }, expired() { it('reverts as AccessManagerExpired', async function () { - await expectRevertCustomError( - web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), - 'AccessManagerExpired', - [this.operationId], - ); + await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata })) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerExpired') + .withArgs(this.operationId); }); }, }, notScheduled() { it('reverts as AccessManagerNotScheduled', async function () { - await expectRevertCustomError( - web3.eth.sendTransaction({ to: this.target.address, data: this.calldata, from: this.caller }), - 'AccessManagerNotScheduled', - [this.operationId], - ); + await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata })) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerNotScheduled') + .withArgs(this.operationId); }); }, }; @@ -135,7 +118,7 @@ const LIKE_COMMON_SCHEDULABLE = { function testAsClosable({ closed, open }) { describe('when the manager is closed', function () { beforeEach('close', async function () { - await this.manager.$_setTargetClosed(this.target.address, true); + await this.manager.$_setTargetClosed(this.target, true); }); closed(); @@ -143,7 +126,7 @@ function testAsClosable({ closed, open }) { describe('when the manager is open', function () { beforeEach('open', async function () { - await this.manager.$_setTargetClosed(this.target.address, false); + await this.manager.$_setTargetClosed(this.target, false); }); open(); @@ -157,13 +140,13 @@ function testAsClosable({ closed, open }) { */ function testAsDelay(type, { before, after }) { beforeEach('define timestamp when delay takes effect', async function () { - const timestamp = await time.latest(); - this.delayEffect = timestamp.add(this.delay); + const timestamp = await time.clock.timestamp(); + this.delayEffect = timestamp + this.delay; }); describe(`when ${type} delay has not taken effect yet`, function () { beforeEach(`set next block timestamp before ${type} takes effect`, async function () { - await setNextBlockTimestamp(this.delayEffect.subn(1)); + await time.forward.timestamp(this.delayEffect - 1n, !!before.mineDelay); }); before(); @@ -171,7 +154,7 @@ function testAsDelay(type, { before, after }) { describe(`when ${type} delay has taken effect`, function () { beforeEach(`set next block timestamp when ${type} takes effect`, async function () { - await setNextBlockTimestamp(this.delayEffect); + await time.forward.timestamp(this.delayEffect, !!after.mineDelay); }); after(); @@ -186,21 +169,25 @@ function testAsDelay(type, { before, after }) { function testAsSchedulableOperation({ scheduled: { before, after, expired }, notScheduled }) { describe('when operation is scheduled', function () { beforeEach('schedule operation', async function () { - await impersonate(this.caller); // May be a contract - const { operationId } = await scheduleOperation(this.manager, { + if (this.caller.target) { + await impersonate(this.caller.target); + this.caller = await ethers.getSigner(this.caller.target); + } + const { operationId, schedule } = await prepareOperation(this.manager, { caller: this.caller, - target: this.target.address, + target: this.target, calldata: this.calldata, delay: this.scheduleIn, }); + await schedule(); this.operationId = operationId; }); describe('when operation is not ready for execution', function () { beforeEach('set next block time before operation is ready', async function () { - this.scheduledAt = await time.latest(); + this.scheduledAt = await time.clock.timestamp(); const schedule = await this.manager.getSchedule(this.operationId); - await setNextBlockTimestamp(schedule.subn(1)); + await time.forward.timestamp(schedule - 1n, !!before.mineDelay); }); before(); @@ -208,9 +195,9 @@ function testAsSchedulableOperation({ scheduled: { before, after, expired }, not describe('when operation is ready for execution', function () { beforeEach('set next block time when operation is ready for execution', async function () { - this.scheduledAt = await time.latest(); + this.scheduledAt = await time.clock.timestamp(); const schedule = await this.manager.getSchedule(this.operationId); - await setNextBlockTimestamp(schedule); + await time.forward.timestamp(schedule, !!after.mineDelay); }); after(); @@ -218,9 +205,9 @@ function testAsSchedulableOperation({ scheduled: { before, after, expired }, not describe('when operation has expired', function () { beforeEach('set next block time when operation expired', async function () { - this.scheduledAt = await time.latest(); + this.scheduledAt = await time.clock.timestamp(); const schedule = await this.manager.getSchedule(this.operationId); - await setNextBlockTimestamp(schedule.add(EXPIRATION)); + await time.forward.timestamp(schedule + EXPIRATION, !!expired.mineDelay); }); expired(); @@ -229,10 +216,10 @@ function testAsSchedulableOperation({ scheduled: { before, after, expired }, not describe('when operation is not scheduled', function () { beforeEach('set expected operationId', async function () { - this.operationId = await this.manager.hashOperation(this.caller, this.target.address, this.calldata); + this.operationId = await this.manager.hashOperation(this.caller, this.target, this.calldata); // Assert operation is not scheduled - expect(await this.manager.getSchedule(this.operationId)).to.be.bignumber.equal(web3.utils.toBN(0)); + expect(await this.manager.getSchedule(this.operationId)).to.equal(0n); }); notScheduled(); @@ -245,16 +232,22 @@ function testAsSchedulableOperation({ scheduled: { before, after, expired }, not function testAsRestrictedOperation({ callerIsTheManager: { executing, notExecuting }, callerIsNotTheManager }) { describe('when the call comes from the manager (msg.sender == manager)', function () { beforeEach('define caller as manager', async function () { - this.caller = this.manager.address; - await impersonate(this.caller); + this.caller = this.manager; + if (this.caller.target) { + await impersonate(this.caller.target); + this.caller = await ethers.getSigner(this.caller.target); + } }); describe('when _executionId is in storage for target and selector', function () { beforeEach('set _executionId flag from calldata and target', async function () { - const executionId = web3.utils.keccak256( - web3.eth.abi.encodeParameters(['address', 'bytes4'], [this.target.address, this.calldata.substring(0, 10)]), + const executionId = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'bytes4'], + [this.target.target, this.calldata.substring(0, 10)], + ), ); - await setStorageAt(this.manager.address, EXECUTION_ID_STORAGE_SLOT, executionId); + await setStorageAt(this.manager.target, EXECUTION_ID_STORAGE_SLOT, executionId); }); executing(); @@ -279,8 +272,8 @@ function testAsDelayedOperation() { describe('with operation delay', function () { describe('when operation delay is greater than execution delay', function () { beforeEach('set operation delay', async function () { - this.operationDelay = this.executionDelay.add(time.duration.hours(1)); - await this.manager.$_setTargetAdminDelay(this.target.address, this.operationDelay); + this.operationDelay = this.executionDelay + time.duration.hours(1); + await this.manager.$_setTargetAdminDelay(this.target, this.operationDelay); this.scheduleIn = this.operationDelay; // For testAsSchedulableOperation }); @@ -289,8 +282,8 @@ function testAsDelayedOperation() { describe('when operation delay is shorter than execution delay', function () { beforeEach('set operation delay', async function () { - this.operationDelay = this.executionDelay.sub(time.duration.hours(1)); - await this.manager.$_setTargetAdminDelay(this.target.address, this.operationDelay); + this.operationDelay = this.executionDelay - time.duration.hours(1); + await this.manager.$_setTargetAdminDelay(this.target, this.operationDelay); this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation }); @@ -300,8 +293,8 @@ function testAsDelayedOperation() { describe('without operation delay', function () { beforeEach('set operation delay', async function () { - this.operationDelay = web3.utils.toBN(0); - await this.manager.$_setTargetAdminDelay(this.target.address, this.operationDelay); + this.operationDelay = 0n; + await this.manager.$_setTargetAdminDelay(this.target, this.operationDelay); this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation }); @@ -344,9 +337,9 @@ function testAsHasRole({ publicRoleIsRequired, specificRoleIsRequired }) { describe('when the function requires the caller to be granted with the PUBLIC_ROLE', function () { beforeEach('set target function role as PUBLIC_ROLE', async function () { this.role = this.roles.PUBLIC; - await this.manager.$_setTargetFunctionRole(this.target.address, this.calldata.substring(0, 10), this.role.id, { - from: this.roles.ADMIN.members[0], - }); + await this.manager + .connect(this.roles.ADMIN.members[0]) + .$_setTargetFunctionRole(this.target, this.calldata.substring(0, 10), this.role.id); }); publicRoleIsRequired(); @@ -354,9 +347,9 @@ function testAsHasRole({ publicRoleIsRequired, specificRoleIsRequired }) { describe('when the function requires the caller to be granted with a role other than PUBLIC_ROLE', function () { beforeEach('set target function role as PUBLIC_ROLE', async function () { - await this.manager.$_setTargetFunctionRole(this.target.address, this.calldata.substring(0, 10), this.role.id, { - from: this.roles.ADMIN.members[0], - }); + await this.manager + .connect(this.roles.ADMIN.members[0]) + .$_setTargetFunctionRole(this.target, this.calldata.substring(0, 10), this.role.id); }); testAsGetAccess(specificRoleIsRequired); @@ -400,7 +393,7 @@ function testAsGetAccess({ describe('when caller has no execution delay', function () { beforeEach('set role and delay', async function () { - this.executionDelay = web3.utils.toBN(0); + this.executionDelay = 0n; await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay); }); @@ -410,7 +403,7 @@ function testAsGetAccess({ describe('when role granting is not delayed', function () { beforeEach('define delay', function () { - this.grantDelay = web3.utils.toBN(0); + this.grantDelay = 0n; }); describe('when caller has an execution delay', function () { @@ -424,7 +417,7 @@ function testAsGetAccess({ describe('when caller has no execution delay', function () { beforeEach('set role and delay', async function () { - this.executionDelay = web3.utils.toBN(0); + this.executionDelay = 0n; await this.manager.$_grantRole(this.role.id, this.caller, this.grantDelay, this.executionDelay); }); @@ -439,7 +432,7 @@ function testAsGetAccess({ // Although this is highly unlikely, we check for it here to avoid false positives. beforeEach('assert role is unset', async function () { const { since } = await this.manager.getAccess(this.role.id, this.caller); - expect(since).to.be.bignumber.equal(web3.utils.toBN(0)); + expect(since).to.equal(0n); }); requiredRoleIsNotGranted(); diff --git a/test/access/manager/AccessManager.test.js b/test/access/manager/AccessManager.test.js index 40c684505ba..b26a246af47 100644 --- a/test/access/manager/AccessManager.test.js +++ b/test/access/manager/AccessManager.test.js @@ -1,8 +1,5 @@ -const { web3 } = require('hardhat'); -const { constants, expectEvent, time, expectRevert } = require('@openzeppelin/test-helpers'); -const { expectRevertCustomError } = require('../../helpers/customError'); +const { ethers, expect } = require('hardhat'); const { selector } = require('../../helpers/methods'); -const { clockFromReceipt } = require('../../helpers/time'); const { buildBaseRoles, formatAccess, @@ -10,7 +7,7 @@ const { MINSETBACK, EXECUTION_ID_STORAGE_SLOT, CONSUMING_SCHEDULE_STORAGE_SLOT, - scheduleOperation, + prepareOperation, hashOperation, } = require('../../helpers/access-manager'); const { @@ -28,20 +25,68 @@ const { testAsHasRole, testAsGetAccess, } = require('./AccessManager.predicate'); -const { default: Wallet } = require('ethereumjs-wallet'); + const { - mine, - time: { setNextBlockTimestamp }, + time: { increase }, getStorageAt, + loadFixture, } = require('@nomicfoundation/hardhat-network-helpers'); const { MAX_UINT48 } = require('../../helpers/constants'); const { impersonate } = require('../../helpers/account'); +const { bigint: time } = require('../../helpers/time'); +const { ZeroAddress: ZERO_ADDRESS, Wallet, toBeHex, id } = require('ethers'); + +const { address: someAddress } = Wallet.createRandom(); + +async function fixture() { + const [admin, roleAdmin, roleGuardian, member, user, other] = await ethers.getSigners(); + + // Build roles + const roles = buildBaseRoles(); -const AccessManager = artifacts.require('$AccessManager'); -const AccessManagedTarget = artifacts.require('$AccessManagedTarget'); -const Ownable = artifacts.require('$Ownable'); + // Add members + roles.ADMIN.members = [admin]; + roles.SOME_ADMIN.members = [roleAdmin]; + roles.SOME_GUARDIAN.members = [roleGuardian]; + roles.SOME.members = [member]; + roles.PUBLIC.members = [admin, roleAdmin, roleGuardian, member, user, other]; -const someAddress = Wallet.generate().getChecksumAddressString(); + const manager = await ethers.deployContract('$AccessManager', [admin]); + const target = await ethers.deployContract('$AccessManagedTarget', [manager]); + + for (const { id: roleId, admin, guardian, members } of Object.values(roles)) { + if (roleId === roles.PUBLIC.id) continue; // Every address belong to public and is locked + if (roleId === roles.ADMIN.id) continue; // Admin set during construction and is locked + + // Set admin role avoiding default + if (admin.id !== roles.ADMIN.id) { + await manager.$_setRoleAdmin(roleId, admin.id); + } + + // Set guardian role avoiding default + if (guardian.id !== roles.ADMIN.id) { + await manager.$_setRoleGuardian(roleId, guardian.id); + } + + // Grant role to members + for (const member of members) { + await manager.$_grantRole(roleId, member, 0, 0); + } + } + + return { + // TODO: Check if all signers are actually used + admin, + roleAdmin, + roleGuardian, + member, + user, + other, + roles, + manager, + target, + }; +} // This test suite is made using the following tools: // @@ -57,59 +102,29 @@ const someAddress = Wallet.generate().getChecksumAddressString(); // The predicates can be identified by the `testAs*` prefix while the behaviors // are prefixed with `shouldBehave*`. The common assertions for predicates are // defined as constants. -contract('AccessManager', function (accounts) { - const [admin, manager, guardian, member, user, other] = accounts; +contract('AccessManager', function () { + // const [admin, manager, guardian, member, user, other] = accounts; beforeEach(async function () { - this.roles = buildBaseRoles(); - - // Add members - this.roles.ADMIN.members = [admin]; - this.roles.SOME_ADMIN.members = [manager]; - this.roles.SOME_GUARDIAN.members = [guardian]; - this.roles.SOME.members = [member]; - this.roles.PUBLIC.members = [admin, manager, guardian, member, user, other]; - - this.manager = await AccessManager.new(admin); - this.target = await AccessManagedTarget.new(this.manager.address); - - for (const { id: roleId, admin, guardian, members } of Object.values(this.roles)) { - if (roleId === this.roles.PUBLIC.id) continue; // Every address belong to public and is locked - if (roleId === this.roles.ADMIN.id) continue; // Admin set during construction and is locked - - // Set admin role avoiding default - if (admin.id !== this.roles.ADMIN.id) { - await this.manager.$_setRoleAdmin(roleId, admin.id); - } - - // Set guardian role avoiding default - if (guardian.id !== this.roles.ADMIN.id) { - await this.manager.$_setRoleGuardian(roleId, guardian.id); - } - - // Grant role to members - for (const member of members) { - await this.manager.$_grantRole(roleId, member, 0, 0); - } - } + Object.assign(this, await loadFixture(fixture)); }); describe('during construction', function () { it('grants admin role to initialAdmin', async function () { - const manager = await AccessManager.new(other); - expect(await manager.hasRole(this.roles.ADMIN.id, other).then(formatAccess)).to.be.deep.equal([true, '0']); + const manager = await ethers.deployContract('$AccessManager', [this.other]); + expect(await manager.hasRole(this.roles.ADMIN.id, this.other).then(formatAccess)).to.be.deep.equal([true, '0']); }); it('rejects zero address for initialAdmin', async function () { - await expectRevertCustomError(AccessManager.new(constants.ZERO_ADDRESS), 'AccessManagerInvalidInitialAdmin', [ - constants.ZERO_ADDRESS, - ]); + await expect(ethers.deployContract('$AccessManager', [ethers.ZeroAddress])) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerInvalidInitialAdmin') + .withArgs(ZERO_ADDRESS); }); it('initializes setup roles correctly', async function () { for (const { id: roleId, admin, guardian, members } of Object.values(this.roles)) { - expect(await this.manager.getRoleAdmin(roleId)).to.be.bignumber.equal(admin.id); - expect(await this.manager.getRoleGuardian(roleId)).to.be.bignumber.equal(guardian.id); + expect(await this.manager.getRoleAdmin(roleId)).to.equal(admin.id); + expect(await this.manager.getRoleGuardian(roleId)).to.equal(guardian.id); for (const user of this.roles.PUBLIC.members) { expect(await this.manager.hasRole(roleId, user).then(formatAccess)).to.be.deep.equal([ @@ -125,7 +140,7 @@ contract('AccessManager', function (accounts) { describe('#canCall', function () { beforeEach('set calldata', function () { this.calldata = '0x12345678'; - this.role = { id: web3.utils.toBN(379204) }; + this.role = { id: 379204n }; }); testAsCanCall({ @@ -133,11 +148,11 @@ contract('AccessManager', function (accounts) { it('should return false and no delay', async function () { const { immediate, delay } = await this.manager.canCall( someAddress, - this.target.address, + this.target, this.calldata.substring(0, 10), ); - expect(immediate).to.be.equal(false); - expect(delay).to.be.bignumber.equal('0'); + expect(immediate).to.be.false; + expect(delay).to.equal(0n); }); }, open: { @@ -146,22 +161,22 @@ contract('AccessManager', function (accounts) { it('should return true and no delay', async function () { const { immediate, delay } = await this.manager.canCall( this.caller, - this.target.address, + this.target, this.calldata.substring(0, 10), ); - expect(immediate).to.be.equal(true); - expect(delay).to.be.bignumber.equal('0'); + expect(immediate).to.be.true; + expect(delay).to.equal(0n); }); }, notExecuting() { it('should return false and no delay', async function () { const { immediate, delay } = await this.manager.canCall( this.caller, - this.target.address, + this.target, this.calldata.substring(0, 10), ); - expect(immediate).to.be.equal(false); - expect(delay).to.be.bignumber.equal('0'); + expect(immediate).to.be.false; + expect(delay).to.equal(0n); }); }, }, @@ -170,87 +185,76 @@ contract('AccessManager', function (accounts) { it('should return true and no delay', async function () { const { immediate, delay } = await this.manager.canCall( this.caller, - this.target.address, + this.target, this.calldata.substring(0, 10), ); - expect(immediate).to.be.equal(true); - expect(delay).to.be.bignumber.equal('0'); + expect(immediate).to.be.true; + expect(delay).to.equal(0n); }); }, specificRoleIsRequired: { requiredRoleIsGranted: { roleGrantingIsDelayed: { callerHasAnExecutionDelay: { - beforeGrantDelay() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + beforeGrantDelay: function self() { + self.mineDelay = true; it('should return false and no execution delay', async function () { const { immediate, delay } = await this.manager.canCall( this.caller, - this.target.address, + this.target, this.calldata.substring(0, 10), ); - expect(immediate).to.be.equal(false); - expect(delay).to.be.bignumber.equal('0'); + expect(immediate).to.be.false; + expect(delay).to.equal(0n); }); }, - afterGrantDelay() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); + afterGrantDelay: function self() { + self.mineDelay = true; + + beforeEach('sets execution delay', function () { this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation }); testAsSchedulableOperation({ scheduled: { - before() { - beforeEach('consume previously set delay', async function () { - // Consume previously set delay - await mine(); - }); + before: function self() { + self.mineDelay = true; it('should return false and execution delay', async function () { const { immediate, delay } = await this.manager.canCall( this.caller, - this.target.address, + this.target, this.calldata.substring(0, 10), ); - expect(immediate).to.be.equal(false); - expect(delay).to.be.bignumber.equal(this.executionDelay); + expect(immediate).to.be.false; + expect(delay).to.equal(this.executionDelay); }); }, - after() { - beforeEach('consume previously set delay', async function () { - // Consume previously set delay - await mine(); - }); + after: function self() { + self.mineDelay = true; it('should return false and execution delay', async function () { const { immediate, delay } = await this.manager.canCall( this.caller, - this.target.address, + this.target, this.calldata.substring(0, 10), ); - expect(immediate).to.be.equal(false); - expect(delay).to.be.bignumber.equal(this.executionDelay); + expect(immediate).to.be.false; + expect(delay).to.equal(this.executionDelay); }); }, - expired() { - beforeEach('consume previously set delay', async function () { - // Consume previously set delay - await mine(); - }); + expired: function self() { + self.mineDelay = true; + it('should return false and execution delay', async function () { const { immediate, delay } = await this.manager.canCall( this.caller, - this.target.address, + this.target, this.calldata.substring(0, 10), ); - expect(immediate).to.be.equal(false); - expect(delay).to.be.bignumber.equal(this.executionDelay); + expect(immediate).to.be.false; + expect(delay).to.equal(this.executionDelay); }); }, }, @@ -258,47 +262,41 @@ contract('AccessManager', function (accounts) { it('should return false and execution delay', async function () { const { immediate, delay } = await this.manager.canCall( this.caller, - this.target.address, + this.target, this.calldata.substring(0, 10), ); - expect(immediate).to.be.equal(false); - expect(delay).to.be.bignumber.equal(this.executionDelay); + expect(immediate).to.be.false; + expect(delay).to.equal(this.executionDelay); }); }, }); }, }, callerHasNoExecutionDelay: { - beforeGrantDelay() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + beforeGrantDelay: function self() { + self.mineDelay = true; it('should return false and no execution delay', async function () { const { immediate, delay } = await this.manager.canCall( this.caller, - this.target.address, + this.target, this.calldata.substring(0, 10), ); - expect(immediate).to.be.equal(false); - expect(delay).to.be.bignumber.equal('0'); + expect(immediate).to.be.false; + expect(delay).to.equal(0n); }); }, - afterGrantDelay() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + afterGrantDelay: function self() { + self.mineDelay = true; it('should return true and no execution delay', async function () { const { immediate, delay } = await this.manager.canCall( this.caller, - this.target.address, + this.target, this.calldata.substring(0, 10), ); - expect(immediate).to.be.equal(true); - expect(delay).to.be.bignumber.equal('0'); + expect(immediate).to.be.true; + expect(delay).to.equal(0n); }); }, }, @@ -308,22 +306,22 @@ contract('AccessManager', function (accounts) { it('should return false and execution delay', async function () { const { immediate, delay } = await this.manager.canCall( this.caller, - this.target.address, + this.target, this.calldata.substring(0, 10), ); - expect(immediate).to.be.equal(false); - expect(delay).to.be.bignumber.equal(this.executionDelay); + expect(immediate).to.be.false; + expect(delay).to.equal(this.executionDelay); }); }, callerHasNoExecutionDelay() { it('should return true and no execution delay', async function () { const { immediate, delay } = await this.manager.canCall( this.caller, - this.target.address, + this.target, this.calldata.substring(0, 10), ); - expect(immediate).to.be.equal(true); - expect(delay).to.be.bignumber.equal('0'); + expect(immediate).to.be.true; + expect(delay).to.equal(0n); }); }, }, @@ -332,11 +330,11 @@ contract('AccessManager', function (accounts) { it('should return false and no execution delay', async function () { const { immediate, delay } = await this.manager.canCall( this.caller, - this.target.address, + this.target, this.calldata.substring(0, 10), ); - expect(immediate).to.be.equal(false); - expect(delay).to.be.bignumber.equal('0'); + expect(immediate).to.be.false; + expect(delay).to.equal(0n); }); }, }, @@ -347,13 +345,13 @@ contract('AccessManager', function (accounts) { describe('#expiration', function () { it('has a 7 days default expiration', async function () { - expect(await this.manager.expiration()).to.be.bignumber.equal(EXPIRATION); + expect(await this.manager.expiration()).to.equal(EXPIRATION); }); }); describe('#minSetback', function () { it('has a 5 days default minimum setback', async function () { - expect(await this.manager.minSetback()).to.be.bignumber.equal(MINSETBACK); + expect(await this.manager.minSetback()).to.equal(MINSETBACK); }); }); @@ -361,12 +359,12 @@ contract('AccessManager', function (accounts) { testAsClosable({ closed() { it('returns true', async function () { - expect(await this.manager.isTargetClosed(this.target.address)).to.be.equal(true); + expect(await this.manager.isTargetClosed(this.target)).to.be.true; }); }, open() { it('returns false', async function () { - expect(await this.manager.isTargetClosed(this.target.address)).to.be.equal(false); + expect(await this.manager.isTargetClosed(this.target)).to.be.false; }); }, }); @@ -376,94 +374,84 @@ contract('AccessManager', function (accounts) { const methodSelector = selector('something(address,bytes)'); it('returns the target function role', async function () { - const roleId = web3.utils.toBN(21498); - await this.manager.$_setTargetFunctionRole(this.target.address, methodSelector, roleId); + const roleId = 21498n; + await this.manager.$_setTargetFunctionRole(this.target, methodSelector, roleId); - expect(await this.manager.getTargetFunctionRole(this.target.address, methodSelector)).to.be.bignumber.equal( - roleId, - ); + expect(await this.manager.getTargetFunctionRole(this.target, methodSelector)).to.equal(roleId); }); it('returns the ADMIN role if not set', async function () { - expect(await this.manager.getTargetFunctionRole(this.target.address, methodSelector)).to.be.bignumber.equal( - this.roles.ADMIN.id, - ); + expect(await this.manager.getTargetFunctionRole(this.target, methodSelector)).to.equal(this.roles.ADMIN.id); }); }); describe('#getTargetAdminDelay', function () { describe('when the target admin delay is setup', function () { beforeEach('set target admin delay', async function () { - this.oldDelay = await this.manager.getTargetAdminDelay(this.target.address); + this.oldDelay = await this.manager.getTargetAdminDelay(this.target); this.newDelay = time.duration.days(10); - await this.manager.$_setTargetAdminDelay(this.target.address, this.newDelay); + await this.manager.$_setTargetAdminDelay(this.target, this.newDelay); this.delay = MINSETBACK; // For testAsDelay }); testAsDelay('effect', { - before() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + before: function self() { + self.mineDelay = true; it('returns the old target admin delay', async function () { - expect(await this.manager.getTargetAdminDelay(this.target.address)).to.be.bignumber.equal(this.oldDelay); + expect(await this.manager.getTargetAdminDelay(this.target)).to.equal(this.oldDelay); }); }, - after() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + after: function self() { + self.mineDelay = true; it('returns the new target admin delay', async function () { - expect(await this.manager.getTargetAdminDelay(this.target.address)).to.be.bignumber.equal(this.newDelay); + expect(await this.manager.getTargetAdminDelay(this.target)).to.equal(this.newDelay); }); }, }); }); it('returns the 0 if not set', async function () { - expect(await this.manager.getTargetAdminDelay(this.target.address)).to.be.bignumber.equal('0'); + expect(await this.manager.getTargetAdminDelay(this.target)).to.equal(0n); }); }); describe('#getRoleAdmin', function () { - const roleId = web3.utils.toBN(5234907); + const roleId = 5234907n; it('returns the role admin', async function () { - const adminId = web3.utils.toBN(789433); + const adminId = 789433n; await this.manager.$_setRoleAdmin(roleId, adminId); - expect(await this.manager.getRoleAdmin(roleId)).to.be.bignumber.equal(adminId); + expect(await this.manager.getRoleAdmin(roleId)).to.equal(adminId); }); it('returns the ADMIN role if not set', async function () { - expect(await this.manager.getRoleAdmin(roleId)).to.be.bignumber.equal(this.roles.ADMIN.id); + expect(await this.manager.getRoleAdmin(roleId)).to.equal(this.roles.ADMIN.id); }); }); describe('#getRoleGuardian', function () { - const roleId = web3.utils.toBN(5234907); + const roleId = 5234907n; it('returns the role guardian', async function () { - const guardianId = web3.utils.toBN(789433); + const guardianId = 789433n; await this.manager.$_setRoleGuardian(roleId, guardianId); - expect(await this.manager.getRoleGuardian(roleId)).to.be.bignumber.equal(guardianId); + expect(await this.manager.getRoleGuardian(roleId)).to.equal(guardianId); }); it('returns the ADMIN role if not set', async function () { - expect(await this.manager.getRoleGuardian(roleId)).to.be.bignumber.equal(this.roles.ADMIN.id); + expect(await this.manager.getRoleGuardian(roleId)).to.equal(this.roles.ADMIN.id); }); }); describe('#getRoleGrantDelay', function () { - const roleId = web3.utils.toBN(9248439); + const roleId = 9248439n; describe('when the grant admin delay is setup', function () { beforeEach('set grant admin delay', async function () { @@ -475,113 +463,95 @@ contract('AccessManager', function (accounts) { }); testAsDelay('grant', { - before() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + before: function self() { + self.mineDelay = true; it('returns the old role grant delay', async function () { - expect(await this.manager.getRoleGrantDelay(roleId)).to.be.bignumber.equal(this.oldDelay); + expect(await this.manager.getRoleGrantDelay(roleId)).to.equal(this.oldDelay); }); }, - after() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + after: function self() { + self.mineDelay = true; it('returns the new role grant delay', async function () { - expect(await this.manager.getRoleGrantDelay(roleId)).to.be.bignumber.equal(this.newDelay); + expect(await this.manager.getRoleGrantDelay(roleId)).to.equal(this.newDelay); }); }, }); }); it('returns 0 if delay is not set', async function () { - expect(await this.manager.getTargetAdminDelay(this.target.address)).to.be.bignumber.equal('0'); + expect(await this.manager.getTargetAdminDelay(this.target)).to.equal(0n); }); }); describe('#getAccess', function () { beforeEach('set role', function () { - this.role = { id: web3.utils.toBN(9452) }; - this.caller = user; + this.role = { id: 9452n }; + this.caller = this.user; }); testAsGetAccess({ requiredRoleIsGranted: { roleGrantingIsDelayed: { callerHasAnExecutionDelay: { - beforeGrantDelay() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + beforeGrantDelay: function self() { + self.mineDelay = true; it('role is not in effect and execution delay is set', async function () { const access = await this.manager.getAccess(this.role.id, this.caller); - expect(access[0]).to.be.bignumber.equal(this.delayEffect); // inEffectSince - expect(access[1]).to.be.bignumber.equal(this.executionDelay); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + expect(access[0]).to.equal(this.delayEffect); // inEffectSince + expect(access[1]).to.equal(this.executionDelay); // currentDelay + expect(access[2]).to.equal(0n); // pendingDelay + expect(access[3]).to.equal(0n); // pendingDelayEffect // Not in effect yet - expect(await time.latest()).to.be.bignumber.lt(access[0]); + expect(await time.clock.timestamp()).to.lt(access[0]); }); }, - afterGrantDelay() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + afterGrantDelay: function self() { + self.mineDelay = true; it('access has role in effect and execution delay is set', async function () { const access = await this.manager.getAccess(this.role.id, this.caller); - expect(access[0]).to.be.bignumber.equal(this.delayEffect); // inEffectSince - expect(access[1]).to.be.bignumber.equal(this.executionDelay); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + expect(access[0]).to.equal(this.delayEffect); // inEffectSince + expect(access[1]).to.equal(this.executionDelay); // currentDelay + expect(access[2]).to.equal(0n); // pendingDelay + expect(access[3]).to.equal(0n); // pendingDelayEffect // Already in effect - expect(await time.latest()).to.be.bignumber.equal(access[0]); + expect(await time.clock.timestamp()).to.equal(access[0]); }); }, }, callerHasNoExecutionDelay: { - beforeGrantDelay() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + beforeGrantDelay: function self() { + self.mineDelay = true; it('access has role not in effect without execution delay', async function () { const access = await this.manager.getAccess(this.role.id, this.caller); - expect(access[0]).to.be.bignumber.equal(this.delayEffect); // inEffectSince - expect(access[1]).to.be.bignumber.equal('0'); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + expect(access[0]).to.equal(this.delayEffect); // inEffectSince + expect(access[1]).to.equal(0n); // currentDelay + expect(access[2]).to.equal(0n); // pendingDelay + expect(access[3]).to.equal(0n); // pendingDelayEffect // Not in effect yet - expect(await time.latest()).to.be.bignumber.lt(access[0]); + expect(await time.clock.timestamp()).to.lt(access[0]); }); }, - afterGrantDelay() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + afterGrantDelay: function self() { + self.mineDelay = true; it('role is in effect without execution delay', async function () { const access = await this.manager.getAccess(this.role.id, this.caller); - expect(access[0]).to.be.bignumber.equal(this.delayEffect); // inEffectSince - expect(access[1]).to.be.bignumber.equal('0'); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + expect(access[0]).to.equal(this.delayEffect); // inEffectSince + expect(access[1]).to.equal(0n); // currentDelay + expect(access[2]).to.equal(0n); // pendingDelay + expect(access[3]).to.equal(0n); // pendingDelayEffect // Already in effect - expect(await time.latest()).to.be.bignumber.equal(access[0]); + expect(await time.clock.timestamp()).to.equal(access[0]); }); }, }, @@ -590,25 +560,25 @@ contract('AccessManager', function (accounts) { callerHasAnExecutionDelay() { it('access has role in effect and execution delay is set', async function () { const access = await this.manager.getAccess(this.role.id, this.caller); - expect(access[0]).to.be.bignumber.equal(await time.latest()); // inEffectSince - expect(access[1]).to.be.bignumber.equal(this.executionDelay); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + expect(access[0]).to.equal(await time.clock.timestamp()); // inEffectSince + expect(access[1]).to.equal(this.executionDelay); // currentDelay + expect(access[2]).to.equal(0n); // pendingDelay + expect(access[3]).to.equal(0n); // pendingDelayEffect // Already in effect - expect(await time.latest()).to.be.bignumber.equal(access[0]); + expect(await time.clock.timestamp()).to.equal(access[0]); }); }, callerHasNoExecutionDelay() { it('access has role in effect without execution delay', async function () { const access = await this.manager.getAccess(this.role.id, this.caller); - expect(access[0]).to.be.bignumber.equal(await time.latest()); // inEffectSince - expect(access[1]).to.be.bignumber.equal('0'); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + expect(access[0]).to.equal(await time.clock.timestamp()); // inEffectSince + expect(access[1]).to.equal(0n); // currentDelay + expect(access[2]).to.equal(0n); // pendingDelay + expect(access[3]).to.equal(0n); // pendingDelayEffect // Already in effect - expect(await time.latest()).to.be.bignumber.equal(access[0]); + expect(await time.clock.timestamp()).to.equal(access[0]); }); }, }, @@ -616,10 +586,10 @@ contract('AccessManager', function (accounts) { requiredRoleIsNotGranted() { it('has empty access', async function () { const access = await this.manager.getAccess(this.role.id, this.caller); - expect(access[0]).to.be.bignumber.equal('0'); // inEffectSince - expect(access[1]).to.be.bignumber.equal('0'); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + expect(access[0]).to.equal(0n); // inEffectSince + expect(access[1]).to.equal(0n); // currentDelay + expect(access[2]).to.equal(0n); // pendingDelay + expect(access[3]).to.equal(0n); // pendingDelayEffect }); }, }); @@ -627,9 +597,9 @@ contract('AccessManager', function (accounts) { describe('#hasRole', function () { beforeEach('setup testAsHasRole', function () { - this.role = { id: web3.utils.toBN(49832) }; - this.calldata = '0x1234'; - this.caller = user; + this.role = { id: 49832n }; + this.calldata = '0x12345678'; + this.caller = this.user; }); testAsHasRole({ @@ -637,61 +607,49 @@ contract('AccessManager', function (accounts) { it('has PUBLIC role', async function () { const { isMember, executionDelay } = await this.manager.hasRole(this.role.id, this.caller); expect(isMember).to.be.true; - expect(executionDelay).to.be.bignumber.eq('0'); + expect(executionDelay).to.equal('0'); }); }, specificRoleIsRequired: { requiredRoleIsGranted: { roleGrantingIsDelayed: { callerHasAnExecutionDelay: { - beforeGrantDelay() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + beforeGrantDelay: function self() { + self.mineDelay = true; it('does not have role but execution delay', async function () { const { isMember, executionDelay } = await this.manager.hasRole(this.role.id, this.caller); expect(isMember).to.be.false; - expect(executionDelay).to.be.bignumber.eq(this.executionDelay); + expect(executionDelay).to.equal(this.executionDelay); }); }, - afterGrantDelay() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + afterGrantDelay: function self() { + self.mineDelay = true; it('has role and execution delay', async function () { const { isMember, executionDelay } = await this.manager.hasRole(this.role.id, this.caller); expect(isMember).to.be.true; - expect(executionDelay).to.be.bignumber.eq(this.executionDelay); + expect(executionDelay).to.equal(this.executionDelay); }); }, }, callerHasNoExecutionDelay: { - beforeGrantDelay() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + beforeGrantDelay: function self() { + self.mineDelay = true; it('does not have role nor execution delay', async function () { const { isMember, executionDelay } = await this.manager.hasRole(this.role.id, this.caller); expect(isMember).to.be.false; - expect(executionDelay).to.be.bignumber.eq('0'); + expect(executionDelay).to.equal('0'); }); }, - afterGrantDelay() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + afterGrantDelay: function self() { + self.mineDelay = true; it('has role and no execution delay', async function () { const { isMember, executionDelay } = await this.manager.hasRole(this.role.id, this.caller); expect(isMember).to.be.true; - expect(executionDelay).to.be.bignumber.eq('0'); + expect(executionDelay).to.equal('0'); }); }, }, @@ -701,14 +659,14 @@ contract('AccessManager', function (accounts) { it('has role and execution delay', async function () { const { isMember, executionDelay } = await this.manager.hasRole(this.role.id, this.caller); expect(isMember).to.be.true; - expect(executionDelay).to.be.bignumber.eq(this.executionDelay); + expect(executionDelay).to.equal(this.executionDelay); }); }, callerHasNoExecutionDelay() { it('has role and no execution delay', async function () { const { isMember, executionDelay } = await this.manager.hasRole(this.role.id, this.caller); expect(isMember).to.be.true; - expect(executionDelay).to.be.bignumber.eq('0'); + expect(executionDelay).to.equal('0'); }); }, }, @@ -717,7 +675,7 @@ contract('AccessManager', function (accounts) { it('has no role and no execution delay', async function () { const { isMember, executionDelay } = await this.manager.hasRole(this.role.id, this.caller); expect(isMember).to.be.false; - expect(executionDelay).to.be.bignumber.eq('0'); + expect(executionDelay).to.equal('0'); }); }, }, @@ -726,56 +684,47 @@ contract('AccessManager', function (accounts) { describe('#getSchedule', function () { beforeEach('set role and calldata', async function () { - const method = 'fnRestricted()'; - this.caller = user; - this.role = { id: web3.utils.toBN(493590) }; - await this.manager.$_setTargetFunctionRole(this.target.address, selector(method), this.role.id); + const fnRestricted = this.target.fnRestricted.getFragment().selector; + this.caller = this.user; + this.role = { id: 493590n }; + await this.manager.$_setTargetFunctionRole(this.target, fnRestricted, this.role.id); await this.manager.$_grantRole(this.role.id, this.caller, 0, 1); // nonzero execution delay - this.calldata = this.target.contract.methods[method]().encodeABI(); + this.calldata = this.target.interface.encodeFunctionData(fnRestricted, []); this.scheduleIn = time.duration.days(10); // For testAsSchedulableOperation }); testAsSchedulableOperation({ scheduled: { - before() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + before: function self() { + self.mineDelay = true; it('returns schedule in the future', async function () { const schedule = await this.manager.getSchedule(this.operationId); - expect(schedule).to.be.bignumber.equal(this.scheduledAt.add(this.scheduleIn)); - expect(schedule).to.be.bignumber.gt(await time.latest()); + expect(schedule).to.equal(this.scheduledAt + this.scheduleIn); + expect(schedule).to.gt(await time.clock.timestamp()); }); }, - after() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + after: function self() { + self.mineDelay = true; it('returns schedule', async function () { const schedule = await this.manager.getSchedule(this.operationId); - expect(schedule).to.be.bignumber.equal(this.scheduledAt.add(this.scheduleIn)); - expect(schedule).to.be.bignumber.eq(await time.latest()); + expect(schedule).to.equal(this.scheduledAt + this.scheduleIn); + expect(schedule).to.equal(await time.clock.timestamp()); }); }, - expired() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + expired: function self() { + self.mineDelay = true; it('returns 0', async function () { - expect(await this.manager.getSchedule(this.operationId)).to.be.bignumber.equal('0'); + expect(await this.manager.getSchedule(this.operationId)).to.equal(0n); }); }, }, notScheduled() { it('defaults to 0', async function () { - expect(await this.manager.getSchedule(this.operationId)).to.be.bignumber.equal('0'); + expect(await this.manager.getSchedule(this.operationId)).to.equal(0n); }); }, }); @@ -784,32 +733,33 @@ contract('AccessManager', function (accounts) { describe('#getNonce', function () { describe('when operation is scheduled', function () { beforeEach('schedule operation', async function () { - const method = 'fnRestricted()'; - this.caller = user; - this.role = { id: web3.utils.toBN(4209043) }; - await this.manager.$_setTargetFunctionRole(this.target.address, selector(method), this.role.id); + const fnRestricted = this.target.fnRestricted.getFragment().selector; + this.caller = this.user; + this.role = { id: 4209043n }; + await this.manager.$_setTargetFunctionRole(this.target, fnRestricted, this.role.id); await this.manager.$_grantRole(this.role.id, this.caller, 0, 1); // nonzero execution delay - this.calldata = this.target.contract.methods[method]().encodeABI(); + this.calldata = this.target.interface.encodeFunctionData(fnRestricted, []); this.delay = time.duration.days(10); - const { operationId } = await scheduleOperation(this.manager, { + const { operationId, schedule } = await prepareOperation(this.manager, { caller: this.caller, - target: this.target.address, + target: this.target, calldata: this.calldata, delay: this.delay, }); + await schedule(); this.operationId = operationId; }); it('returns nonce', async function () { - expect(await this.manager.getNonce(this.operationId)).to.be.bignumber.equal('1'); + expect(await this.manager.getNonce(this.operationId)).to.equal(1n); }); }); describe('when is not scheduled', function () { it('returns default 0', async function () { - expect(await this.manager.getNonce(web3.utils.keccak256('operation'))).to.be.bignumber.equal('0'); + expect(await this.manager.getNonce(id('operation'))).to.equal(0n); }); }); }); @@ -819,9 +769,9 @@ contract('AccessManager', function (accounts) { const calldata = '0x123543'; const address = someAddress; - const args = [user, address, calldata]; + const args = [this.user.address, address, calldata]; - expect(await this.manager.hashOperation(...args)).to.be.bignumber.eq(hashOperation(...args)); + expect(await this.manager.hashOperation(...args)).to.equal(hashOperation(...args)); }); }); }); @@ -835,167 +785,147 @@ contract('AccessManager', function (accounts) { describe('#labelRole', function () { describe('restrictions', function () { beforeEach('set method and args', function () { - const method = 'labelRole(uint64,string)'; const args = [123443, 'TEST']; - this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + const method = this.manager.interface.getFunction('labelRole(uint64,string)'); + this.calldata = this.manager.interface.encodeFunctionData(method, args); }); shouldBehaveLikeDelayedAdminOperation(); }); it('emits an event with the label', async function () { - expectEvent(await this.manager.labelRole(this.roles.SOME.id, 'Some label', { from: admin }), 'RoleLabel', { - roleId: this.roles.SOME.id, - label: 'Some label', - }); + await expect(this.manager.connect(this.admin).labelRole(this.roles.SOME.id, 'Some label')) + .to.emit(this.manager, 'RoleLabel') + .withArgs(this.roles.SOME.id, 'Some label'); }); it('updates label on a second call', async function () { - await this.manager.labelRole(this.roles.SOME.id, 'Some label', { from: admin }); + await this.manager.connect(this.admin).labelRole(this.roles.SOME.id, 'Some label'); - expectEvent(await this.manager.labelRole(this.roles.SOME.id, 'Updated label', { from: admin }), 'RoleLabel', { - roleId: this.roles.SOME.id, - label: 'Updated label', - }); + await expect(this.manager.connect(this.admin).labelRole(this.roles.SOME.id, 'Updated label')) + .to.emit(this.manager, 'RoleLabel') + .withArgs(this.roles.SOME.id, 'Updated label'); }); it('reverts labeling PUBLIC_ROLE', async function () { - await expectRevertCustomError( - this.manager.labelRole(this.roles.PUBLIC.id, 'Some label', { from: admin }), - 'AccessManagerLockedRole', - [this.roles.PUBLIC.id], - ); + await expect(this.manager.connect(this.admin).labelRole(this.roles.PUBLIC.id, 'Some label')) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerLockedRole') + .withArgs(this.roles.PUBLIC.id); }); it('reverts labeling ADMIN_ROLE', async function () { - await expectRevertCustomError( - this.manager.labelRole(this.roles.ADMIN.id, 'Some label', { from: admin }), - 'AccessManagerLockedRole', - [this.roles.ADMIN.id], - ); + await expect(this.manager.connect(this.admin).labelRole(this.roles.ADMIN.id, 'Some label')) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerLockedRole') + .withArgs(this.roles.ADMIN.id); }); }); describe('#setRoleAdmin', function () { describe('restrictions', function () { beforeEach('set method and args', function () { - const method = 'setRoleAdmin(uint64,uint64)'; const args = [93445, 84532]; - this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + const method = this.manager.interface.getFunction('setRoleAdmin(uint64,uint64)'); + this.calldata = this.manager.interface.encodeFunctionData(method, args); }); shouldBehaveLikeDelayedAdminOperation(); }); it("sets any role's admin if called by an admin", async function () { - expect(await this.manager.getRoleAdmin(this.roles.SOME.id)).to.be.bignumber.equal(this.roles.SOME_ADMIN.id); + expect(await this.manager.getRoleAdmin(this.roles.SOME.id)).to.equal(this.roles.SOME_ADMIN.id); - const { receipt } = await this.manager.setRoleAdmin(this.roles.SOME.id, this.roles.ADMIN.id, { from: admin }); - expectEvent(receipt, 'RoleAdminChanged', { roleId: this.roles.SOME.id, admin: this.roles.ADMIN.id }); + await expect(this.manager.connect(this.admin).setRoleAdmin(this.roles.SOME.id, this.roles.ADMIN.id)) + .to.emit(this.manager, 'RoleAdminChanged') + .withArgs(this.roles.SOME.id, this.roles.ADMIN.id); - expect(await this.manager.getRoleAdmin(this.roles.SOME.id)).to.be.bignumber.equal(this.roles.ADMIN.id); + expect(await this.manager.getRoleAdmin(this.roles.SOME.id)).to.equal(this.roles.ADMIN.id); }); it('reverts setting PUBLIC_ROLE admin', async function () { - await expectRevertCustomError( - this.manager.setRoleAdmin(this.roles.PUBLIC.id, this.roles.ADMIN.id, { from: admin }), - 'AccessManagerLockedRole', - [this.roles.PUBLIC.id], - ); + await expect(this.manager.connect(this.admin).setRoleAdmin(this.roles.PUBLIC.id, this.roles.ADMIN.id)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerLockedRole') + .withArgs(this.roles.PUBLIC.id); }); it('reverts setting ADMIN_ROLE admin', async function () { - await expectRevertCustomError( - this.manager.setRoleAdmin(this.roles.ADMIN.id, this.roles.ADMIN.id, { from: admin }), - 'AccessManagerLockedRole', - [this.roles.ADMIN.id], - ); + await expect(this.manager.connect(this.admin).setRoleAdmin(this.roles.ADMIN.id, this.roles.ADMIN.id)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerLockedRole') + .withArgs(this.roles.ADMIN.id); }); }); describe('#setRoleGuardian', function () { describe('restrictions', function () { beforeEach('set method and args', function () { - const method = 'setRoleGuardian(uint64,uint64)'; const args = [93445, 84532]; - this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + const method = this.manager.interface.getFunction('setRoleGuardian(uint64,uint64)'); + this.calldata = this.manager.interface.encodeFunctionData(method, args); }); shouldBehaveLikeDelayedAdminOperation(); }); it("sets any role's guardian if called by an admin", async function () { - expect(await this.manager.getRoleGuardian(this.roles.SOME.id)).to.be.bignumber.equal( - this.roles.SOME_GUARDIAN.id, - ); + expect(await this.manager.getRoleGuardian(this.roles.SOME.id)).to.equal(this.roles.SOME_GUARDIAN.id); - const { receipt } = await this.manager.setRoleGuardian(this.roles.SOME.id, this.roles.ADMIN.id, { - from: admin, - }); - expectEvent(receipt, 'RoleGuardianChanged', { roleId: this.roles.SOME.id, guardian: this.roles.ADMIN.id }); + await expect(this.manager.connect(this.admin).setRoleGuardian(this.roles.SOME.id, this.roles.ADMIN.id)) + .to.emit(this.manager, 'RoleGuardianChanged') + .withArgs(this.roles.SOME.id, this.roles.ADMIN.id); - expect(await this.manager.getRoleGuardian(this.roles.SOME.id)).to.be.bignumber.equal(this.roles.ADMIN.id); + expect(await this.manager.getRoleGuardian(this.roles.SOME.id)).to.equal(this.roles.ADMIN.id); }); it('reverts setting PUBLIC_ROLE admin', async function () { - await expectRevertCustomError( - this.manager.setRoleGuardian(this.roles.PUBLIC.id, this.roles.ADMIN.id, { from: admin }), - 'AccessManagerLockedRole', - [this.roles.PUBLIC.id], - ); + await expect(this.manager.connect(this.admin).setRoleGuardian(this.roles.PUBLIC.id, this.roles.ADMIN.id)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerLockedRole') + .withArgs(this.roles.PUBLIC.id); }); it('reverts setting ADMIN_ROLE admin', async function () { - await expectRevertCustomError( - this.manager.setRoleGuardian(this.roles.ADMIN.id, this.roles.ADMIN.id, { from: admin }), - 'AccessManagerLockedRole', - [this.roles.ADMIN.id], - ); + await expect(this.manager.connect(this.admin).setRoleGuardian(this.roles.ADMIN.id, this.roles.ADMIN.id)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerLockedRole') + .withArgs(this.roles.ADMIN.id); }); }); describe('#setGrantDelay', function () { describe('restrictions', function () { beforeEach('set method and args', function () { - const method = 'setGrantDelay(uint64,uint32)'; const args = [984910, time.duration.days(2)]; - this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + const method = this.manager.interface.getFunction('setGrantDelay(uint64,uint32)'); + this.calldata = this.manager.interface.encodeFunctionData(method, args); }); shouldBehaveLikeDelayedAdminOperation(); }); - it('reverts setting grant delay for the PUBLIC_ROLE', async function () { - await expectRevertCustomError( - this.manager.setGrantDelay(this.roles.PUBLIC.id, web3.utils.toBN(69), { from: admin }), - 'AccessManagerLockedRole', - [this.roles.PUBLIC.id], - ); + it('reverts setting grant delay for the PUBLIC_ROLE', function () { + expect(this.manager.connect(this.admin).setGrantDelay(this.roles.PUBLIC.id, 69n)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerLockedRole') + .withArgs(this.roles.PUBLIC.id); }); describe('when increasing the delay', function () { - const oldDelay = web3.utils.toBN(10); - const newDelay = web3.utils.toBN(100); + const oldDelay = 10n; + const newDelay = 100n; beforeEach('sets old delay', async function () { this.role = this.roles.SOME; await this.manager.$_setGrantDelay(this.role.id, oldDelay); - await time.increase(MINSETBACK); - expect(await this.manager.getRoleGrantDelay(this.role.id)).to.be.bignumber.equal(oldDelay); + await increase(MINSETBACK); + expect(await this.manager.getRoleGrantDelay(this.role.id)).to.equal(oldDelay); }); it('increases the delay after minsetback', async function () { - const { receipt } = await this.manager.setGrantDelay(this.role.id, newDelay, { from: admin }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - expectEvent(receipt, 'RoleGrantDelayChanged', { - roleId: this.role.id, - delay: newDelay, - since: timestamp.add(MINSETBACK), - }); - - expect(await this.manager.getRoleGrantDelay(this.role.id)).to.be.bignumber.equal(oldDelay); - await time.increase(MINSETBACK); - expect(await this.manager.getRoleGrantDelay(this.role.id)).to.be.bignumber.equal(newDelay); + const txResponse = await this.manager.connect(this.admin).setGrantDelay(this.role.id, newDelay); + const setGrantDelayAt = await time.clockFromReceipt.timestamp(txResponse); + expect(txResponse) + .to.emit(this.manager, 'RoleGrantDelayChanged') + .withArgs(this.role.id, newDelay, setGrantDelayAt + MINSETBACK); + + expect(await this.manager.getRoleGrantDelay(this.role.id)).to.equal(oldDelay); + await increase(MINSETBACK); + expect(await this.manager.getRoleGrantDelay(this.role.id)).to.equal(newDelay); }); }); @@ -1005,48 +935,46 @@ contract('AccessManager', function (accounts) { beforeEach('sets old delay', async function () { this.role = this.roles.SOME; await this.manager.$_setGrantDelay(this.role.id, oldDelay); - await time.increase(MINSETBACK); - expect(await this.manager.getRoleGrantDelay(this.role.id)).to.be.bignumber.equal(oldDelay); + await increase(MINSETBACK); + expect(await this.manager.getRoleGrantDelay(this.role.id)).to.equal(oldDelay); }); describe('when the delay difference is shorter than minimum setback', function () { - const newDelay = oldDelay.subn(1); + const newDelay = oldDelay - 1n; it('increases the delay after minsetback', async function () { - const { receipt } = await this.manager.setGrantDelay(this.role.id, newDelay, { from: admin }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - expectEvent(receipt, 'RoleGrantDelayChanged', { - roleId: this.role.id, - delay: newDelay, - since: timestamp.add(MINSETBACK), - }); - - expect(await this.manager.getRoleGrantDelay(this.role.id)).to.be.bignumber.equal(oldDelay); - await time.increase(MINSETBACK); - expect(await this.manager.getRoleGrantDelay(this.role.id)).to.be.bignumber.equal(newDelay); + const txResponse = await this.manager.connect(this.admin).setGrantDelay(this.role.id, newDelay); + const setGrantDelayAt = await time.clockFromReceipt.timestamp(txResponse); + expect(txResponse) + .to.emit(this.manager, 'RoleGrantDelayChanged') + .withArgs(this.role.id, newDelay, setGrantDelayAt + MINSETBACK); + + expect(await this.manager.getRoleGrantDelay(this.role.id)).to.equal(oldDelay); + await increase(MINSETBACK); + expect(await this.manager.getRoleGrantDelay(this.role.id)).to.equal(newDelay); }); }); describe('when the delay difference is longer than minimum setback', function () { - const newDelay = web3.utils.toBN(1); + const newDelay = 1n; beforeEach('assert delay difference is higher than minsetback', function () { - expect(oldDelay.sub(newDelay)).to.be.bignumber.gt(MINSETBACK); + expect(oldDelay - newDelay).to.gt(MINSETBACK); }); it('increases the delay after delay difference', async function () { - const setback = oldDelay.sub(newDelay); - const { receipt } = await this.manager.setGrantDelay(this.role.id, newDelay, { from: admin }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - expectEvent(receipt, 'RoleGrantDelayChanged', { - roleId: this.role.id, - delay: newDelay, - since: timestamp.add(setback), - }); + const setback = oldDelay - newDelay; + + const txResponse = await this.manager.connect(this.admin).setGrantDelay(this.role.id, newDelay); + const setGrantDelayAt = await time.clockFromReceipt.timestamp(txResponse); + + expect(txResponse) + .to.emit(this.manager, 'RoleGrantDelayChanged') + .withArgs(this.role.id, newDelay, setGrantDelayAt + setback); - expect(await this.manager.getRoleGrantDelay(this.role.id)).to.be.bignumber.equal(oldDelay); - await time.increase(setback); - expect(await this.manager.getRoleGrantDelay(this.role.id)).to.be.bignumber.equal(newDelay); + expect(await this.manager.getRoleGrantDelay(this.role.id)).to.equal(oldDelay); + await increase(setback); + expect(await this.manager.getRoleGrantDelay(this.role.id)).to.equal(newDelay); }); }); }); @@ -1055,9 +983,9 @@ contract('AccessManager', function (accounts) { describe('#setTargetAdminDelay', function () { describe('restrictions', function () { beforeEach('set method and args', function () { - const method = 'setTargetAdminDelay(address,uint32)'; const args = [someAddress, time.duration.days(3)]; - this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + const method = this.manager.interface.getFunction('setTargetAdminDelay(address,uint32)'); + this.calldata = this.manager.interface.encodeFunctionData(method, args); }); shouldBehaveLikeDelayedAdminOperation(); @@ -1070,22 +998,20 @@ contract('AccessManager', function (accounts) { beforeEach('sets old delay', async function () { await this.manager.$_setTargetAdminDelay(target, oldDelay); - await time.increase(MINSETBACK); - expect(await this.manager.getTargetAdminDelay(target)).to.be.bignumber.equal(oldDelay); + await increase(MINSETBACK); + expect(await this.manager.getTargetAdminDelay(target)).to.equal(oldDelay); }); it('increases the delay after minsetback', async function () { - const { receipt } = await this.manager.setTargetAdminDelay(target, newDelay, { from: admin }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - expectEvent(receipt, 'TargetAdminDelayUpdated', { - target, - delay: newDelay, - since: timestamp.add(MINSETBACK), - }); - - expect(await this.manager.getTargetAdminDelay(target)).to.be.bignumber.equal(oldDelay); - await time.increase(MINSETBACK); - expect(await this.manager.getTargetAdminDelay(target)).to.be.bignumber.equal(newDelay); + const txResponse = await this.manager.connect(this.admin).setTargetAdminDelay(target, newDelay); + const setTargetAdminDelayAt = await time.clockFromReceipt.timestamp(txResponse); + expect(txResponse) + .to.emit(this.manager, 'TargetAdminDelayUpdated') + .withArgs(target, newDelay, setTargetAdminDelayAt + MINSETBACK); + + expect(await this.manager.getTargetAdminDelay(target)).to.equal(oldDelay); + await increase(MINSETBACK); + expect(await this.manager.getTargetAdminDelay(target)).to.equal(newDelay); }); }); @@ -1095,48 +1021,46 @@ contract('AccessManager', function (accounts) { beforeEach('sets old delay', async function () { await this.manager.$_setTargetAdminDelay(target, oldDelay); - await time.increase(MINSETBACK); - expect(await this.manager.getTargetAdminDelay(target)).to.be.bignumber.equal(oldDelay); + await increase(MINSETBACK); + expect(await this.manager.getTargetAdminDelay(target)).to.equal(oldDelay); }); describe('when the delay difference is shorter than minimum setback', function () { - const newDelay = oldDelay.subn(1); + const newDelay = oldDelay - 1n; it('increases the delay after minsetback', async function () { - const { receipt } = await this.manager.setTargetAdminDelay(target, newDelay, { from: admin }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - expectEvent(receipt, 'TargetAdminDelayUpdated', { - target, - delay: newDelay, - since: timestamp.add(MINSETBACK), - }); - - expect(await this.manager.getTargetAdminDelay(target)).to.be.bignumber.equal(oldDelay); - await time.increase(MINSETBACK); - expect(await this.manager.getTargetAdminDelay(target)).to.be.bignumber.equal(newDelay); + const txResponse = await this.manager.connect(this.admin).setTargetAdminDelay(target, newDelay); + const setTargetAdminDelayAt = await time.clockFromReceipt.timestamp(txResponse); + expect(txResponse) + .to.emit(this.manager, 'TargetAdminDelayUpdated') + .withArgs(target, newDelay, setTargetAdminDelayAt + MINSETBACK); + + expect(await this.manager.getTargetAdminDelay(target)).to.equal(oldDelay); + await increase(MINSETBACK); + expect(await this.manager.getTargetAdminDelay(target)).to.equal(newDelay); }); }); describe('when the delay difference is longer than minimum setback', function () { - const newDelay = web3.utils.toBN(1); + const newDelay = 1n; beforeEach('assert delay difference is higher than minsetback', function () { - expect(oldDelay.sub(newDelay)).to.be.bignumber.gt(MINSETBACK); + expect(oldDelay - newDelay).to.gt(MINSETBACK); }); it('increases the delay after delay difference', async function () { - const setback = oldDelay.sub(newDelay); - const { receipt } = await this.manager.setTargetAdminDelay(target, newDelay, { from: admin }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - expectEvent(receipt, 'TargetAdminDelayUpdated', { - target, - delay: newDelay, - since: timestamp.add(setback), - }); + const setback = oldDelay - newDelay; + + const txResponse = await this.manager.connect(this.admin).setTargetAdminDelay(target, newDelay); + const setTargetAdminDelayAt = await time.clockFromReceipt.timestamp(txResponse); + + expect(txResponse) + .to.emit(this.manager, 'TargetAdminDelayUpdated') + .withArgs(target, newDelay, setTargetAdminDelayAt + setback); - expect(await this.manager.getTargetAdminDelay(target)).to.be.bignumber.equal(oldDelay); - await time.increase(setback); - expect(await this.manager.getTargetAdminDelay(target)).to.be.bignumber.equal(newDelay); + expect(await this.manager.getTargetAdminDelay(target)).to.equal(oldDelay); + await increase(setback); + expect(await this.manager.getTargetAdminDelay(target)).to.equal(newDelay); }); }); }); @@ -1146,73 +1070,68 @@ contract('AccessManager', function (accounts) { describe('not subject to a delay', function () { describe('#updateAuthority', function () { beforeEach('create a target and a new authority', async function () { - this.newAuthority = await AccessManager.new(admin); - this.newManagedTarget = await AccessManagedTarget.new(this.manager.address); + this.newAuthority = await ethers.deployContract('$AccessManager', [this.admin]); + this.newManagedTarget = await ethers.deployContract('$AccessManagedTarget', [this.manager]); }); describe('restrictions', function () { beforeEach('set method and args', function () { - const method = 'updateAuthority(address,address)'; - const args = [this.newManagedTarget.address, this.newAuthority.address]; - this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + this.calldata = this.manager.interface.encodeFunctionData('updateAuthority(address,address)', [ + this.newManagedTarget.target, + this.newAuthority.target, + ]); }); shouldBehaveLikeNotDelayedAdminOperation(); }); it('changes the authority', async function () { - expect(await this.newManagedTarget.authority()).to.be.equal(this.manager.address); + expect(await this.newManagedTarget.authority()).to.be.equal(this.manager.target); - const { tx } = await this.manager.updateAuthority(this.newManagedTarget.address, this.newAuthority.address, { - from: admin, - }); + await expect(this.manager.connect(this.admin).updateAuthority(this.newManagedTarget, this.newAuthority)) + .to.emit(this.newManagedTarget, 'AuthorityUpdated') // Managed contract is responsible of notifying the change through an event + .withArgs(this.newAuthority.target); - // Managed contract is responsible of notifying the change through an event - await expectEvent.inTransaction(tx, this.newManagedTarget, 'AuthorityUpdated', { - authority: this.newAuthority.address, - }); - - expect(await this.newManagedTarget.authority()).to.be.equal(this.newAuthority.address); + expect(await this.newManagedTarget.authority()).to.be.equal(this.newAuthority.target); }); }); describe('#setTargetClosed', function () { describe('restrictions', function () { beforeEach('set method and args', function () { - const method = 'setTargetClosed(address,bool)'; const args = [someAddress, true]; - this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + const method = this.manager.interface.getFunction('setTargetClosed(address,bool)'); + this.calldata = this.manager.interface.encodeFunctionData(method, args); }); shouldBehaveLikeNotDelayedAdminOperation(); }); it('closes and opens a target', async function () { - const close = await this.manager.setTargetClosed(this.target.address, true, { from: admin }); - expectEvent(close.receipt, 'TargetClosed', { target: this.target.address, closed: true }); - - expect(await this.manager.isTargetClosed(this.target.address)).to.be.equal(true); - - const open = await this.manager.setTargetClosed(this.target.address, false, { from: admin }); - expectEvent(open.receipt, 'TargetClosed', { target: this.target.address, closed: false }); - expect(await this.manager.isTargetClosed(this.target.address)).to.be.equal(false); + await expect(this.manager.connect(this.admin).setTargetClosed(this.target, true)) + .to.emit(this.manager, 'TargetClosed') + .withArgs(this.target.target, true); + expect(await this.manager.isTargetClosed(this.target)).to.be.true; + + await expect(this.manager.connect(this.admin).setTargetClosed(this.target, false)) + .to.emit(this.manager, 'TargetClosed') + .withArgs(this.target.target, false); + expect(await this.manager.isTargetClosed(this.target)).to.be.false; }); it('reverts if closing the manager', async function () { - await expectRevertCustomError( - this.manager.setTargetClosed(this.manager.address, true, { from: admin }), - 'AccessManagerLockedAccount', - [this.manager.address], - ); + await expect(this.manager.connect(this.admin).setTargetClosed(this.manager, true)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerLockedAccount') + .withArgs(this.manager.target); }); }); describe('#setTargetFunctionRole', function () { describe('restrictions', function () { beforeEach('set method and args', function () { - const method = 'setTargetFunctionRole(address,bytes4[],uint64)'; const args = [someAddress, ['0x12345678'], 443342]; - this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + const method = this.manager.interface.getFunction('setTargetFunctionRole(address,bytes4[],uint64)'); + this.calldata = this.manager.interface.encodeFunctionData(method, args); }); shouldBehaveLikeNotDelayedAdminOperation(); @@ -1222,47 +1141,28 @@ contract('AccessManager', function (accounts) { it('sets function roles', async function () { for (const sig of sigs) { - expect(await this.manager.getTargetFunctionRole(this.target.address, sig)).to.be.bignumber.equal( - this.roles.ADMIN.id, - ); + expect(await this.manager.getTargetFunctionRole(this.target, sig)).to.equal(this.roles.ADMIN.id); } - const { receipt: receipt1 } = await this.manager.setTargetFunctionRole( - this.target.address, - sigs, - this.roles.SOME.id, - { - from: admin, - }, - ); + const allowRole = await this.manager + .connect(this.admin) + .setTargetFunctionRole(this.target, sigs, this.roles.SOME.id); for (const sig of sigs) { - expectEvent(receipt1, 'TargetFunctionRoleUpdated', { - target: this.target.address, - selector: sig, - roleId: this.roles.SOME.id, - }); - expect(await this.manager.getTargetFunctionRole(this.target.address, sig)).to.be.bignumber.equal( - this.roles.SOME.id, - ); + expect(allowRole) + .to.emit(this.manager, 'TargetFunctionRoleUpdated') + .withArgs(this.target.target, sig, this.roles.SOME.id); + expect(await this.manager.getTargetFunctionRole(this.target, sig)).to.equal(this.roles.SOME.id); } - const { receipt: receipt2 } = await this.manager.setTargetFunctionRole( - this.target.address, - [sigs[1]], - this.roles.SOME_ADMIN.id, - { - from: admin, - }, - ); - expectEvent(receipt2, 'TargetFunctionRoleUpdated', { - target: this.target.address, - selector: sigs[1], - roleId: this.roles.SOME_ADMIN.id, - }); + await expect( + this.manager.connect(this.admin).setTargetFunctionRole(this.target, [sigs[1]], this.roles.SOME_ADMIN.id), + ) + .to.emit(this.manager, 'TargetFunctionRoleUpdated') + .withArgs(this.target.target, sigs[1], this.roles.SOME_ADMIN.id); for (const sig of sigs) { - expect(await this.manager.getTargetFunctionRole(this.target.address, sig)).to.be.bignumber.equal( + expect(await this.manager.getTargetFunctionRole(this.target, sig)).to.equal( sig == sigs[1] ? this.roles.SOME_ADMIN.id : this.roles.SOME.id, ); } @@ -1270,38 +1170,33 @@ contract('AccessManager', function (accounts) { }); describe('role admin operations', function () { - const ANOTHER_ADMIN = web3.utils.toBN(0xdeadc0de1); - const ANOTHER_ROLE = web3.utils.toBN(0xdeadc0de2); + const ANOTHER_ADMIN = 0xdeadc0de1n; + const ANOTHER_ROLE = 0xdeadc0de2n; beforeEach('set required role', async function () { // Make admin a member of ANOTHER_ADMIN - await this.manager.$_grantRole(ANOTHER_ADMIN, admin, 0, 0); + await this.manager.$_grantRole(ANOTHER_ADMIN, this.admin, 0, 0); await this.manager.$_setRoleAdmin(ANOTHER_ROLE, ANOTHER_ADMIN); this.role = { id: ANOTHER_ADMIN }; - this.user = user; await this.manager.$_grantRole(this.role.id, this.user, 0, 0); }); describe('#grantRole', function () { describe('restrictions', function () { beforeEach('set method and args', function () { - const method = 'grantRole(uint64,address,uint32)'; const args = [ANOTHER_ROLE, someAddress, 0]; - this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + const method = this.manager.interface.getFunction('grantRole(uint64,address,uint32)'); + this.calldata = this.manager.interface.encodeFunctionData(method, args); }); shouldBehaveLikeRoleAdminOperation(ANOTHER_ADMIN); }); it('reverts when granting PUBLIC_ROLE', async function () { - await expectRevertCustomError( - this.manager.grantRole(this.roles.PUBLIC.id, user, 0, { - from: admin, - }), - 'AccessManagerLockedRole', - [this.roles.PUBLIC.id], - ); + await expect(this.manager.connect(this.admin).grantRole(this.roles.PUBLIC.id, this.user, 0)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerLockedRole') + .withArgs(this.roles.PUBLIC.id); }); describe('when the user is not a role member', function () { @@ -1310,7 +1205,7 @@ contract('AccessManager', function (accounts) { // Delay granting this.grantDelay = time.duration.weeks(2); await this.manager.$_setGrantDelay(ANOTHER_ROLE, this.grantDelay); - await time.increase(MINSETBACK); + await increase(MINSETBACK); // Grant role this.executionDelay = time.duration.days(3); @@ -1318,74 +1213,59 @@ contract('AccessManager', function (accounts) { false, '0', ]); - const { receipt } = await this.manager.grantRole(ANOTHER_ROLE, this.user, this.executionDelay, { - from: admin, - }); - this.receipt = receipt; + this.txResponse = await this.manager + .connect(this.admin) + .grantRole(ANOTHER_ROLE, this.user, this.executionDelay); this.delay = this.grantDelay; // For testAsDelay }); testAsDelay('grant', { - before() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + before: function self() { + self.mineDelay = true; it('does not grant role to the user yet', async function () { - const timestamp = await clockFromReceipt.timestamp(this.receipt).then(web3.utils.toBN); - expectEvent(this.receipt, 'RoleGranted', { - roleId: ANOTHER_ROLE, - account: this.user, - since: timestamp.add(this.grantDelay), - delay: this.executionDelay, - newMember: true, - }); + const timestamp = await time.clockFromReceipt.timestamp(this.txResponse); + expect(this.txResponse) + .to.emit(this.manager, 'RoleGranted') + .withArgs(ANOTHER_ROLE, this.user, timestamp + this.grantDelay, this.executionDelay, true); // Access is correctly stored - const access = await this.manager.getAccess(ANOTHER_ROLE, user); - expect(access[0]).to.be.bignumber.equal(timestamp.add(this.grantDelay)); // inEffectSince - expect(access[1]).to.be.bignumber.equal(this.executionDelay); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + const access = await this.manager.getAccess(ANOTHER_ROLE, this.user); + expect(access[0]).to.equal(timestamp + this.grantDelay); // inEffectSince + expect(access[1]).to.equal(this.executionDelay); // currentDelay + expect(access[2]).to.equal(0n); // pendingDelay + expect(access[3]).to.equal(0n); // pendingDelayEffect // Not in effect yet - const currentTimestamp = await time.latest(); - expect(currentTimestamp).to.be.a.bignumber.lt(access[0]); - expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + const currentTimestamp = await time.clock.timestamp(); + expect(currentTimestamp).to.be.lt(access[0]); + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ false, this.executionDelay.toString(), ]); }); }, - after() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + after: function self() { + self.mineDelay = true; it('grants role to the user', async function () { - const timestamp = await clockFromReceipt.timestamp(this.receipt).then(web3.utils.toBN); - expectEvent(this.receipt, 'RoleGranted', { - roleId: ANOTHER_ROLE, - account: this.user, - since: timestamp.add(this.grantDelay), - delay: this.executionDelay, - newMember: true, - }); + const timestamp = await time.clockFromReceipt.timestamp(this.txResponse); + expect(this.txResponse) + .to.emit(this.manager, 'RoleAccessRequested') + .withArgs(ANOTHER_ROLE, this.user, timestamp + this.grantDelay, this.executionDelay, true); // Access is correctly stored - const access = await this.manager.getAccess(ANOTHER_ROLE, user); - expect(access[0]).to.be.bignumber.equal(timestamp.add(this.grantDelay)); // inEffectSince - expect(access[1]).to.be.bignumber.equal(this.executionDelay); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + const access = await this.manager.getAccess(ANOTHER_ROLE, this.user); + expect(access[0]).to.equal(timestamp + this.grantDelay); // inEffectSince + expect(access[1]).to.equal(this.executionDelay); // currentDelay + expect(access[2]).to.equal(0n); // pendingDelay + expect(access[3]).to.equal(0n); // pendingDelayEffect // Already in effect - const currentTimestamp = await time.latest(); - expect(currentTimestamp).to.be.a.bignumber.equal(access[0]); - expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + const currentTimestamp = await time.clock.timestamp(); + expect(currentTimestamp).to.be.equal(access[0]); + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ true, this.executionDelay.toString(), ]); @@ -1399,41 +1279,36 @@ contract('AccessManager', function (accounts) { // Delay granting this.grantDelay = 0; await this.manager.$_setGrantDelay(ANOTHER_ROLE, this.grantDelay); - await time.increase(MINSETBACK); + await increase(MINSETBACK); }); it('immediately grants the role to the user', async function () { - this.executionDelay = time.duration.days(6); + const executionDelay = time.duration.days(6); expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ false, '0', ]); - const { receipt } = await this.manager.grantRole(ANOTHER_ROLE, this.user, this.executionDelay, { - from: admin, - }); - - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - expectEvent(receipt, 'RoleGranted', { - roleId: ANOTHER_ROLE, - account: this.user, - since: timestamp, - delay: this.executionDelay, - newMember: true, - }); + const txResponse = await this.manager + .connect(this.admin) + .grantRole(ANOTHER_ROLE, this.user, executionDelay); + const grantedAt = await time.clockFromReceipt.timestamp(txResponse); + expect(txResponse) + .to.emit(this.manager, 'RoleGranted') + .withArgs(ANOTHER_ROLE, this.user.address, executionDelay, grantedAt, true); // Access is correctly stored - const access = await this.manager.getAccess(ANOTHER_ROLE, user); - expect(access[0]).to.be.bignumber.equal(timestamp); // inEffectSince - expect(access[1]).to.be.bignumber.equal(this.executionDelay); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + const access = await this.manager.getAccess(ANOTHER_ROLE, this.user); + expect(access[0]).to.equal(grantedAt); // inEffectSince + expect(access[1]).to.equal(executionDelay); // currentDelay + expect(access[2]).to.equal(0n); // pendingDelay + expect(access[3]).to.equal(0n); // pendingDelayEffect // Already in effect - const currentTimestamp = await time.latest(); - expect(currentTimestamp).to.be.a.bignumber.equal(access[0]); - expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + const currentTimestamp = await time.clock.timestamp(); + expect(currentTimestamp).to.be.equal(access[0]); + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ true, - this.executionDelay.toString(), + executionDelay.toString(), ]); }); }); @@ -1443,7 +1318,7 @@ contract('AccessManager', function (accounts) { beforeEach('make user role member', async function () { this.previousExecutionDelay = time.duration.days(6); await this.manager.$_grantRole(ANOTHER_ROLE, this.user, 0, this.previousExecutionDelay); - this.oldAccess = await this.manager.getAccess(ANOTHER_ROLE, user); + this.oldAccess = await this.manager.getAccess(ANOTHER_ROLE, this.user); }); describe('with grant delay', function () { @@ -1451,7 +1326,7 @@ contract('AccessManager', function (accounts) { // Delay granting const grantDelay = time.duration.weeks(2); await this.manager.$_setGrantDelay(ANOTHER_ROLE, grantDelay); - await time.increase(MINSETBACK); + await increase(MINSETBACK); }); describe('when increasing the execution delay', function () { @@ -1461,7 +1336,7 @@ contract('AccessManager', function (accounts) { this.previousExecutionDelay.toString(), ]); - this.newExecutionDelay = this.previousExecutionDelay.add(time.duration.days(4)); + this.newExecutionDelay = this.previousExecutionDelay + time.duration.days(4); }); it('emits event and immediately changes the execution delay', async function () { @@ -1469,28 +1344,24 @@ contract('AccessManager', function (accounts) { true, this.previousExecutionDelay.toString(), ]); - const { receipt } = await this.manager.grantRole(ANOTHER_ROLE, this.user, this.newExecutionDelay, { - from: admin, - }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - - expectEvent(receipt, 'RoleGranted', { - roleId: ANOTHER_ROLE, - account: this.user, - since: timestamp, - delay: this.newExecutionDelay, - newMember: false, - }); + const txResponse = await this.manager + .connect(this.admin) + .grantRole(ANOTHER_ROLE, this.user, this.newExecutionDelay); + const timestamp = await time.clockFromReceipt.timestamp(txResponse); + + expect(txResponse) + .to.emit(this.manager, 'RoleGranted') + .withArgs(ANOTHER_ROLE, this.user.address, timestamp, this.newExecutionDelay, false); // Access is correctly stored - const access = await this.manager.getAccess(ANOTHER_ROLE, user); - expect(access[0]).to.be.bignumber.equal(this.oldAccess[0]); // inEffectSince - expect(access[1]).to.be.bignumber.equal(this.newExecutionDelay); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + const access = await this.manager.getAccess(ANOTHER_ROLE, this.user); + expect(access[0]).to.equal(this.oldAccess[0]); // inEffectSince + expect(access[1]).to.equal(this.newExecutionDelay); // currentDelay + expect(access[2]).to.equal(0n); // pendingDelay + expect(access[3]).to.equal(0n); // pendingDelayEffect // Already in effect - expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ true, this.newExecutionDelay.toString(), ]); @@ -1504,65 +1375,60 @@ contract('AccessManager', function (accounts) { this.previousExecutionDelay.toString(), ]); - this.newExecutionDelay = this.previousExecutionDelay.sub(time.duration.days(4)); - const { receipt } = await this.manager.grantRole(ANOTHER_ROLE, this.user, this.newExecutionDelay, { - from: admin, - }); - this.grantTimestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + this.newExecutionDelay = this.previousExecutionDelay - time.duration.days(4); + this.txResponse = await this.manager + .connect(this.admin) + .grantRole(ANOTHER_ROLE, this.user, this.newExecutionDelay); + this.grantTimestamp = await time.clockFromReceipt.timestamp(this.txResponse); - this.receipt = receipt; - this.delay = this.previousExecutionDelay.sub(this.newExecutionDelay); // For testAsDelay + this.delay = this.previousExecutionDelay - this.newExecutionDelay; // For testAsDelay }); it('emits event', function () { - expectEvent(this.receipt, 'RoleGranted', { - roleId: ANOTHER_ROLE, - account: this.user, - since: this.grantTimestamp.add(this.delay), - delay: this.newExecutionDelay, - newMember: false, - }); + expect(this.txResponse) + .to.emit(this.manager, 'RoleGranted') + .withArgs( + ANOTHER_ROLE, + this.user.address, + this.grantTimestamp + this.delay, + this.newExecutionDelay, + false, + ); }); testAsDelay('execution delay effect', { - before() { - beforeEach('consume effect delay', async function () { - // Consume previously set delay - await mine(); - }); + before: function self() { + self.mineDelay = true; it('does not change the execution delay yet', async function () { // Access is correctly stored - const access = await this.manager.getAccess(ANOTHER_ROLE, user); - expect(access[0]).to.be.bignumber.equal(this.oldAccess[0]); // inEffectSince - expect(access[1]).to.be.bignumber.equal(this.previousExecutionDelay); // currentDelay - expect(access[2]).to.be.bignumber.equal(this.newExecutionDelay); // pendingDelay - expect(access[3]).to.be.bignumber.equal(this.grantTimestamp.add(this.delay)); // pendingDelayEffect + const access = await this.manager.getAccess(ANOTHER_ROLE, this.user); + expect(access[0]).to.equal(this.oldAccess[0]); // inEffectSince + expect(access[1]).to.equal(this.previousExecutionDelay); // currentDelay + expect(access[2]).to.equal(this.newExecutionDelay); // pendingDelay + expect(access[3]).to.equal(this.grantTimestamp + this.delay); // pendingDelayEffect // Not in effect yet - expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ true, this.previousExecutionDelay.toString(), ]); }); }, - after() { - beforeEach('consume effect delay', async function () { - // Consume previously set delay - await mine(); - }); + after: function self() { + self.mineDelay = true; it('changes the execution delay', async function () { // Access is correctly stored - const access = await this.manager.getAccess(ANOTHER_ROLE, user); + const access = await this.manager.getAccess(ANOTHER_ROLE, this.user); - expect(access[0]).to.be.bignumber.equal(this.oldAccess[0]); // inEffectSince - expect(access[1]).to.be.bignumber.equal(this.newExecutionDelay); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + expect(access[0]).to.equal(this.oldAccess[0]); // inEffectSince + expect(access[1]).to.equal(this.newExecutionDelay); // currentDelay + expect(access[2]).to.equal(0n); // pendingDelay + expect(access[3]).to.equal(0n); // pendingDelayEffect // Already in effect - expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ true, this.newExecutionDelay.toString(), ]); @@ -1577,7 +1443,7 @@ contract('AccessManager', function (accounts) { // Delay granting const grantDelay = 0; await this.manager.$_setGrantDelay(ANOTHER_ROLE, grantDelay); - await time.increase(MINSETBACK); + await increase(MINSETBACK); }); describe('when increasing the execution delay', function () { @@ -1587,7 +1453,7 @@ contract('AccessManager', function (accounts) { this.previousExecutionDelay.toString(), ]); - this.newExecutionDelay = this.previousExecutionDelay.add(time.duration.days(4)); + this.newExecutionDelay = this.previousExecutionDelay + time.duration.days(4); }); it('emits event and immediately changes the execution delay', async function () { @@ -1595,28 +1461,24 @@ contract('AccessManager', function (accounts) { true, this.previousExecutionDelay.toString(), ]); - const { receipt } = await this.manager.grantRole(ANOTHER_ROLE, this.user, this.newExecutionDelay, { - from: admin, - }); - const timestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); - - expectEvent(receipt, 'RoleGranted', { - roleId: ANOTHER_ROLE, - account: this.user, - since: timestamp, - delay: this.newExecutionDelay, - newMember: false, - }); + const txResponse = await this.manager + .connect(this.admin) + .grantRole(ANOTHER_ROLE, this.user, this.newExecutionDelay); + const timestamp = await time.clockFromReceipt.timestamp(txResponse); + + expect(txResponse) + .to.emit(this.manager, 'RoleGranted') + .withArgs(ANOTHER_ROLE, this.user.address, timestamp, this.newExecutionDelay, false); // Access is correctly stored - const access = await this.manager.getAccess(ANOTHER_ROLE, user); - expect(access[0]).to.be.bignumber.equal(this.oldAccess[0]); // inEffectSince - expect(access[1]).to.be.bignumber.equal(this.newExecutionDelay); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + const access = await this.manager.getAccess(ANOTHER_ROLE, this.user); + expect(access[0]).to.equal(this.oldAccess[0]); // inEffectSince + expect(access[1]).to.equal(this.newExecutionDelay); // currentDelay + expect(access[2]).to.equal(0n); // pendingDelay + expect(access[3]).to.equal(0n); // pendingDelayEffect // Already in effect - expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ true, this.newExecutionDelay.toString(), ]); @@ -1630,65 +1492,60 @@ contract('AccessManager', function (accounts) { this.previousExecutionDelay.toString(), ]); - this.newExecutionDelay = this.previousExecutionDelay.sub(time.duration.days(4)); - const { receipt } = await this.manager.grantRole(ANOTHER_ROLE, this.user, this.newExecutionDelay, { - from: admin, - }); - this.grantTimestamp = await clockFromReceipt.timestamp(receipt).then(web3.utils.toBN); + this.newExecutionDelay = this.previousExecutionDelay - time.duration.days(4); + this.txResponse = await this.manager + .connect(this.admin) + .grantRole(ANOTHER_ROLE, this.user, this.newExecutionDelay); + this.grantTimestamp = await time.clockFromReceipt.timestamp(this.txResponse); - this.receipt = receipt; - this.delay = this.previousExecutionDelay.sub(this.newExecutionDelay); // For testAsDelay + this.delay = this.previousExecutionDelay - this.newExecutionDelay; // For testAsDelay }); it('emits event', function () { - expectEvent(this.receipt, 'RoleGranted', { - roleId: ANOTHER_ROLE, - account: this.user, - since: this.grantTimestamp.add(this.delay), - delay: this.newExecutionDelay, - newMember: false, - }); + expect(this.txResponse) + .to.emit(this.manager, 'RoleGranted') + .withArgs( + ANOTHER_ROLE, + this.user.address, + this.grantTimestamp + this.delay, + this.newExecutionDelay, + false, + ); }); testAsDelay('execution delay effect', { - before() { - beforeEach('consume effect delay', async function () { - // Consume previously set delay - await mine(); - }); + before: function self() { + self.mineDelay = true; it('does not change the execution delay yet', async function () { // Access is correctly stored - const access = await this.manager.getAccess(ANOTHER_ROLE, user); - expect(access[0]).to.be.bignumber.equal(this.oldAccess[0]); // inEffectSince - expect(access[1]).to.be.bignumber.equal(this.previousExecutionDelay); // currentDelay - expect(access[2]).to.be.bignumber.equal(this.newExecutionDelay); // pendingDelay - expect(access[3]).to.be.bignumber.equal(this.grantTimestamp.add(this.delay)); // pendingDelayEffect + const access = await this.manager.getAccess(ANOTHER_ROLE, this.user); + expect(access[0]).to.equal(this.oldAccess[0]); // inEffectSince + expect(access[1]).to.equal(this.previousExecutionDelay); // currentDelay + expect(access[2]).to.equal(this.newExecutionDelay); // pendingDelay + expect(access[3]).to.equal(this.grantTimestamp + this.delay); // pendingDelayEffect // Not in effect yet - expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ true, this.previousExecutionDelay.toString(), ]); }); }, - after() { - beforeEach('consume effect delay', async function () { - // Consume previously set delay - await mine(); - }); + after: function self() { + self.mineDelay = true; it('changes the execution delay', async function () { // Access is correctly stored - const access = await this.manager.getAccess(ANOTHER_ROLE, user); + const access = await this.manager.getAccess(ANOTHER_ROLE, this.user); - expect(access[0]).to.be.bignumber.equal(this.oldAccess[0]); // inEffectSince - expect(access[1]).to.be.bignumber.equal(this.newExecutionDelay); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // pendingDelayEffect + expect(access[0]).to.equal(this.oldAccess[0]); // inEffectSince + expect(access[1]).to.equal(this.newExecutionDelay); // currentDelay + expect(access[2]).to.equal(0n); // pendingDelay + expect(access[3]).to.equal(0n); // pendingDelayEffect // Already in effect - expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ true, this.newExecutionDelay.toString(), ]); @@ -1703,9 +1560,9 @@ contract('AccessManager', function (accounts) { describe('#revokeRole', function () { describe('restrictions', function () { beforeEach('set method and args', async function () { - const method = 'revokeRole(uint64,address)'; const args = [ANOTHER_ROLE, someAddress]; - this.calldata = this.manager.contract.methods[method](...args).encodeABI(); + const method = this.manager.interface.getFunction('revokeRole(uint64,address)'); + this.calldata = this.manager.interface.encodeFunctionData(method, args); // Need to be set before revoking await this.manager.$_grantRole(...args, 0, 0); @@ -1717,64 +1574,60 @@ contract('AccessManager', function (accounts) { describe('when role has been granted', function () { beforeEach('grant role with grant delay', async function () { this.grantDelay = time.duration.weeks(1); - await this.manager.$_grantRole(ANOTHER_ROLE, user, this.grantDelay, 0); + await this.manager.$_grantRole(ANOTHER_ROLE, this.user, this.grantDelay, 0); this.delay = this.grantDelay; // For testAsDelay }); testAsDelay('grant', { - before() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + before: function self() { + self.mineDelay = true; it('revokes a granted role that will take effect in the future', async function () { - expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ false, '0', ]); - const { receipt } = await this.manager.revokeRole(ANOTHER_ROLE, user, { from: admin }); - expectEvent(receipt, 'RoleRevoked', { roleId: ANOTHER_ROLE, account: user }); + await expect(this.manager.connect(this.admin).revokeRole(ANOTHER_ROLE, this.user)) + .to.emit(this.manager, 'RoleRevoked') + .withArgs(ANOTHER_ROLE, this.user.address); - expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ false, '0', ]); - const access = await this.manager.getAccess(ANOTHER_ROLE, user); - expect(access[0]).to.be.bignumber.equal('0'); // inRoleSince - expect(access[1]).to.be.bignumber.equal('0'); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // effect + const access = await this.manager.getAccess(ANOTHER_ROLE, this.user); + expect(access[0]).to.equal(0n); // inRoleSince + expect(access[1]).to.equal(0n); // currentDelay + expect(access[2]).to.equal(0n); // pendingDelay + expect(access[3]).to.equal(0n); // effect }); }, - after() { - beforeEach('consume previously set grant delay', async function () { - // Consume previously set delay - await mine(); - }); + after: function self() { + self.mineDelay = true; it('revokes a granted role that already took effect', async function () { - expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ true, '0', ]); - const { receipt } = await this.manager.revokeRole(ANOTHER_ROLE, user, { from: admin }); - expectEvent(receipt, 'RoleRevoked', { roleId: ANOTHER_ROLE, account: user }); + await expect(this.manager.connect(this.admin).revokeRole(ANOTHER_ROLE, this.user)) + .to.emit(this.manager, 'RoleRevoked') + .withArgs(ANOTHER_ROLE, this.user.address); - expect(await this.manager.hasRole(ANOTHER_ROLE, user).then(formatAccess)).to.be.deep.equal([ + expect(await this.manager.hasRole(ANOTHER_ROLE, this.user).then(formatAccess)).to.be.deep.equal([ false, '0', ]); - const access = await this.manager.getAccess(ANOTHER_ROLE, user); - expect(access[0]).to.be.bignumber.equal('0'); // inRoleSince - expect(access[1]).to.be.bignumber.equal('0'); // currentDelay - expect(access[2]).to.be.bignumber.equal('0'); // pendingDelay - expect(access[3]).to.be.bignumber.equal('0'); // effect + const access = await this.manager.getAccess(ANOTHER_ROLE, this.user); + expect(access[0]).to.equal(0n); // inRoleSince + expect(access[1]).to.equal(0n); // currentDelay + expect(access[2]).to.equal(0n); // pendingDelay + expect(access[3]).to.equal(0n); // effect }); }, }); @@ -1782,13 +1635,15 @@ contract('AccessManager', function (accounts) { describe('when role has not been granted', function () { it('has no effect', async function () { - expect(await this.manager.hasRole(this.roles.SOME.id, user).then(formatAccess)).to.be.deep.equal([ + expect(await this.manager.hasRole(this.roles.SOME.id, this.user).then(formatAccess)).to.be.deep.equal([ false, '0', ]); - const { receipt } = await this.manager.revokeRole(this.roles.SOME.id, user, { from: manager }); - expectEvent.notEmitted(receipt, 'RoleRevoked', { roleId: ANOTHER_ROLE, account: user }); - expect(await this.manager.hasRole(this.roles.SOME.id, user).then(formatAccess)).to.be.deep.equal([ + await expect(this.manager.connect(this.roleAdmin).revokeRole(this.roles.SOME.id, this.user)).to.not.emit( + this.manager, + 'RoleRevoked', + ); + expect(await this.manager.hasRole(this.roles.SOME.id, this.user).then(formatAccess)).to.be.deep.equal([ false, '0', ]); @@ -1796,11 +1651,9 @@ contract('AccessManager', function (accounts) { }); it('reverts revoking PUBLIC_ROLE', async function () { - await expectRevertCustomError( - this.manager.revokeRole(this.roles.PUBLIC.id, user, { from: admin }), - 'AccessManagerLockedRole', - [this.roles.PUBLIC.id], - ); + await expect(this.manager.connect(this.admin).revokeRole(this.roles.PUBLIC.id, this.user)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerLockedRole') + .withArgs(this.roles.PUBLIC.id); }); }); }); @@ -1808,8 +1661,8 @@ contract('AccessManager', function (accounts) { describe('self role operations', function () { describe('#renounceRole', function () { beforeEach('grant role', async function () { - this.role = { id: web3.utils.toBN(783164) }; - this.caller = user; + this.role = { id: 783164n }; + this.caller = this.user; await this.manager.$_grantRole(this.role.id, this.caller, 0, 0); }); @@ -1818,13 +1671,9 @@ contract('AccessManager', function (accounts) { true, '0', ]); - const { receipt } = await this.manager.renounceRole(this.role.id, this.caller, { - from: this.caller, - }); - expectEvent(receipt, 'RoleRevoked', { - roleId: this.role.id, - account: this.caller, - }); + await expect(this.manager.connect(this.caller).renounceRole(this.role.id, this.caller)) + .to.emit(this.manager, 'RoleRevoked') + .withArgs(this.role.id, this.caller.address); expect(await this.manager.hasRole(this.role.id, this.caller).then(formatAccess)).to.be.deep.equal([ false, '0', @@ -1832,23 +1681,15 @@ contract('AccessManager', function (accounts) { }); it('reverts if renouncing the PUBLIC_ROLE', async function () { - await expectRevertCustomError( - this.manager.renounceRole(this.roles.PUBLIC.id, this.caller, { - from: this.caller, - }), - 'AccessManagerLockedRole', - [this.roles.PUBLIC.id], - ); + await expect(this.manager.connect(this.caller).renounceRole(this.roles.PUBLIC.id, this.caller)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerLockedRole') + .withArgs(this.roles.PUBLIC.id); }); it('reverts if renouncing with bad caller confirmation', async function () { - await expectRevertCustomError( - this.manager.renounceRole(this.role.id, someAddress, { - from: this.caller, - }), - 'AccessManagerBadConfirmation', - [], - ); + await expect( + this.manager.connect(this.caller).renounceRole(this.role.id, someAddress), + ).to.be.revertedWithCustomError(this.manager, 'AccessManagerBadConfirmation'); }); }); }); @@ -1857,32 +1698,31 @@ contract('AccessManager', function (accounts) { describe('access managed target operations', function () { describe('when calling a restricted target function', function () { - const method = 'fnRestricted()'; - beforeEach('set required role', function () { - this.role = { id: web3.utils.toBN(3597243) }; - this.manager.$_setTargetFunctionRole(this.target.address, selector(method), this.role.id); + this.method = this.target.fnRestricted.getFragment(); + this.role = { id: 3597243n }; + this.manager.$_setTargetFunctionRole(this.target, this.method.selector, this.role.id); }); describe('restrictions', function () { beforeEach('set method and args', function () { - this.calldata = this.target.contract.methods[method]().encodeABI(); - this.caller = user; + this.calldata = this.target.interface.encodeFunctionData(this.method, []); + this.caller = this.user; }); shouldBehaveLikeAManagedRestrictedOperation(); }); it('succeeds called by a role member', async function () { - await this.manager.$_grantRole(this.role.id, user, 0, 0); + await this.manager.$_grantRole(this.role.id, this.user, 0, 0); - const { receipt } = await this.target.methods[method]({ - data: this.calldata, - from: user, - }); - expectEvent(receipt, 'CalledRestricted', { - caller: user, - }); + await expect( + this.target.connect(this.user)[this.method.selector]({ + data: this.calldata, + }), + ) + .to.emit(this.target, 'CalledRestricted') + .withArgs(this.user.address); }); }); @@ -1890,33 +1730,36 @@ contract('AccessManager', function (accounts) { const method = 'fnUnrestricted()'; beforeEach('set required role', async function () { - this.role = { id: web3.utils.toBN(879435) }; - await this.manager.$_setTargetFunctionRole(this.target.address, selector(method), this.role.id); + this.role = { id: 879435n }; + await this.manager.$_setTargetFunctionRole( + this.target, + this.target[method].getFragment().selector, + this.role.id, + ); }); it('succeeds called by anyone', async function () { - const { receipt } = await this.target.methods[method]({ - data: this.calldata, - from: user, - }); - expectEvent(receipt, 'CalledUnrestricted', { - caller: user, - }); + await expect( + this.target.connect(this.user)[method]({ + data: this.calldata, + }), + ) + .to.emit(this.target, 'CalledUnrestricted') + .withArgs(this.user.address); }); }); }); describe('#schedule', function () { - const method = 'fnRestricted()'; - beforeEach('set target function role', async function () { - this.role = { id: web3.utils.toBN(498305) }; - this.caller = user; + this.method = this.target.fnRestricted.getFragment(); + this.role = { id: 498305n }; + this.caller = this.user; - await this.manager.$_setTargetFunctionRole(this.target.address, selector(method), this.role.id); + await this.manager.$_setTargetFunctionRole(this.target, this.method.selector, this.role.id); await this.manager.$_grantRole(this.role.id, this.caller, 0, 1); // nonzero execution delay - this.calldata = this.target.contract.methods[method]().encodeABI(); + this.calldata = this.target.interface.encodeFunctionData(this.method, []); this.delay = time.duration.weeks(2); }); @@ -1924,16 +1767,15 @@ contract('AccessManager', function (accounts) { testAsCanCall({ closed() { it('reverts as AccessManagerUnauthorizedCall', async function () { - await expectRevertCustomError( - scheduleOperation(this.manager, { - caller: this.caller, - target: this.target.address, - calldata: this.calldata, - delay: this.delay, - }), - 'AccessManagerUnauthorizedCall', - [this.caller, this.target.address, this.calldata.substring(0, 10)], - ); + const { schedule } = await prepareOperation(this.manager, { + caller: this.caller, + target: this.target, + calldata: this.calldata, + delay: this.delay, + }); + await expect(schedule()) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.caller.address, this.target.target, this.calldata.substring(0, 10)); }); }, open: { @@ -1943,30 +1785,25 @@ contract('AccessManager', function (accounts) { }, notExecuting() { it('reverts as AccessManagerUnauthorizedCall', async function () { - await expectRevertCustomError( - scheduleOperation(this.manager, { - caller: this.caller, - target: this.target.address, - calldata: this.calldata, - delay: this.delay, - }), - 'AccessManagerUnauthorizedCall', - [this.caller, this.target.address, this.calldata.substring(0, 10)], - ); + const { schedule } = await prepareOperation(this.manager, { + caller: this.caller, + target: this.target, + calldata: this.calldata, + delay: this.delay, + }); + await expect(schedule()) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.caller.address, this.target.target, this.calldata.substring(0, 10)); }); }, }, callerIsNotTheManager: { publicRoleIsRequired() { it('reverts as AccessManagerUnauthorizedCall', async function () { - // scheduleOperation is not used here because it alters the next block timestamp - await expectRevertCustomError( - this.manager.schedule(this.target.address, this.calldata, MAX_UINT48, { - from: this.caller, - }), - 'AccessManagerUnauthorizedCall', - [this.caller, this.target.address, this.calldata.substring(0, 10)], - ); + // prepareOperation is not used here because it alters the next block timestamp + await expect(this.manager.connect(this.caller).schedule(this.target, this.calldata, MAX_UINT48)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.caller.address, this.target.target, this.calldata.substring(0, 10)); }); }, specificRoleIsRequired: { @@ -1975,48 +1812,34 @@ contract('AccessManager', function (accounts) { callerHasAnExecutionDelay: { beforeGrantDelay() { it('reverts as AccessManagerUnauthorizedCall', async function () { - // scheduleOperation is not used here because it alters the next block timestamp - await expectRevertCustomError( - this.manager.schedule(this.target.address, this.calldata, MAX_UINT48, { - from: this.caller, - }), - 'AccessManagerUnauthorizedCall', - [this.caller, this.target.address, this.calldata.substring(0, 10)], - ); + // prepareOperation is not used here because it alters the next block timestamp + await expect(this.manager.connect(this.caller).schedule(this.target, this.calldata, MAX_UINT48)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.caller.address, this.target.target, this.calldata.substring(0, 10)); }); }, afterGrantDelay() { it('succeeds', async function () { - // scheduleOperation is not used here because it alters the next block timestamp - await this.manager.schedule(this.target.address, this.calldata, MAX_UINT48, { - from: this.caller, - }); + // prepareOperation is not used here because it alters the next block timestamp + await this.manager.connect(this.caller).schedule(this.target, this.calldata, MAX_UINT48); }); }, }, callerHasNoExecutionDelay: { beforeGrantDelay() { it('reverts as AccessManagerUnauthorizedCall', async function () { - // scheduleOperation is not used here because it alters the next block timestamp - await expectRevertCustomError( - this.manager.schedule(this.target.address, this.calldata, MAX_UINT48, { - from: this.caller, - }), - 'AccessManagerUnauthorizedCall', - [this.caller, this.target.address, this.calldata.substring(0, 10)], - ); + // prepareOperation is not used here because it alters the next block timestamp + await expect(this.manager.connect(this.caller).schedule(this.target, this.calldata, MAX_UINT48)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.caller.address, this.target.target, this.calldata.substring(0, 10)); }); }, afterGrantDelay() { it('reverts as AccessManagerUnauthorizedCall', async function () { - // scheduleOperation is not used here because it alters the next block timestamp - await expectRevertCustomError( - this.manager.schedule(this.target.address, this.calldata, MAX_UINT48, { - from: this.caller, - }), - 'AccessManagerUnauthorizedCall', - [this.caller, this.target.address, this.calldata.substring(0, 10)], - ); + // prepareOperation is not used here because it alters the next block timestamp + await expect(this.manager.connect(this.caller).schedule(this.target, this.calldata, MAX_UINT48)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.caller.address, this.target.target, this.calldata.substring(0, 10)); }); }, }, @@ -2024,40 +1847,37 @@ contract('AccessManager', function (accounts) { roleGrantingIsNotDelayed: { callerHasAnExecutionDelay() { it('succeeds', async function () { - await scheduleOperation(this.manager, { + const { schedule } = await prepareOperation(this.manager, { caller: this.caller, - target: this.target.address, + target: this.target, calldata: this.calldata, delay: this.delay, }); + + await schedule(); }); }, callerHasNoExecutionDelay() { it('reverts as AccessManagerUnauthorizedCall', async function () { - // scheduleOperation is not used here because it alters the next block timestamp - await expectRevertCustomError( - this.manager.schedule(this.target.address, this.calldata, MAX_UINT48, { - from: this.caller, - }), - 'AccessManagerUnauthorizedCall', - [this.caller, this.target.address, this.calldata.substring(0, 10)], - ); + // prepareOperation is not used here because it alters the next block timestamp + await expect(this.manager.connect(this.caller).schedule(this.target, this.calldata, MAX_UINT48)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.caller.address, this.target.target, this.calldata.substring(0, 10)); }); }, }, }, requiredRoleIsNotGranted() { it('reverts as AccessManagerUnauthorizedCall', async function () { - await expectRevertCustomError( - scheduleOperation(this.manager, { - caller: this.caller, - target: this.target.address, - calldata: this.calldata, - delay: this.delay, - }), - 'AccessManagerUnauthorizedCall', - [this.caller, this.target.address, this.calldata.substring(0, 10)], - ); + const { schedule } = await prepareOperation(this.manager, { + caller: this.caller, + target: this.target, + calldata: this.calldata, + delay: this.delay, + }); + await expect(schedule()) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.caller.address, this.target.target, this.calldata.substring(0, 10)); }); }, }, @@ -2067,215 +1887,206 @@ contract('AccessManager', function (accounts) { }); it('schedules an operation at the specified execution date if it is larger than caller execution delay', async function () { - const { operationId, scheduledAt, receipt } = await scheduleOperation(this.manager, { + const { operationId, scheduledAt, schedule } = await prepareOperation(this.manager, { caller: this.caller, - target: this.target.address, + target: this.target, calldata: this.calldata, delay: this.delay, }); - expect(await this.manager.getSchedule(operationId)).to.be.bignumber.equal(scheduledAt.add(this.delay)); - expectEvent(receipt, 'OperationScheduled', { - operationId, - nonce: '1', - schedule: scheduledAt.add(this.delay), - target: this.target.address, - data: this.calldata, - }); + const txResponse = await schedule(); + + expect(await this.manager.getSchedule(operationId)).to.equal(scheduledAt + this.delay); + expect(txResponse) + .to.emit(this.manager, 'OperationScheduled') + .withArgs(operationId, '1', scheduledAt + this.delay, this.target.target, this.calldata); }); it('schedules an operation at the minimum execution date if no specified execution date (when == 0)', async function () { const executionDelay = await time.duration.hours(72); await this.manager.$_grantRole(this.role.id, this.caller, 0, executionDelay); - const timestamp = await time.latest(); - const scheduledAt = timestamp.addn(1); - await setNextBlockTimestamp(scheduledAt); - const { receipt } = await this.manager.schedule(this.target.address, this.calldata, 0, { - from: this.caller, - }); + const txResponse = await this.manager.connect(this.caller).schedule(this.target, this.calldata, 0); + const scheduledAt = await time.clockFromReceipt.timestamp(txResponse); - const operationId = await this.manager.hashOperation(this.caller, this.target.address, this.calldata); + const operationId = await this.manager.hashOperation(this.caller, this.target, this.calldata); - expect(await this.manager.getSchedule(operationId)).to.be.bignumber.equal(scheduledAt.add(executionDelay)); - expectEvent(receipt, 'OperationScheduled', { - operationId, - nonce: '1', - schedule: scheduledAt.add(executionDelay), - target: this.target.address, - data: this.calldata, - }); + expect(await this.manager.getSchedule(operationId)).to.equal(scheduledAt + executionDelay); + expect(txResponse) + .to.emit(this.manager, 'OperationScheduled') + .withArgs(operationId, '1', scheduledAt + executionDelay, this.target.target, this.calldata); }); it('increases the nonce of an operation scheduled more than once', async function () { // Setup and check initial nonce - const expectedOperationId = hashOperation(this.caller, this.target.address, this.calldata); - expect(await this.manager.getNonce(expectedOperationId)).to.be.bignumber.eq('0'); + const expectedOperationId = hashOperation(this.caller, this.target, this.calldata); + expect(await this.manager.getNonce(expectedOperationId)).to.equal('0'); // Schedule - const op1 = await scheduleOperation(this.manager, { + const op1 = await prepareOperation(this.manager, { caller: this.caller, - target: this.target.address, + target: this.target, calldata: this.calldata, delay: this.delay, }); - expectEvent(op1.receipt, 'OperationScheduled', { - operationId: op1.operationId, - nonce: '1', - schedule: op1.scheduledAt.add(this.delay), - target: this.target.address, - data: this.calldata, - }); - expect(expectedOperationId).to.eq(op1.operationId); + await expect(op1.schedule()) + .to.emit(this.manager, 'OperationScheduled') + .withArgs( + op1.operationId, + 1n, + op1.scheduledAt + this.delay, + this.caller.address, + this.target.target, + this.calldata, + ); + expect(expectedOperationId).to.equal(op1.operationId); // Consume - await time.increase(this.delay); + await increase(this.delay); await this.manager.$_consumeScheduledOp(expectedOperationId); // Check nonce - expect(await this.manager.getNonce(expectedOperationId)).to.be.bignumber.eq('1'); + expect(await this.manager.getNonce(expectedOperationId)).to.equal('1'); // Schedule again - const op2 = await scheduleOperation(this.manager, { + const op2 = await prepareOperation(this.manager, { caller: this.caller, - target: this.target.address, + target: this.target, calldata: this.calldata, delay: this.delay, }); - expectEvent(op2.receipt, 'OperationScheduled', { - operationId: op2.operationId, - nonce: '2', - schedule: op2.scheduledAt.add(this.delay), - target: this.target.address, - data: this.calldata, - }); - expect(expectedOperationId).to.eq(op2.operationId); + await expect(op2.schedule()) + .to.emit(this.manager, 'OperationScheduled') + .withArgs( + op2.operationId, + 2n, + op2.scheduledAt + this.delay, + this.caller.address, + this.target.target, + this.calldata, + ); + expect(expectedOperationId).to.equal(op2.operationId); // Check final nonce - expect(await this.manager.getNonce(expectedOperationId)).to.be.bignumber.eq('2'); + expect(await this.manager.getNonce(expectedOperationId)).to.equal('2'); }); it('reverts if the specified execution date is before the current timestamp + caller execution delay', async function () { - const executionDelay = time.duration.weeks(1).add(this.delay); + const executionDelay = time.duration.weeks(1) + this.delay; await this.manager.$_grantRole(this.role.id, this.caller, 0, executionDelay); - await expectRevertCustomError( - scheduleOperation(this.manager, { - caller: this.caller, - target: this.target.address, - calldata: this.calldata, - delay: this.delay, - }), - 'AccessManagerUnauthorizedCall', - [this.caller, this.target.address, this.calldata.substring(0, 10)], - ); + const { schedule } = await prepareOperation(this.manager, { + caller: this.caller, + target: this.target, + calldata: this.calldata, + delay: this.delay, + }); + + await expect(schedule()) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.caller.address, this.target.target, this.calldata.substring(0, 10)); }); it('reverts if an operation is already schedule', async function () { - const { operationId } = await scheduleOperation(this.manager, { + const op1 = await prepareOperation(this.manager, { caller: this.caller, - target: this.target.address, + target: this.target, calldata: this.calldata, delay: this.delay, }); - await expectRevertCustomError( - scheduleOperation(this.manager, { - caller: this.caller, - target: this.target.address, - calldata: this.calldata, - delay: this.delay, - }), - 'AccessManagerAlreadyScheduled', - [operationId], - ); + await op1.schedule(); + + const op2 = await prepareOperation(this.manager, { + caller: this.caller, + target: this.target, + calldata: this.calldata, + delay: this.delay, + }); + + await expect(op2.schedule()) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerAlreadyScheduled') + .withArgs(op1.operationId); }); it('panics scheduling calldata with less than 4 bytes', async function () { const calldata = '0x1234'; // 2 bytes // Managed contract - await expectRevert.unspecified( - scheduleOperation(this.manager, { - caller: this.caller, - target: this.target.address, - calldata: calldata, - delay: this.delay, - }), - ); + const op1 = await prepareOperation(this.manager, { + caller: this.caller, + target: this.target, + calldata: calldata, + delay: this.delay, + }); + await expect(op1.schedule()).to.be.revertedWithoutReason(); // Manager contract - await expectRevert.unspecified( - scheduleOperation(this.manager, { - caller: this.caller, - target: this.manager.address, - calldata: calldata, - delay: this.delay, - }), - ); + const op2 = await prepareOperation(this.manager, { + caller: this.caller, + target: this.manager, + calldata: calldata, + delay: this.delay, + }); + await expect(op2.schedule()).to.be.revertedWithoutReason(); }); it('reverts scheduling an unknown operation to the manager', async function () { const calldata = '0x12345678'; - await expectRevertCustomError( - scheduleOperation(this.manager, { - caller: this.caller, - target: this.manager.address, - calldata, - delay: this.delay, - }), - 'AccessManagerUnauthorizedCall', - [this.caller, this.manager.address, calldata], - ); + const { schedule } = await prepareOperation(this.manager, { + caller: this.caller, + target: this.manager, + calldata, + delay: this.delay, + }); + + await expect(schedule()) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.caller.address, this.manager.target, calldata); }); }); describe('#execute', function () { - const method = 'fnRestricted()'; - beforeEach('set target function role', async function () { - this.role = { id: web3.utils.toBN(9825430) }; - this.caller = user; + this.method = this.target.fnRestricted.getFragment(); + this.role = { id: 9825430n }; + this.caller = this.user; - await this.manager.$_setTargetFunctionRole(this.target.address, selector(method), this.role.id); + await this.manager.$_setTargetFunctionRole(this.target, this.method.selector, this.role.id); await this.manager.$_grantRole(this.role.id, this.caller, 0, 0); - this.calldata = this.target.contract.methods[method]().encodeABI(); + this.calldata = this.target.interface.encodeFunctionData(this.method, []); }); describe('restrictions', function () { testAsCanCall({ closed() { it('reverts as AccessManagerUnauthorizedCall', async function () { - await expectRevertCustomError( - this.manager.execute(this.target.address, this.calldata, { from: this.caller }), - 'AccessManagerUnauthorizedCall', - [this.caller, this.target.address, this.calldata.substring(0, 10)], - ); + await expect(this.manager.connect(this.caller).execute(this.target, this.calldata)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.caller.address, this.target.target, this.calldata.substring(0, 10)); }); }, open: { callerIsTheManager: { executing() { it('succeeds', async function () { - await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); + await this.manager.connect(this.caller).execute(this.target, this.calldata); }); }, notExecuting() { it('reverts as AccessManagerUnauthorizedCall', async function () { - await expectRevertCustomError( - this.manager.execute(this.target.address, this.calldata, { from: this.caller }), - 'AccessManagerUnauthorizedCall', - [this.caller, this.target.address, this.calldata.substring(0, 10)], - ); + await expect(this.manager.connect(this.caller).execute(this.target, this.calldata)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.caller.address, this.target.target, this.calldata.substring(0, 10)); }); }, }, callerIsNotTheManager: { publicRoleIsRequired() { it('succeeds', async function () { - await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); + await this.manager.connect(this.caller).execute(this.target, this.calldata); }); }, specificRoleIsRequired: { @@ -2284,18 +2095,16 @@ contract('AccessManager', function (accounts) { callerHasAnExecutionDelay: { beforeGrantDelay() { it('reverts as AccessManagerUnauthorizedCall', async function () { - await expectRevertCustomError( - this.manager.execute(this.target.address, this.calldata, { from: this.caller }), - 'AccessManagerUnauthorizedCall', - [this.caller, this.target.address, this.calldata.substring(0, 10)], - ); + await expect(this.manager.connect(this.caller).execute(this.target, this.calldata)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.caller.address, this.target.target, this.calldata.substring(0, 10)); }); }, - afterGrantDelay() { - beforeEach('define schedule delay', async function () { - // Consume previously set delay - await mine(); - this.scheduleIn = time.duration.days(21); + afterGrantDelay: function self() { + self.mineDelay = true; + + beforeEach('define schedule delay', function () { + this.scheduleIn = time.duration.days(21); // For testAsSchedulableOperation }); testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE); @@ -2304,47 +2113,40 @@ contract('AccessManager', function (accounts) { callerHasNoExecutionDelay: { beforeGrantDelay() { it('reverts as AccessManagerUnauthorizedCall', async function () { - await expectRevertCustomError( - this.manager.execute(this.target.address, this.calldata, { from: this.caller }), - 'AccessManagerUnauthorizedCall', - [this.caller, this.target.address, this.calldata.substring(0, 10)], - ); + await expect(this.manager.connect(this.caller).execute(this.target, this.calldata)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.caller.address, this.target.target, this.calldata.substring(0, 10)); }); }, - afterGrantDelay() { - beforeEach('define schedule delay', async function () { - // Consume previously set delay - await mine(); - }); + afterGrantDelay: function self() { + self.mineDelay = true; it('succeeds', async function () { - await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); + await this.manager.connect(this.caller).execute(this.target, this.calldata); }); }, }, }, roleGrantingIsNotDelayed: { callerHasAnExecutionDelay() { - beforeEach('define schedule delay', async function () { - this.scheduleIn = time.duration.days(15); + beforeEach('define schedule delay', function () { + this.scheduleIn = time.duration.days(15); // For testAsSchedulableOperation }); testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE); }, callerHasNoExecutionDelay() { it('succeeds', async function () { - await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); + await this.manager.connect(this.caller).execute(this.target, this.calldata); }); }, }, }, requiredRoleIsNotGranted() { it('reverts as AccessManagerUnauthorizedCall', async function () { - await expectRevertCustomError( - this.manager.execute(this.target.address, this.calldata, { from: this.caller }), - 'AccessManagerUnauthorizedCall', - [this.caller, this.target.address, this.calldata.substring(0, 10)], - ); + await expect(this.manager.connect(this.caller).execute(this.target, this.calldata)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.caller.address, this.target.target, this.calldata.substring(0, 10)); }); }, }, @@ -2357,19 +2159,19 @@ contract('AccessManager', function (accounts) { const delay = time.duration.hours(4); await this.manager.$_grantRole(this.role.id, this.caller, 0, 1); // Execution delay is needed so the operation is consumed - const { operationId } = await scheduleOperation(this.manager, { + const { operationId, schedule } = await prepareOperation(this.manager, { caller: this.caller, - target: this.target.address, + target: this.target, calldata: this.calldata, delay, }); - await time.increase(delay); - const { receipt } = await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); - expectEvent(receipt, 'OperationExecuted', { - operationId, - nonce: '1', - }); - expect(await this.manager.getSchedule(operationId)).to.be.bignumber.equal('0'); + await schedule(); + await increase(delay); + await expect(this.manager.connect(this.caller).execute(this.target, this.calldata)) + .to.emit(this.manager, 'OperationExecuted') + .withArgs(operationId, 1n); + + expect(await this.manager.getSchedule(operationId)).to.equal(0n); }); it('executes with no delay consuming a scheduled operation', async function () { @@ -2378,60 +2180,60 @@ contract('AccessManager', function (accounts) { // give caller an execution delay await this.manager.$_grantRole(this.role.id, this.caller, 0, 1); - const { operationId } = await scheduleOperation(this.manager, { + const { operationId, schedule } = await prepareOperation(this.manager, { caller: this.caller, - target: this.target.address, + target: this.target, calldata: this.calldata, delay, }); + await schedule(); // remove the execution delay await this.manager.$_grantRole(this.role.id, this.caller, 0, 0); - await time.increase(delay); - const { receipt } = await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); - expectEvent(receipt, 'OperationExecuted', { - operationId, - nonce: '1', - }); - expect(await this.manager.getSchedule(operationId)).to.be.bignumber.equal('0'); + await increase(delay); + await expect(this.manager.connect(this.caller).execute(this.target, this.calldata)) + .to.emit(this.manager, 'OperationExecuted') + .withArgs(operationId, 1n); + + expect(await this.manager.getSchedule(operationId)).to.equal(0n); }); it('keeps the original _executionId after finishing the call', async function () { - const executionIdBefore = await getStorageAt(this.manager.address, EXECUTION_ID_STORAGE_SLOT); - await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); - const executionIdAfter = await getStorageAt(this.manager.address, EXECUTION_ID_STORAGE_SLOT); - expect(executionIdBefore).to.be.bignumber.equal(executionIdAfter); + const executionIdBefore = await getStorageAt(this.manager.target, EXECUTION_ID_STORAGE_SLOT); + await this.manager.connect(this.caller).execute(this.target, this.calldata); + const executionIdAfter = await getStorageAt(this.manager.target, EXECUTION_ID_STORAGE_SLOT); + expect(executionIdBefore).to.equal(executionIdAfter); }); it('reverts executing twice', async function () { const delay = time.duration.hours(2); await this.manager.$_grantRole(this.role.id, this.caller, 0, 1); // Execution delay is needed so the operation is consumed - const { operationId } = await scheduleOperation(this.manager, { + const { operationId, schedule } = await prepareOperation(this.manager, { caller: this.caller, - target: this.target.address, + target: this.target, calldata: this.calldata, delay, }); - await time.increase(delay); - await this.manager.execute(this.target.address, this.calldata, { from: this.caller }); - await expectRevertCustomError( - this.manager.execute(this.target.address, this.calldata, { from: this.caller }), - 'AccessManagerNotScheduled', - [operationId], - ); + await schedule(); + await increase(delay); + await this.manager.connect(this.caller).execute(this.target, this.calldata); + await expect(this.manager.connect(this.caller).execute(this.target, this.calldata)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerNotScheduled') + .withArgs(operationId); }); }); describe('#consumeScheduledOp', function () { beforeEach('define scheduling parameters', async function () { - const method = 'fnRestricted()'; - this.caller = this.target.address; - this.calldata = this.target.contract.methods[method]().encodeABI(); - this.role = { id: web3.utils.toBN(9834983) }; + const method = this.target.fnRestricted.getFragment(); + this.caller = await ethers.getSigner(this.target.target); + await impersonate(this.caller.address); + this.calldata = this.target.interface.encodeFunctionData(method, []); + this.role = { id: 9834983n }; - await this.manager.$_setTargetFunctionRole(this.target.address, selector(method), this.role.id); + await this.manager.$_setTargetFunctionRole(this.target, method.selector, this.role.id); await this.manager.$_grantRole(this.role.id, this.caller, 0, 1); // nonzero execution delay this.scheduleIn = time.duration.hours(10); // For testAsSchedulableOperation @@ -2439,71 +2241,53 @@ contract('AccessManager', function (accounts) { describe('when caller is not consuming scheduled operation', function () { beforeEach('set consuming false', async function () { - await this.target.setIsConsumingScheduledOp(false, `0x${CONSUMING_SCHEDULE_STORAGE_SLOT.toString(16)}`); + await this.target.setIsConsumingScheduledOp(false, toBeHex(CONSUMING_SCHEDULE_STORAGE_SLOT, 32)); }); it('reverts as AccessManagerUnauthorizedConsume', async function () { - await impersonate(this.caller); - await expectRevertCustomError( - this.manager.consumeScheduledOp(this.caller, this.calldata, { from: this.caller }), - 'AccessManagerUnauthorizedConsume', - [this.caller], - ); + await expect(this.manager.connect(this.caller).consumeScheduledOp(this.caller, this.calldata)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedConsume') + .withArgs(this.caller.address); }); }); describe('when caller is consuming scheduled operation', function () { beforeEach('set consuming true', async function () { - await this.target.setIsConsumingScheduledOp(true, `0x${CONSUMING_SCHEDULE_STORAGE_SLOT.toString(16)}`); + await this.target.setIsConsumingScheduledOp(true, toBeHex(CONSUMING_SCHEDULE_STORAGE_SLOT, 32)); }); testAsSchedulableOperation({ scheduled: { before() { it('reverts as AccessManagerNotReady', async function () { - await impersonate(this.caller); - await expectRevertCustomError( - this.manager.consumeScheduledOp(this.caller, this.calldata, { from: this.caller }), - 'AccessManagerNotReady', - [this.operationId], - ); + await expect(this.manager.connect(this.caller).consumeScheduledOp(this.caller, this.calldata)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerNotReady') + .withArgs(this.operationId); }); }, after() { it('consumes the scheduled operation and resets timepoint', async function () { - expect(await this.manager.getSchedule(this.operationId)).to.be.bignumber.equal( - this.scheduledAt.add(this.scheduleIn), - ); - await impersonate(this.caller); - const { receipt } = await this.manager.consumeScheduledOp(this.caller, this.calldata, { - from: this.caller, - }); - expectEvent(receipt, 'OperationExecuted', { - operationId: this.operationId, - nonce: '1', - }); - expect(await this.manager.getSchedule(this.operationId)).to.be.bignumber.equal('0'); + expect(await this.manager.getSchedule(this.operationId)).to.equal(this.scheduledAt + this.scheduleIn); + + await expect(this.manager.connect(this.caller).consumeScheduledOp(this.caller, this.calldata)) + .to.emit(this.manager, 'OperationExecuted') + .withArgs(this.operationId, 1n); + expect(await this.manager.getSchedule(this.operationId)).to.equal(0n); }); }, expired() { it('reverts as AccessManagerExpired', async function () { - await impersonate(this.caller); - await expectRevertCustomError( - this.manager.consumeScheduledOp(this.caller, this.calldata, { from: this.caller }), - 'AccessManagerExpired', - [this.operationId], - ); + await expect(this.manager.connect(this.caller).consumeScheduledOp(this.caller, this.calldata)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerExpired') + .withArgs(this.operationId); }); }, }, notScheduled() { it('reverts as AccessManagerNotScheduled', async function () { - await impersonate(this.caller); - await expectRevertCustomError( - this.manager.consumeScheduledOp(this.caller, this.calldata, { from: this.caller }), - 'AccessManagerNotScheduled', - [this.operationId], - ); + await expect(this.manager.connect(this.caller).consumeScheduledOp(this.caller, this.calldata)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerNotScheduled') + .withArgs(this.operationId); }); }, }); @@ -2511,14 +2295,13 @@ contract('AccessManager', function (accounts) { }); describe('#cancelScheduledOp', function () { - const method = 'fnRestricted()'; - beforeEach('setup scheduling', async function () { + this.method = this.target.fnRestricted.getFragment(); this.caller = this.roles.SOME.members[0]; - await this.manager.$_setTargetFunctionRole(this.target.address, selector(method), this.roles.SOME.id); + await this.manager.$_setTargetFunctionRole(this.target, this.method.selector, this.roles.SOME.id); await this.manager.$_grantRole(this.roles.SOME.id, this.caller, 0, 1); // nonzero execution delay - this.calldata = this.target.contract.methods[method]().encodeABI(); + this.calldata = this.target.interface.encodeFunctionData(this.method, []); this.scheduleIn = time.duration.days(10); // For testAsSchedulableOperation }); @@ -2527,162 +2310,161 @@ contract('AccessManager', function (accounts) { before() { describe('when caller is the scheduler', function () { it('succeeds', async function () { - await this.manager.cancel(this.caller, this.target.address, this.calldata, { from: this.caller }); + await this.manager.connect(this.caller).cancel(this.caller, this.target, this.calldata); }); }); describe('when caller is an admin', function () { it('succeeds', async function () { - await this.manager.cancel(this.caller, this.target.address, this.calldata, { - from: this.roles.ADMIN.members[0], - }); + await this.manager.connect(this.roles.ADMIN.members[0]).cancel(this.caller, this.target, this.calldata); }); }); describe('when caller is the role guardian', function () { it('succeeds', async function () { - await this.manager.cancel(this.caller, this.target.address, this.calldata, { - from: this.roles.SOME_GUARDIAN.members[0], - }); + await this.manager + .connect(this.roles.SOME_GUARDIAN.members[0]) + .cancel(this.caller, this.target, this.calldata); }); }); describe('when caller is any other account', function () { it('reverts as AccessManagerUnauthorizedCancel', async function () { - await expectRevertCustomError( - this.manager.cancel(this.caller, this.target.address, this.calldata, { from: other }), - 'AccessManagerUnauthorizedCancel', - [other, this.caller, this.target.address, selector(method)], - ); + await expect(this.manager.connect(this.other).cancel(this.caller, this.target, this.calldata)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCancel') + .withArgs(this.other.address, this.caller.address, this.target.target, this.method.selector); }); }); }, after() { it('succeeds', async function () { - await this.manager.cancel(this.caller, this.target.address, this.calldata, { from: this.caller }); + await this.manager.connect(this.caller).cancel(this.caller, this.target, this.calldata); }); }, expired() { it('succeeds', async function () { - await this.manager.cancel(this.caller, this.target.address, this.calldata, { from: this.caller }); + await this.manager.connect(this.caller).cancel(this.caller, this.target, this.calldata); }); }, }, notScheduled() { it('reverts as AccessManagerNotScheduled', async function () { - await expectRevertCustomError( - this.manager.cancel(this.caller, this.target.address, this.calldata), - 'AccessManagerNotScheduled', - [this.operationId], - ); + await expect(this.manager.cancel(this.caller, this.target, this.calldata)) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerNotScheduled') + .withArgs(this.operationId); }); }, }); it('cancels an operation and resets schedule', async function () { - const { operationId } = await scheduleOperation(this.manager, { + const { operationId, schedule } = await prepareOperation(this.manager, { caller: this.caller, - target: this.target.address, + target: this.target, calldata: this.calldata, delay: this.scheduleIn, }); - const { receipt } = await this.manager.cancel(this.caller, this.target.address, this.calldata, { - from: this.caller, - }); - expectEvent(receipt, 'OperationCanceled', { - operationId, - nonce: '1', - }); - expect(await this.manager.getSchedule(operationId)).to.be.bignumber.eq('0'); + await schedule(); + await expect(this.manager.connect(this.caller).cancel(this.caller, this.target, this.calldata)) + .to.emit(this.manager, 'OperationCanceled') + .withArgs(operationId, 1n); + expect(await this.manager.getSchedule(operationId)).to.equal('0'); }); }); describe('with Ownable target contract', function () { - const roleId = web3.utils.toBN(1); + const roleId = 1n; beforeEach(async function () { - this.ownable = await Ownable.new(this.manager.address); + this.ownable = await ethers.deployContract('$Ownable', [this.manager]); // add user to role - await this.manager.$_grantRole(roleId, user, 0, 0); + await this.manager.$_grantRole(roleId, this.user, 0, 0); }); it('initial state', async function () { - expect(await this.ownable.owner()).to.be.equal(this.manager.address); + expect(await this.ownable.owner()).to.be.equal(this.manager.target); }); describe('Contract is closed', function () { beforeEach(async function () { - await this.manager.$_setTargetClosed(this.ownable.address, true); + await this.manager.$_setTargetClosed(this.ownable, true); }); it('directly call: reverts', async function () { - await expectRevertCustomError(this.ownable.$_checkOwner({ from: user }), 'OwnableUnauthorizedAccount', [user]); + await expect(this.ownable.connect(this.user).$_checkOwner()) + .to.be.revertedWithCustomError(this.ownable, 'OwnableUnauthorizedAccount') + .withArgs(this.user.address); }); it('relayed call (with role): reverts', async function () { - await expectRevertCustomError( - this.manager.execute(this.ownable.address, selector('$_checkOwner()'), { from: user }), - 'AccessManagerUnauthorizedCall', - [user, this.ownable.address, selector('$_checkOwner()')], - ); + await expect( + this.manager.connect(this.user).execute(this.ownable, this.ownable.$_checkOwner.getFragment().selector), + ) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.user.address, this.ownable.target, this.ownable.$_checkOwner.getFragment().selector); }); it('relayed call (without role): reverts', async function () { - await expectRevertCustomError( - this.manager.execute(this.ownable.address, selector('$_checkOwner()'), { from: other }), - 'AccessManagerUnauthorizedCall', - [other, this.ownable.address, selector('$_checkOwner()')], - ); + await expect( + this.manager.connect(this.other).execute(this.ownable, this.ownable.$_checkOwner.getFragment().selector), + ) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.other.address, this.ownable.target, this.ownable.$_checkOwner.getFragment().selector); }); }); describe('Contract is managed', function () { describe('function is open to specific role', function () { beforeEach(async function () { - await this.manager.$_setTargetFunctionRole(this.ownable.address, selector('$_checkOwner()'), roleId); + await this.manager.$_setTargetFunctionRole( + this.ownable, + this.ownable.$_checkOwner.getFragment().selector, + roleId, + ); }); it('directly call: reverts', async function () { - await expectRevertCustomError(this.ownable.$_checkOwner({ from: user }), 'OwnableUnauthorizedAccount', [ - user, - ]); + await expect(this.ownable.connect(this.user).$_checkOwner()) + .to.be.revertedWithCustomError(this.ownable, 'OwnableUnauthorizedAccount') + .withArgs(this.user.address); }); it('relayed call (with role): success', async function () { - await this.manager.execute(this.ownable.address, selector('$_checkOwner()'), { from: user }); + await this.manager.connect(this.user).execute(this.ownable, this.ownable.$_checkOwner.getFragment().selector); }); it('relayed call (without role): reverts', async function () { - await expectRevertCustomError( - this.manager.execute(this.ownable.address, selector('$_checkOwner()'), { from: other }), - 'AccessManagerUnauthorizedCall', - [other, this.ownable.address, selector('$_checkOwner()')], - ); + await expect( + this.manager.connect(this.other).execute(this.ownable, this.ownable.$_checkOwner.getFragment().selector), + ) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedCall') + .withArgs(this.other.address, this.ownable.target, this.ownable.$_checkOwner.getFragment().selector); }); }); describe('function is open to public role', function () { beforeEach(async function () { await this.manager.$_setTargetFunctionRole( - this.ownable.address, - selector('$_checkOwner()'), + this.ownable, + this.ownable.$_checkOwner.getFragment().selector, this.roles.PUBLIC.id, ); }); it('directly call: reverts', async function () { - await expectRevertCustomError(this.ownable.$_checkOwner({ from: user }), 'OwnableUnauthorizedAccount', [ - user, - ]); + await expect(this.ownable.connect(this.user).$_checkOwner()) + .to.be.revertedWithCustomError(this.ownable, 'OwnableUnauthorizedAccount') + .withArgs(this.user.address); }); it('relayed call (with role): success', async function () { - await this.manager.execute(this.ownable.address, selector('$_checkOwner()'), { from: user }); + await this.manager.connect(this.user).execute(this.ownable, this.ownable.$_checkOwner.getFragment().selector); }); it('relayed call (without role): success', async function () { - await this.manager.execute(this.ownable.address, selector('$_checkOwner()'), { from: other }); + await this.manager + .connect(this.other) + .execute(this.ownable, this.ownable.$_checkOwner.getFragment().selector); }); }); }); diff --git a/test/access/manager/AuthorityUtils.test.js b/test/access/manager/AuthorityUtils.test.js index 3e1133083c3..6c353f20609 100644 --- a/test/access/manager/AuthorityUtils.test.js +++ b/test/access/manager/AuthorityUtils.test.js @@ -1,68 +1,81 @@ -require('@openzeppelin/test-helpers'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { ethers } = require('hardhat'); -const AuthorityUtils = artifacts.require('$AuthorityUtils'); -const NotAuthorityMock = artifacts.require('NotAuthorityMock'); -const AuthorityNoDelayMock = artifacts.require('AuthorityNoDelayMock'); -const AuthorityDelayMock = artifacts.require('AuthorityDelayMock'); -const AuthorityNoResponse = artifacts.require('AuthorityNoResponse'); +async function fixture() { + const [user, other] = await ethers.getSigners(); -contract('AuthorityUtils', function (accounts) { - const [user, other] = accounts; + const mock = await ethers.deployContract('$AuthorityUtils'); + const notAuthorityMock = await ethers.deployContract('NotAuthorityMock'); + const authorityNoDelayMock = await ethers.deployContract('AuthorityNoDelayMock'); + const authorityDelayMock = await ethers.deployContract('AuthorityDelayMock'); + const authorityNoResponse = await ethers.deployContract('AuthorityNoResponse'); + return { + user, + other, + mock, + notAuthorityMock, + authorityNoDelayMock, + authorityDelayMock, + authorityNoResponse, + }; +} + +describe('AuthorityUtils', function () { beforeEach(async function () { - this.mock = await AuthorityUtils.new(); + Object.assign(this, await loadFixture(fixture)); }); describe('canCallWithDelay', function () { describe('when authority does not have a canCall function', function () { beforeEach(async function () { - this.authority = await NotAuthorityMock.new(); + this.authority = this.notAuthorityMock; }); it('returns (immediate = 0, delay = 0)', async function () { const { immediate, delay } = await this.mock.$canCallWithDelay( - this.authority.address, - user, - other, + this.authority, + this.user, + this.other, '0x12345678', ); expect(immediate).to.equal(false); - expect(delay).to.be.bignumber.equal('0'); + expect(delay).to.be.equal(0n); }); }); describe('when authority has no delay', function () { beforeEach(async function () { - this.authority = await AuthorityNoDelayMock.new(); + this.authority = this.authorityNoDelayMock; this.immediate = true; await this.authority._setImmediate(this.immediate); }); it('returns (immediate, delay = 0)', async function () { const { immediate, delay } = await this.mock.$canCallWithDelay( - this.authority.address, - user, - other, + this.authority, + this.user, + this.other, '0x12345678', ); expect(immediate).to.equal(this.immediate); - expect(delay).to.be.bignumber.equal('0'); + expect(delay).to.be.equal(0n); }); }); describe('when authority replies with a delay', function () { beforeEach(async function () { - this.authority = await AuthorityDelayMock.new(); + this.authority = this.authorityDelayMock; }); for (const immediate of [true, false]) { - for (const delay of ['0', '42']) { + for (const delay of [0n, 42n]) { it(`returns (immediate=${immediate}, delay=${delay})`, async function () { await this.authority._setImmediate(immediate); await this.authority._setDelay(delay); - const result = await this.mock.$canCallWithDelay(this.authority.address, user, other, '0x12345678'); + const result = await this.mock.$canCallWithDelay(this.authority, this.user, this.other, '0x12345678'); expect(result.immediate).to.equal(immediate); - expect(result.delay).to.be.bignumber.equal(delay); + expect(result.delay).to.be.equal(delay); }); } } @@ -70,18 +83,18 @@ contract('AuthorityUtils', function (accounts) { describe('when authority replies with empty data', function () { beforeEach(async function () { - this.authority = await AuthorityNoResponse.new(); + this.authority = this.authorityNoResponse; }); it('returns (immediate = 0, delay = 0)', async function () { const { immediate, delay } = await this.mock.$canCallWithDelay( - this.authority.address, - user, - other, + this.authority, + this.user, + this.other, '0x12345678', ); expect(immediate).to.equal(false); - expect(delay).to.be.bignumber.equal('0'); + expect(delay).to.be.equal(0n); }); }); }); diff --git a/test/helpers/access-manager.js b/test/helpers/access-manager.js index beada69be54..5f55dd51876 100644 --- a/test/helpers/access-manager.js +++ b/test/helpers/access-manager.js @@ -1,23 +1,23 @@ -const { time } = require('@openzeppelin/test-helpers'); -const { MAX_UINT64 } = require('./constants'); -const { namespaceSlot } = require('./namespaced-storage'); const { - time: { setNextBlockTimestamp }, -} = require('@nomicfoundation/hardhat-network-helpers'); + bigint: { MAX_UINT64 }, +} = require('./constants'); +const { namespaceSlot } = require('./namespaced-storage'); +const { bigint: time } = require('./time'); +const { keccak256, AbiCoder } = require('ethers'); function buildBaseRoles() { const roles = { ADMIN: { - id: web3.utils.toBN(0), + id: 0n, }, SOME_ADMIN: { - id: web3.utils.toBN(17), + id: 17n, }, SOME_GUARDIAN: { - id: web3.utils.toBN(35), + id: 35n, }, SOME: { - id: web3.utils.toBN(42), + id: 42n, }, PUBLIC: { id: MAX_UINT64, @@ -53,23 +53,27 @@ const CONSUMING_SCHEDULE_STORAGE_SLOT = namespaceSlot('AccessManaged', 0n); /** * @requires this.{manager, caller, target, calldata} */ -async function scheduleOperation(manager, { caller, target, calldata, delay }) { - const timestamp = await time.latest(); - const scheduledAt = timestamp.addn(1); - await setNextBlockTimestamp(scheduledAt); // Fix next block timestamp for predictability - const { receipt } = await manager.schedule(target, calldata, scheduledAt.add(delay), { - from: caller, - }); +async function prepareOperation(manager, { caller, target, calldata, delay }) { + const timestamp = await time.clock.timestamp(); + const scheduledAt = timestamp + 1n; + await time.forward.timestamp(scheduledAt, false); // Fix next block timestamp for predictability return { - receipt, + schedule: () => manager.connect(caller).schedule(target, calldata, scheduledAt + delay), scheduledAt, operationId: hashOperation(caller, target, calldata), }; } +const lazyGetAddress = addressable => addressable.address ?? addressable.target ?? addressable; + const hashOperation = (caller, target, data) => - web3.utils.keccak256(web3.eth.abi.encodeParameters(['address', 'address', 'bytes'], [caller, target, data])); + keccak256( + AbiCoder.defaultAbiCoder().encode( + ['address', 'address', 'bytes'], + [lazyGetAddress(caller), lazyGetAddress(target), data], + ), + ); module.exports = { buildBaseRoles, @@ -78,6 +82,6 @@ module.exports = { EXPIRATION, EXECUTION_ID_STORAGE_SLOT, CONSUMING_SCHEDULE_STORAGE_SLOT, - scheduleOperation, + prepareOperation, hashOperation, }; diff --git a/test/helpers/constants.js b/test/helpers/constants.js index 6a3a82f4f16..17937bee108 100644 --- a/test/helpers/constants.js +++ b/test/helpers/constants.js @@ -1,5 +1,12 @@ +// TODO: deprecate the old version in favor of this one +const bigint = { + MAX_UINT48: 2n ** 48n - 1n, + MAX_UINT64: 2n ** 64n - 1n, +}; + // TODO: remove toString() when bigint are supported module.exports = { - MAX_UINT48: (2n ** 48n - 1n).toString(), - MAX_UINT64: (2n ** 64n - 1n).toString(), + MAX_UINT48: bigint.MAX_UINT48.toString(), + MAX_UINT64: bigint.MAX_UINT64.toString(), + bigint, }; diff --git a/test/helpers/namespaced-storage.js b/test/helpers/namespaced-storage.js index fdd761fdc51..9fa70411363 100644 --- a/test/helpers/namespaced-storage.js +++ b/test/helpers/namespaced-storage.js @@ -1,16 +1,14 @@ +const { keccak256, id, toBeHex, MaxUint256 } = require('ethers'); const { artifacts } = require('hardhat'); function namespaceId(contractName) { return `openzeppelin.storage.${contractName}`; } -function namespaceLocation(id) { - const hashIdBN = web3.utils.toBN(web3.utils.keccak256(id)).subn(1); // keccak256(id) - 1 - const hashIdHex = web3.utils.padLeft(web3.utils.numberToHex(hashIdBN), 64); - - const mask = BigInt(web3.utils.padLeft('0x00', 64, 'f')); // ~0xff - - return BigInt(web3.utils.keccak256(hashIdHex)) & mask; +function namespaceLocation(value) { + const hashIdBN = BigInt(id(value)) - 1n; // keccak256(id) - 1 + const mask = MaxUint256 - 0xffn; // ~0xff + return BigInt(keccak256(toBeHex(hashIdBN, 32))) & mask; } function namespaceSlot(contractName, offset) { From 7de6fd4a2604764497990bcc0013f95763713190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 9 Nov 2023 16:27:40 +0000 Subject: [PATCH 05/44] Close `access-control.adoc` code block (#4726) --- docs/modules/ROOT/pages/access-control.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/modules/ROOT/pages/access-control.adoc b/docs/modules/ROOT/pages/access-control.adoc index b2d6dbbfdde..baf5652f8fe 100644 --- a/docs/modules/ROOT/pages/access-control.adoc +++ b/docs/modules/ROOT/pages/access-control.adoc @@ -190,6 +190,7 @@ await manager.setTargetFunctionRole( ['0x40c10f19'], // bytes4(keccak256('mint(address,uint256)')) MINTER ); +``` Even though each role has its own list of function permissions, each role member (`address`) has an execution delay that will dictate how long the account should wait to execute a function that requires its role. Delayed operations must have the xref:api:access.adoc#AccessManager-schedule-address-bytes-uint48-[`schedule`] function called on them first in the AccessManager before they can be executed, either by calling to the target function or using the AccessManager's xref:api:access.adoc#AccessManager-execute-address-bytes-[`execute`] function. From 7294d34c17ca215c201b3772ff67036fa4b1ef12 Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Fri, 10 Nov 2023 15:15:38 +0000 Subject: [PATCH 06/44] Rename VotesTimestamp to ERC20VotesTimestampMock (#4731) --- .../token/{VotesTimestamp.sol => ERC20VotesTimestampMock.sol} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contracts/mocks/token/{VotesTimestamp.sol => ERC20VotesTimestampMock.sol} (100%) diff --git a/contracts/mocks/token/VotesTimestamp.sol b/contracts/mocks/token/ERC20VotesTimestampMock.sol similarity index 100% rename from contracts/mocks/token/VotesTimestamp.sol rename to contracts/mocks/token/ERC20VotesTimestampMock.sol From 4e419d407cb13c4ebd64d3f47faa78964cbbdd71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Nov 2023 15:48:07 +0000 Subject: [PATCH 07/44] Bump axios from 1.5.1 to 1.6.1 (#4734) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 97546e5b1d1..f96576102aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4946,9 +4946,9 @@ "dev": true }, "node_modules/axios": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", - "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", + "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", "dev": true, "dependencies": { "follow-redirects": "^1.15.0", From 4e17c2e95821b9bff7c2b1e099b078d319142c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 14 Nov 2023 21:40:26 +0000 Subject: [PATCH 08/44] Update SECURITY.md and remove support for 2.x version (#4683) --- SECURITY.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index e9a5148ecdb..9922c45e7a1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,7 +8,7 @@ Security vulnerabilities should be disclosed to the project maintainers through Responsible disclosure of security vulnerabilities is rewarded through a bug bounty program on [Immunefi]. -There is a bonus reward for issues introduced in release candidates that are reported before making it into a stable release. +There is a bonus reward for issues introduced in release candidates that are reported before making it into a stable release. Learn more about release candidates at [`RELEASING.md`](./RELEASING.md). ## Security Patches @@ -30,13 +30,14 @@ Only critical severity bug fixes will be backported to past major releases. | Version | Critical security fixes | Other security fixes | | ------- | ----------------------- | -------------------- | -| 4.x | :white_check_mark: | :white_check_mark: | +| 5.x | :white_check_mark: | :white_check_mark: | +| 4.9 | :white_check_mark: | :x: | | 3.4 | :white_check_mark: | :x: | -| 2.5 | :white_check_mark: | :x: | +| 2.5 | :x: | :x: | | < 2.0 | :x: | :x: | Note as well that the Solidity language itself only guarantees security updates for the latest release. ## Legal -Smart contracts are a nascent technology and carry a high level of technical risk and uncertainty. OpenZeppelin Contracts is made available under the MIT License, which disclaims all warranties in relation to the project and which limits the liability of those that contribute and maintain the project, including OpenZeppelin. Your use of the project is also governed by the terms found at www.openzeppelin.com/tos (the "Terms"). As set out in the Terms, you are solely responsible for any use of OpenZeppelin Contracts and you assume all risks associated with any such use. This Security Policy in no way evidences or represents an on-going duty by any contributor, including OpenZeppelin, to correct any flaws or alert you to all or any of the potential risks of utilizing the project. +Smart contracts are a nascent technology and carry a high level of technical risk and uncertainty. OpenZeppelin Contracts is made available under the MIT License, which disclaims all warranties in relation to the project and which limits the liability of those that contribute and maintain the project, including OpenZeppelin. Your use of the project is also governed by the terms found at www.openzeppelin.com/tos (the "Terms"). As set out in the Terms, you are solely responsible for any use of OpenZeppelin Contracts and you assume all risks associated with any such use. This Security Policy in no way evidences or represents an on-going duty by any contributor, including OpenZeppelin, to correct any flaws or alert you to all or any of the potential risks of utilizing the project. From 6bc1173c8e37ca7de2201a0230bb08e395074da1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 10:34:51 +0300 Subject: [PATCH 09/44] Update dependency @nomicfoundation/hardhat-toolbox to v4 (#4742) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 101 +++++++++++++++++++++++++++++++++------------- package.json | 2 +- 2 files changed, 74 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index f96576102aa..9242999c35c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@nomicfoundation/hardhat-ethers": "^3.0.4", "@nomicfoundation/hardhat-foundry": "^1.1.1", "@nomicfoundation/hardhat-network-helpers": "^1.0.3", - "@nomicfoundation/hardhat-toolbox": "^3.0.0", + "@nomicfoundation/hardhat-toolbox": "^4.0.0", "@nomiclabs/hardhat-truffle5": "^2.0.5", "@nomiclabs/hardhat-web3": "^2.0.0", "@openzeppelin/docs-utils": "^0.1.5", @@ -1613,6 +1613,12 @@ "fs-extra": "^8.1.0" } }, + "node_modules/@manypkg/find-root/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true + }, "node_modules/@manypkg/find-root/node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -2129,34 +2135,34 @@ } }, "node_modules/@nomicfoundation/hardhat-toolbox": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-toolbox/-/hardhat-toolbox-3.0.0.tgz", - "integrity": "sha512-MsteDXd0UagMksqm9KvcFG6gNKYNa3GGNCy73iQ6bEasEgg2v8Qjl6XA5hjs8o5UD5A3153B6W2BIVJ8SxYUtA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-toolbox/-/hardhat-toolbox-4.0.0.tgz", + "integrity": "sha512-jhcWHp0aHaL0aDYj8IJl80v4SZXWMS1A2XxXa1CA6pBiFfJKuZinCkO6wb+POAt0LIfXB3gA3AgdcOccrcwBwA==", "dev": true, "peerDependencies": { "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", "@nomicfoundation/hardhat-ethers": "^3.0.0", "@nomicfoundation/hardhat-network-helpers": "^1.0.0", - "@nomicfoundation/hardhat-verify": "^1.0.0", - "@typechain/ethers-v6": "^0.4.0", - "@typechain/hardhat": "^8.0.0", + "@nomicfoundation/hardhat-verify": "^2.0.0", + "@typechain/ethers-v6": "^0.5.0", + "@typechain/hardhat": "^9.0.0", "@types/chai": "^4.2.0", "@types/mocha": ">=9.1.0", - "@types/node": ">=12.0.0", + "@types/node": ">=16.0.0", "chai": "^4.2.0", "ethers": "^6.4.0", "hardhat": "^2.11.0", "hardhat-gas-reporter": "^1.0.8", "solidity-coverage": "^0.8.1", "ts-node": ">=8.0.0", - "typechain": "^8.2.0", + "typechain": "^8.3.0", "typescript": ">=4.5.0" } }, "node_modules/@nomicfoundation/hardhat-verify": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-verify/-/hardhat-verify-1.1.1.tgz", - "integrity": "sha512-9QsTYD7pcZaQFEA3tBb/D/oCStYDiEVDN7Dxeo/4SCyHRSm86APypxxdOMEPlGmXsAvd+p1j/dTODcpxb8aztA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-verify/-/hardhat-verify-2.0.1.tgz", + "integrity": "sha512-TuJrhW5p9x92wDRiRhNkGQ/wzRmOkfCLkoRg8+IRxyeLigOALbayQEmkNiGWR03vGlxZS4znXhKI7y97JwZ6Og==", "dev": true, "peer": true, "dependencies": { @@ -3253,6 +3259,12 @@ "node": "^16.20 || ^18.16 || >=20" } }, + "node_modules/@truffle/contract/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true + }, "node_modules/@truffle/contract/node_modules/aes-js": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", @@ -3701,6 +3713,12 @@ "node": "^16.20 || ^18.16 || >=20" } }, + "node_modules/@truffle/interface-adapter/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true + }, "node_modules/@truffle/interface-adapter/node_modules/aes-js": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", @@ -4172,9 +4190,9 @@ "peer": true }, "node_modules/@typechain/ethers-v6": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@typechain/ethers-v6/-/ethers-v6-0.4.3.tgz", - "integrity": "sha512-TrxBsyb4ryhaY9keP6RzhFCviWYApcLCIRMPyWaKp2cZZrfaM3QBoxXTnw/eO4+DAY3l+8O0brNW0WgeQeOiDA==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@typechain/ethers-v6/-/ethers-v6-0.5.1.tgz", + "integrity": "sha512-F+GklO8jBWlsaVV+9oHaPh5NJdd6rAKN4tklGfInX1Q7h0xPgVLP39Jl3eCulPB5qexI71ZFHwbljx4ZXNfouA==", "dev": true, "peer": true, "dependencies": { @@ -4183,24 +4201,24 @@ }, "peerDependencies": { "ethers": "6.x", - "typechain": "^8.3.1", + "typechain": "^8.3.2", "typescript": ">=4.7.0" } }, "node_modules/@typechain/hardhat": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@typechain/hardhat/-/hardhat-8.0.3.tgz", - "integrity": "sha512-MytSmJJn+gs7Mqrpt/gWkTCOpOQ6ZDfRrRT2gtZL0rfGe4QrU4x9ZdW15fFbVM/XTa+5EsKiOMYXhRABibNeng==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@typechain/hardhat/-/hardhat-9.1.0.tgz", + "integrity": "sha512-mtaUlzLlkqTlfPwB3FORdejqBskSnh+Jl8AIJGjXNAQfRQ4ofHADPl1+oU7Z3pAJzmZbUXII8MhOLQltcHgKnA==", "dev": true, "peer": true, "dependencies": { "fs-extra": "^9.1.0" }, "peerDependencies": { - "@typechain/ethers-v6": "^0.4.3", + "@typechain/ethers-v6": "^0.5.1", "ethers": "^6.1.0", "hardhat": "^2.9.9", - "typechain": "^8.3.1" + "typechain": "^8.3.2" } }, "node_modules/@typechain/hardhat/node_modules/fs-extra": { @@ -4366,10 +4384,13 @@ "peer": true }, "node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true + "version": "20.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", + "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -15613,9 +15634,9 @@ } }, "node_modules/typechain": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.1.tgz", - "integrity": "sha512-fA7clol2IP/56yq6vkMTR+4URF1nGjV82Wx6Rf09EsqD4tkzMAvEaqYxVFCavJm/1xaRga/oD55K+4FtuXwQOQ==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.2.tgz", + "integrity": "sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==", "dev": true, "peer": true, "dependencies": { @@ -15843,6 +15864,12 @@ "node": ">=14.0" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -16036,6 +16063,12 @@ "node": ">=8.0.0" } }, + "node_modules/web3-bzz/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true + }, "node_modules/web3-core": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.10.2.tgz", @@ -16256,6 +16289,12 @@ "node": ">=8.0.0" } }, + "node_modules/web3-core/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true + }, "node_modules/web3-core/node_modules/bignumber.js": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", @@ -16590,6 +16629,12 @@ "node": ">=8.0.0" } }, + "node_modules/web3-eth-personal/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true + }, "node_modules/web3-eth-personal/node_modules/bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", diff --git a/package.json b/package.json index e5265dc5106..c2c3a26750e 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@nomicfoundation/hardhat-ethers": "^3.0.4", "@nomicfoundation/hardhat-foundry": "^1.1.1", "@nomicfoundation/hardhat-network-helpers": "^1.0.3", - "@nomicfoundation/hardhat-toolbox": "^3.0.0", + "@nomicfoundation/hardhat-toolbox": "^4.0.0", "@nomiclabs/hardhat-truffle5": "^2.0.5", "@nomiclabs/hardhat-web3": "^2.0.0", "@openzeppelin/docs-utils": "^0.1.5", From e473bcf859e1ac4b6fd736d8e29b462a6192705c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 23 Nov 2023 03:24:21 +0100 Subject: [PATCH 10/44] Migrate metatx tests to ethers.js (#4737) Co-authored-by: ernestognw --- test/helpers/account.js | 8 +- test/helpers/math.js | 1 + test/metatx/ERC2771Context.test.js | 155 +++--- test/metatx/ERC2771Forwarder.test.js | 678 ++++++++++++--------------- test/utils/Context.behavior.js | 48 +- test/utils/Context.test.js | 19 +- 6 files changed, 416 insertions(+), 493 deletions(-) diff --git a/test/helpers/account.js b/test/helpers/account.js index 8c0ea130b41..96874b16b75 100644 --- a/test/helpers/account.js +++ b/test/helpers/account.js @@ -4,10 +4,10 @@ const { impersonateAccount, setBalance } = require('@nomicfoundation/hardhat-net // Hardhat default balance const DEFAULT_BALANCE = 10000n * ethers.WeiPerEther; -async function impersonate(account, balance = DEFAULT_BALANCE) { - await impersonateAccount(account); - await setBalance(account, balance); -} +const impersonate = (account, balance = DEFAULT_BALANCE) => + impersonateAccount(account) + .then(() => setBalance(account, balance)) + .then(() => ethers.getSigner(account)); module.exports = { impersonate, diff --git a/test/helpers/math.js b/test/helpers/math.js index 2bc654c514a..134f8b04509 100644 --- a/test/helpers/math.js +++ b/test/helpers/math.js @@ -1,6 +1,7 @@ module.exports = { // sum of integer / bignumber sum: (...args) => args.reduce((acc, n) => acc + n, 0), + bigintSum: (...args) => args.reduce((acc, n) => acc + n, 0n), BNsum: (...args) => args.reduce((acc, n) => acc.add(n), web3.utils.toBN(0)), // min of integer / bignumber min: (...args) => args.slice(1).reduce((x, y) => (x < y ? x : y), args[0]), diff --git a/test/metatx/ERC2771Context.test.js b/test/metatx/ERC2771Context.test.js index b0ebccca809..bb6718ce229 100644 --- a/test/metatx/ERC2771Context.test.js +++ b/test/metatx/ERC2771Context.test.js @@ -1,134 +1,117 @@ -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; -const { getDomain, domainType } = require('../helpers/eip712'); -const { MAX_UINT48 } = require('../helpers/constants'); - -const { expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const ERC2771ContextMock = artifacts.require('ERC2771ContextMock'); -const ERC2771Forwarder = artifacts.require('ERC2771Forwarder'); -const ContextMockCaller = artifacts.require('ContextMockCaller'); +const { impersonate } = require('../helpers/account'); +const { getDomain } = require('../helpers/eip712'); +const { MAX_UINT48 } = require('../helpers/constants'); const { shouldBehaveLikeRegularContext } = require('../utils/Context.behavior'); -contract('ERC2771Context', function (accounts) { - const [, trustedForwarder] = accounts; - +async function fixture() { + const [sender] = await ethers.getSigners(); + + const forwarder = await ethers.deployContract('ERC2771Forwarder', []); + const forwarderAsSigner = await impersonate(forwarder.target); + const context = await ethers.deployContract('ERC2771ContextMock', [forwarder]); + const domain = await getDomain(forwarder); + const types = { + ForwardRequest: [ + { name: 'from', type: 'address' }, + { name: 'to', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'gas', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint48' }, + { name: 'data', type: 'bytes' }, + ], + }; + + return { sender, forwarder, forwarderAsSigner, context, domain, types }; +} + +describe('ERC2771Context', function () { beforeEach(async function () { - this.forwarder = await ERC2771Forwarder.new('ERC2771Forwarder'); - this.recipient = await ERC2771ContextMock.new(this.forwarder.address); - - this.domain = await getDomain(this.forwarder); - this.types = { - EIP712Domain: domainType(this.domain), - ForwardRequest: [ - { name: 'from', type: 'address' }, - { name: 'to', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'gas', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint48' }, - { name: 'data', type: 'bytes' }, - ], - }; + Object.assign(this, await loadFixture(fixture)); }); it('recognize trusted forwarder', async function () { - expect(await this.recipient.isTrustedForwarder(this.forwarder.address)).to.equal(true); + expect(await this.context.isTrustedForwarder(this.forwarder)).to.equal(true); }); it('returns the trusted forwarder', async function () { - expect(await this.recipient.trustedForwarder()).to.equal(this.forwarder.address); + expect(await this.context.trustedForwarder()).to.equal(this.forwarder.target); }); - context('when called directly', function () { - beforeEach(async function () { - this.context = this.recipient; // The Context behavior expects the contract in this.context - this.caller = await ContextMockCaller.new(); - }); - - shouldBehaveLikeRegularContext(...accounts); + describe('when called directly', function () { + shouldBehaveLikeRegularContext(); }); - context('when receiving a relayed call', function () { - beforeEach(async function () { - this.wallet = Wallet.generate(); - this.sender = web3.utils.toChecksumAddress(this.wallet.getAddressString()); - this.data = { - types: this.types, - domain: this.domain, - primaryType: 'ForwardRequest', - }; - }); - + describe('when receiving a relayed call', function () { describe('msgSender', function () { it('returns the relayed transaction original sender', async function () { - const data = this.recipient.contract.methods.msgSender().encodeABI(); + const nonce = await this.forwarder.nonces(this.sender); + const data = this.context.interface.encodeFunctionData('msgSender'); const req = { - from: this.sender, - to: this.recipient.address, - value: '0', - gas: '100000', - nonce: (await this.forwarder.nonces(this.sender)).toString(), - deadline: MAX_UINT48, + from: await this.sender.getAddress(), + to: await this.context.getAddress(), + value: 0n, data, + gas: 100000n, + nonce, + deadline: MAX_UINT48, }; - req.signature = ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), { - data: { ...this.data, message: req }, - }); + req.signature = await this.sender.signTypedData(this.domain, this.types, req); + expect(await this.forwarder.verify(req)).to.equal(true); - const { tx } = await this.forwarder.execute(req); - await expectEvent.inTransaction(tx, ERC2771ContextMock, 'Sender', { sender: this.sender }); + await expect(this.forwarder.execute(req)).to.emit(this.context, 'Sender').withArgs(this.sender.address); }); it('returns the original sender when calldata length is less than 20 bytes (address length)', async function () { - // The forwarder doesn't produce calls with calldata length less than 20 bytes - const recipient = await ERC2771ContextMock.new(trustedForwarder); - - const { receipt } = await recipient.msgSender({ from: trustedForwarder }); - - await expectEvent(receipt, 'Sender', { sender: trustedForwarder }); + // The forwarder doesn't produce calls with calldata length less than 20 bytes so `this.forwarderAsSigner` is used instead. + await expect(this.context.connect(this.forwarderAsSigner).msgSender()) + .to.emit(this.context, 'Sender') + .withArgs(this.forwarder.target); }); }); describe('msgData', function () { it('returns the relayed transaction original data', async function () { - const integerValue = '42'; - const stringValue = 'OpenZeppelin'; - const data = this.recipient.contract.methods.msgData(integerValue, stringValue).encodeABI(); + const args = [42n, 'OpenZeppelin']; + + const nonce = await this.forwarder.nonces(this.sender); + const data = this.context.interface.encodeFunctionData('msgData', args); const req = { - from: this.sender, - to: this.recipient.address, - value: '0', - gas: '100000', - nonce: (await this.forwarder.nonces(this.sender)).toString(), - deadline: MAX_UINT48, + from: await this.sender.getAddress(), + to: await this.context.getAddress(), + value: 0n, data, + gas: 100000n, + nonce, + deadline: MAX_UINT48, }; - req.signature = ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), { - data: { ...this.data, message: req }, - }); + req.signature = this.sender.signTypedData(this.domain, this.types, req); + expect(await this.forwarder.verify(req)).to.equal(true); - const { tx } = await this.forwarder.execute(req); - await expectEvent.inTransaction(tx, ERC2771ContextMock, 'Data', { data, integerValue, stringValue }); + await expect(this.forwarder.execute(req)) + .to.emit(this.context, 'Data') + .withArgs(data, ...args); }); }); it('returns the full original data when calldata length is less than 20 bytes (address length)', async function () { - // The forwarder doesn't produce calls with calldata length less than 20 bytes - const recipient = await ERC2771ContextMock.new(trustedForwarder); - - const { receipt } = await recipient.msgDataShort({ from: trustedForwarder }); + const data = this.context.interface.encodeFunctionData('msgDataShort'); - const data = recipient.contract.methods.msgDataShort().encodeABI(); - await expectEvent(receipt, 'DataShort', { data }); + // The forwarder doesn't produce calls with calldata length less than 20 bytes so `this.forwarderAsSigner` is used instead. + await expect(await this.context.connect(this.forwarderAsSigner).msgDataShort()) + .to.emit(this.context, 'DataShort') + .withArgs(data); }); }); }); diff --git a/test/metatx/ERC2771Forwarder.test.js b/test/metatx/ERC2771Forwarder.test.js index 0e0998832ff..a665471f358 100644 --- a/test/metatx/ERC2771Forwarder.test.js +++ b/test/metatx/ERC2771Forwarder.test.js @@ -1,245 +1,217 @@ -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; -const { getDomain, domainType } = require('../helpers/eip712'); -const { expectRevertCustomError } = require('../helpers/customError'); - -const { constants, expectRevert, expectEvent, time } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); - -const ERC2771Forwarder = artifacts.require('ERC2771Forwarder'); -const CallReceiverMockTrustingForwarder = artifacts.require('CallReceiverMockTrustingForwarder'); - -contract('ERC2771Forwarder', function (accounts) { - const [, refundReceiver, another] = accounts; - - const tamperedValues = { - from: another, - value: web3.utils.toWei('0.5'), - data: '0x1742', +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { getDomain } = require('../helpers/eip712'); +const { bigint: time } = require('../helpers/time'); +const { bigintSum: sum } = require('../helpers/math'); + +async function fixture() { + const [sender, refundReceiver, another, ...accounts] = await ethers.getSigners(); + + const forwarder = await ethers.deployContract('ERC2771Forwarder', ['ERC2771Forwarder']); + const receiver = await ethers.deployContract('CallReceiverMockTrustingForwarder', [forwarder]); + const domain = await getDomain(forwarder); + const types = { + ForwardRequest: [ + { name: 'from', type: 'address' }, + { name: 'to', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'gas', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint48' }, + { name: 'data', type: 'bytes' }, + ], }; - beforeEach(async function () { - this.forwarder = await ERC2771Forwarder.new('ERC2771Forwarder'); - - this.domain = await getDomain(this.forwarder); - this.types = { - EIP712Domain: domainType(this.domain), - ForwardRequest: [ - { name: 'from', type: 'address' }, - { name: 'to', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'gas', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint48' }, - { name: 'data', type: 'bytes' }, - ], - }; - - this.alice = Wallet.generate(); - this.alice.address = web3.utils.toChecksumAddress(this.alice.getAddressString()); - - this.timestamp = await time.latest(); - this.receiver = await CallReceiverMockTrustingForwarder.new(this.forwarder.address); - this.request = { - from: this.alice.address, - to: this.receiver.address, - value: '0', - gas: '100000', - data: this.receiver.contract.methods.mockFunction().encodeABI(), - deadline: this.timestamp.toNumber() + 60, // 1 minute - }; - this.requestData = { - ...this.request, - nonce: (await this.forwarder.nonces(this.alice.address)).toString(), + const forgeRequest = async (override = {}, signer = sender) => { + const req = { + from: await signer.getAddress(), + to: await receiver.getAddress(), + value: 0n, + data: receiver.interface.encodeFunctionData('mockFunction'), + gas: 100000n, + deadline: (await time.clock.timestamp()) + 60n, + nonce: await forwarder.nonces(sender), + ...override, }; + req.signature = await signer.signTypedData(domain, types, req); + return req; + }; - this.forgeData = request => ({ - types: this.types, - domain: this.domain, - primaryType: 'ForwardRequest', - message: { ...this.requestData, ...request }, + const estimateRequest = request => + ethers.provider.estimateGas({ + from: forwarder, + to: request.to, + data: ethers.solidityPacked(['bytes', 'address'], [request.data, request.from]), + value: request.value, + gasLimit: request.gas, }); - this.sign = (privateKey, request) => - ethSigUtil.signTypedMessage(privateKey, { - data: this.forgeData(request), - }); - this.estimateRequest = request => - web3.eth.estimateGas({ - from: this.forwarder.address, - to: request.to, - data: web3.utils.encodePacked({ value: request.data, type: 'bytes' }, { value: request.from, type: 'address' }), - value: request.value, - gas: request.gas, - }); - this.requestData.signature = this.sign(this.alice.getPrivateKey()); + return { + sender, + refundReceiver, + another, + accounts, + forwarder, + receiver, + forgeRequest, + estimateRequest, + domain, + types, + }; +} + +// values or function to tamper with a signed request. +const tamperedValues = { + from: ethers.Wallet.createRandom().address, + to: ethers.Wallet.createRandom().address, + value: ethers.parseEther('0.5'), + data: '0x1742', + signature: s => { + const t = ethers.toBeArray(s); + t[42] ^= 0xff; + return ethers.hexlify(t); + }, +}; + +describe('ERC2771Forwarder', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); }); - context('verify', function () { - context('with valid signature', function () { + describe('verify', function () { + describe('with valid signature', function () { it('returns true without altering the nonce', async function () { - expect(await this.forwarder.nonces(this.requestData.from)).to.be.bignumber.equal( - web3.utils.toBN(this.requestData.nonce), - ); - expect(await this.forwarder.verify(this.requestData)).to.be.equal(true); - expect(await this.forwarder.nonces(this.requestData.from)).to.be.bignumber.equal( - web3.utils.toBN(this.requestData.nonce), - ); + const request = await this.forgeRequest(); + expect(await this.forwarder.nonces(request.from)).to.be.equal(request.nonce); + expect(await this.forwarder.verify(request)).to.be.equal(true); + expect(await this.forwarder.nonces(request.from)).to.be.equal(request.nonce); }); }); - context('with tampered values', function () { + describe('with tampered values', function () { for (const [key, value] of Object.entries(tamperedValues)) { it(`returns false with tampered ${key}`, async function () { - expect(await this.forwarder.verify(this.forgeData({ [key]: value }).message)).to.be.equal(false); + const request = await this.forgeRequest(); + request[key] = typeof value == 'function' ? value(request[key]) : value; + + expect(await this.forwarder.verify(request)).to.be.equal(false); }); } - it('returns false with an untrustful to', async function () { - expect(await this.forwarder.verify(this.forgeData({ to: another }).message)).to.be.equal(false); - }); - - it('returns false with tampered signature', async function () { - const tamperedsign = web3.utils.hexToBytes(this.requestData.signature); - tamperedsign[42] ^= 0xff; - this.requestData.signature = web3.utils.bytesToHex(tamperedsign); - expect(await this.forwarder.verify(this.requestData)).to.be.equal(false); - }); - it('returns false with valid signature for non-current nonce', async function () { - const req = { - ...this.requestData, - nonce: this.requestData.nonce + 1, - }; - req.signature = this.sign(this.alice.getPrivateKey(), req); - expect(await this.forwarder.verify(req)).to.be.equal(false); + const request = await this.forgeRequest({ nonce: 1337n }); + expect(await this.forwarder.verify(request)).to.be.equal(false); }); it('returns false with valid signature for expired deadline', async function () { - const req = { - ...this.requestData, - deadline: this.timestamp - 1, - }; - req.signature = this.sign(this.alice.getPrivateKey(), req); - expect(await this.forwarder.verify(req)).to.be.equal(false); + const request = await this.forgeRequest({ deadline: (await time.clock.timestamp()) - 1n }); + expect(await this.forwarder.verify(request)).to.be.equal(false); }); }); }); - context('execute', function () { - context('with valid requests', function () { - beforeEach(async function () { - expect(await this.forwarder.nonces(this.requestData.from)).to.be.bignumber.equal( - web3.utils.toBN(this.requestData.nonce), - ); - }); - + describe('execute', function () { + describe('with valid requests', function () { it('emits an event and consumes nonce for a successful request', async function () { - const receipt = await this.forwarder.execute(this.requestData); - await expectEvent.inTransaction(receipt.tx, this.receiver, 'MockFunctionCalled'); - await expectEvent.inTransaction(receipt.tx, this.forwarder, 'ExecutedForwardRequest', { - signer: this.requestData.from, - nonce: web3.utils.toBN(this.requestData.nonce), - success: true, - }); - expect(await this.forwarder.nonces(this.requestData.from)).to.be.bignumber.equal( - web3.utils.toBN(this.requestData.nonce + 1), - ); + const request = await this.forgeRequest(); + + expect(await this.forwarder.nonces(request.from)).to.equal(request.nonce); + + await expect(this.forwarder.execute(request)) + .to.emit(this.receiver, 'MockFunctionCalled') + .to.emit(this.forwarder, 'ExecutedForwardRequest') + .withArgs(request.from, request.nonce, true); + + expect(await this.forwarder.nonces(request.from)).to.be.equal(request.nonce + 1n); }); it('reverts with an unsuccessful request', async function () { - const req = { - ...this.requestData, - data: this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(), - }; - req.signature = this.sign(this.alice.getPrivateKey(), req); - await expectRevertCustomError(this.forwarder.execute(req), 'FailedInnerCall', []); + const request = await this.forgeRequest({ + data: this.receiver.interface.encodeFunctionData('mockFunctionRevertsNoReason'), + }); + + await expect(this.forwarder.execute(request)).to.be.revertedWithCustomError(this.forwarder, 'FailedInnerCall'); }); }); - context('with tampered request', function () { + describe('with tampered request', function () { for (const [key, value] of Object.entries(tamperedValues)) { it(`reverts with tampered ${key}`, async function () { - const data = this.forgeData({ [key]: value }); - await expectRevertCustomError( - this.forwarder.execute(data.message, { - value: key == 'value' ? value : 0, // To avoid MismatchedValue error - }), - 'ERC2771ForwarderInvalidSigner', - [ethSigUtil.recoverTypedSignature({ data, sig: this.requestData.signature }), data.message.from], - ); + const request = await this.forgeRequest(); + request[key] = typeof value == 'function' ? value(request[key]) : value; + + const promise = this.forwarder.execute(request, { value: key == 'value' ? value : 0 }); + if (key != 'to') { + await expect(promise) + .to.be.revertedWithCustomError(this.forwarder, 'ERC2771ForwarderInvalidSigner') + .withArgs(ethers.verifyTypedData(this.domain, this.types, request, request.signature), request.from); + } else { + await expect(promise) + .to.be.revertedWithCustomError(this.forwarder, 'ERC2771UntrustfulTarget') + .withArgs(request.to, this.forwarder.target); + } }); } - it('reverts with an untrustful to', async function () { - const data = this.forgeData({ to: another }); - await expectRevertCustomError(this.forwarder.execute(data.message), 'ERC2771UntrustfulTarget', [ - data.message.to, - this.forwarder.address, - ]); - }); - - it('reverts with tampered signature', async function () { - const tamperedSig = web3.utils.hexToBytes(this.requestData.signature); - tamperedSig[42] ^= 0xff; - this.requestData.signature = web3.utils.bytesToHex(tamperedSig); - await expectRevertCustomError(this.forwarder.execute(this.requestData), 'ERC2771ForwarderInvalidSigner', [ - ethSigUtil.recoverTypedSignature({ data: this.forgeData(), sig: tamperedSig }), - this.requestData.from, - ]); - }); - it('reverts with valid signature for non-current nonce', async function () { - // Execute first a request - await this.forwarder.execute(this.requestData); - - // And then fail due to an already used nonce - await expectRevertCustomError(this.forwarder.execute(this.requestData), 'ERC2771ForwarderInvalidSigner', [ - ethSigUtil.recoverTypedSignature({ - data: this.forgeData({ ...this.requestData, nonce: this.requestData.nonce + 1 }), - sig: this.requestData.signature, - }), - this.requestData.from, - ]); + const request = await this.forgeRequest(); + + // consume nonce + await this.forwarder.execute(request); + + // nonce has changed + await expect(this.forwarder.execute(request)) + .to.be.revertedWithCustomError(this.forwarder, 'ERC2771ForwarderInvalidSigner') + .withArgs( + ethers.verifyTypedData( + this.domain, + this.types, + { ...request, nonce: request.nonce + 1n }, + request.signature, + ), + request.from, + ); }); it('reverts with valid signature for expired deadline', async function () { - const req = { - ...this.requestData, - deadline: this.timestamp - 1, - }; - req.signature = this.sign(this.alice.getPrivateKey(), req); - await expectRevertCustomError(this.forwarder.execute(req), 'ERC2771ForwarderExpiredRequest', [ - this.timestamp - 1, - ]); + const request = await this.forgeRequest({ deadline: (await time.clock.timestamp()) - 1n }); + + await expect(this.forwarder.execute(request)) + .to.be.revertedWithCustomError(this.forwarder, 'ERC2771ForwarderExpiredRequest') + .withArgs(request.deadline); }); it('reverts with valid signature but mismatched value', async function () { - const value = 100; - const req = { - ...this.requestData, - value, - }; - req.signature = this.sign(this.alice.getPrivateKey(), req); - await expectRevertCustomError(this.forwarder.execute(req), 'ERC2771ForwarderMismatchedValue', [0, value]); + const request = await this.forgeRequest({ value: 100n }); + + await expect(this.forwarder.execute(request)) + .to.be.revertedWithCustomError(this.forwarder, 'ERC2771ForwarderMismatchedValue') + .withArgs(request.value, 0n); }); }); it('bubbles out of gas', async function () { - this.requestData.data = this.receiver.contract.methods.mockFunctionOutOfGas().encodeABI(); - this.requestData.gas = 1_000_000; - this.requestData.signature = this.sign(this.alice.getPrivateKey()); + const request = await this.forgeRequest({ + data: this.receiver.interface.encodeFunctionData('mockFunctionOutOfGas'), + gas: 1_000_000n, + }); - const gasAvailable = 100_000; - await expectRevert.assertion(this.forwarder.execute(this.requestData, { gas: gasAvailable })); + const gasLimit = 100_000n; + await expect(this.forwarder.execute(request, { gasLimit })).to.be.revertedWithoutReason(); - const { transactions } = await web3.eth.getBlock('latest'); - const { gasUsed } = await web3.eth.getTransactionReceipt(transactions[0]); + const { gasUsed } = await ethers.provider + .getBlock('latest') + .then(block => block.getTransaction(0)) + .then(tx => ethers.provider.getTransactionReceipt(tx.hash)); - expect(gasUsed).to.be.equal(gasAvailable); + expect(gasUsed).to.be.equal(gasLimit); }); it('bubbles out of gas forced by the relayer', async function () { + const request = await this.forgeRequest(); + // If there's an incentive behind executing requests, a malicious relayer could grief // the forwarder by executing requests and providing a top-level call gas limit that // is too low to successfully finish the request after the 63/64 rule. @@ -247,294 +219,252 @@ contract('ERC2771Forwarder', function (accounts) { // We set the baseline to the gas limit consumed by a successful request if it was executed // normally. Note this includes the 21000 buffer that also the relayer will be charged to // start a request execution. - const estimate = await this.estimateRequest(this.request); + const estimate = await this.estimateRequest(request); // Because the relayer call consumes gas until the `CALL` opcode, the gas left after failing // the subcall won't enough to finish the top level call (after testing), so we add a // moderated buffer. - const gasAvailable = estimate + 2_000; + const gasLimit = estimate + 2_000n; // The subcall out of gas should be caught by the contract and then bubbled up consuming // the available gas with an `invalid` opcode. - await expectRevert.outOfGas(this.forwarder.execute(this.requestData, { gas: gasAvailable })); + await expect(this.forwarder.execute(request, { gasLimit })).to.be.revertedWithoutReason(); - const { transactions } = await web3.eth.getBlock('latest'); - const { gasUsed } = await web3.eth.getTransactionReceipt(transactions[0]); + const { gasUsed } = await ethers.provider + .getBlock('latest') + .then(block => block.getTransaction(0)) + .then(tx => ethers.provider.getTransactionReceipt(tx.hash)); // We assert that indeed the gas was totally consumed. - expect(gasUsed).to.be.equal(gasAvailable); + expect(gasUsed).to.be.equal(gasLimit); }); }); - context('executeBatch', function () { - const batchValue = requestDatas => requestDatas.reduce((value, request) => value + Number(request.value), 0); + describe('executeBatch', function () { + const requestsValue = requests => sum(...requests.map(request => request.value)); + const requestCount = 3; + const idx = 1; // index that will be tampered with beforeEach(async function () { - this.bob = Wallet.generate(); - this.bob.address = web3.utils.toChecksumAddress(this.bob.getAddressString()); - - this.eve = Wallet.generate(); - this.eve.address = web3.utils.toChecksumAddress(this.eve.getAddressString()); - - this.signers = [this.alice, this.bob, this.eve]; - - this.requestDatas = await Promise.all( - this.signers.map(async ({ address }) => ({ - ...this.requestData, - from: address, - nonce: (await this.forwarder.nonces(address)).toString(), - value: web3.utils.toWei('10', 'gwei'), - })), - ); - - this.requestDatas = this.requestDatas.map((requestData, i) => ({ - ...requestData, - signature: this.sign(this.signers[i].getPrivateKey(), requestData), - })); - - this.msgValue = batchValue(this.requestDatas); - - this.gasUntil = async reqIdx => { - const gas = 0; - const estimations = await Promise.all( - new Array(reqIdx + 1).fill().map((_, idx) => this.estimateRequest(this.requestDatas[idx])), - ); - return estimations.reduce((acc, estimation) => acc + estimation, gas); - }; + this.forgeRequests = override => + Promise.all(this.accounts.slice(0, requestCount).map(signer => this.forgeRequest(override, signer))); + this.requests = await this.forgeRequests({ value: 10n }); + this.value = requestsValue(this.requests); }); - context('with valid requests', function () { - beforeEach(async function () { - for (const request of this.requestDatas) { + describe('with valid requests', function () { + it('sanity', async function () { + for (const request of this.requests) { expect(await this.forwarder.verify(request)).to.be.equal(true); } - - this.receipt = await this.forwarder.executeBatch(this.requestDatas, another, { value: this.msgValue }); }); it('emits events', async function () { - for (const request of this.requestDatas) { - await expectEvent.inTransaction(this.receipt.tx, this.receiver, 'MockFunctionCalled'); - await expectEvent.inTransaction(this.receipt.tx, this.forwarder, 'ExecutedForwardRequest', { - signer: request.from, - nonce: web3.utils.toBN(request.nonce), - success: true, - }); + const receipt = this.forwarder.executeBatch(this.requests, this.another, { value: this.value }); + + for (const request of this.requests) { + await expect(receipt) + .to.emit(this.receiver, 'MockFunctionCalled') + .to.emit(this.forwarder, 'ExecutedForwardRequest') + .withArgs(request.from, request.nonce, true); } }); it('increase nonces', async function () { - for (const request of this.requestDatas) { - expect(await this.forwarder.nonces(request.from)).to.be.bignumber.eq(web3.utils.toBN(request.nonce + 1)); + await this.forwarder.executeBatch(this.requests, this.another, { value: this.value }); + + for (const request of this.requests) { + expect(await this.forwarder.nonces(request.from)).to.be.equal(request.nonce + 1n); } }); }); - context('with tampered requests', function () { - beforeEach(async function () { - this.idx = 1; // Tampered idx - }); - + describe('with tampered requests', function () { it('reverts with mismatched value', async function () { - this.requestDatas[this.idx].value = 100; - this.requestDatas[this.idx].signature = this.sign( - this.signers[this.idx].getPrivateKey(), - this.requestDatas[this.idx], - ); - await expectRevertCustomError( - this.forwarder.executeBatch(this.requestDatas, another, { value: this.msgValue }), - 'ERC2771ForwarderMismatchedValue', - [batchValue(this.requestDatas), this.msgValue], - ); + // tamper value of one of the request + resign + this.requests[idx] = await this.forgeRequest({ value: 100n }, this.accounts[1]); + + await expect(this.forwarder.executeBatch(this.requests, this.another, { value: this.value })) + .to.be.revertedWithCustomError(this.forwarder, 'ERC2771ForwarderMismatchedValue') + .withArgs(requestsValue(this.requests), this.value); }); - context('when the refund receiver is the zero address', function () { + describe('when the refund receiver is the zero address', function () { beforeEach(function () { - this.refundReceiver = constants.ZERO_ADDRESS; + this.refundReceiver = ethers.ZeroAddress; }); for (const [key, value] of Object.entries(tamperedValues)) { it(`reverts with at least one tampered request ${key}`, async function () { - const data = this.forgeData({ ...this.requestDatas[this.idx], [key]: value }); - - this.requestDatas[this.idx] = data.message; - - await expectRevertCustomError( - this.forwarder.executeBatch(this.requestDatas, this.refundReceiver, { value: this.msgValue }), - 'ERC2771ForwarderInvalidSigner', - [ - ethSigUtil.recoverTypedSignature({ data, sig: this.requestDatas[this.idx].signature }), - data.message.from, - ], - ); + this.requests[idx][key] = typeof value == 'function' ? value(this.requests[idx][key]) : value; + + const promise = this.forwarder.executeBatch(this.requests, this.refundReceiver, { value: this.value }); + if (key != 'to') { + await expect(promise) + .to.be.revertedWithCustomError(this.forwarder, 'ERC2771ForwarderInvalidSigner') + .withArgs( + ethers.verifyTypedData(this.domain, this.types, this.requests[idx], this.requests[idx].signature), + this.requests[idx].from, + ); + } else { + await expect(promise) + .to.be.revertedWithCustomError(this.forwarder, 'ERC2771UntrustfulTarget') + .withArgs(this.requests[idx].to, this.forwarder.target); + } }); } - it('reverts with at least one untrustful to', async function () { - const data = this.forgeData({ ...this.requestDatas[this.idx], to: another }); - - this.requestDatas[this.idx] = data.message; - - await expectRevertCustomError( - this.forwarder.executeBatch(this.requestDatas, this.refundReceiver, { value: this.msgValue }), - 'ERC2771UntrustfulTarget', - [this.requestDatas[this.idx].to, this.forwarder.address], - ); - }); - - it('reverts with at least one tampered request signature', async function () { - const tamperedSig = web3.utils.hexToBytes(this.requestDatas[this.idx].signature); - tamperedSig[42] ^= 0xff; - - this.requestDatas[this.idx].signature = web3.utils.bytesToHex(tamperedSig); - - await expectRevertCustomError( - this.forwarder.executeBatch(this.requestDatas, this.refundReceiver, { value: this.msgValue }), - 'ERC2771ForwarderInvalidSigner', - [ - ethSigUtil.recoverTypedSignature({ - data: this.forgeData(this.requestDatas[this.idx]), - sig: this.requestDatas[this.idx].signature, - }), - this.requestDatas[this.idx].from, - ], - ); - }); - it('reverts with at least one valid signature for non-current nonce', async function () { // Execute first a request - await this.forwarder.execute(this.requestDatas[this.idx], { value: this.requestDatas[this.idx].value }); + await this.forwarder.execute(this.requests[idx], { value: this.requests[idx].value }); // And then fail due to an already used nonce - await expectRevertCustomError( - this.forwarder.executeBatch(this.requestDatas, this.refundReceiver, { value: this.msgValue }), - 'ERC2771ForwarderInvalidSigner', - [ - ethSigUtil.recoverTypedSignature({ - data: this.forgeData({ ...this.requestDatas[this.idx], nonce: this.requestDatas[this.idx].nonce + 1 }), - sig: this.requestDatas[this.idx].signature, - }), - this.requestDatas[this.idx].from, - ], - ); + await expect(this.forwarder.executeBatch(this.requests, this.refundReceiver, { value: this.value })) + .to.be.revertedWithCustomError(this.forwarder, 'ERC2771ForwarderInvalidSigner') + .withArgs( + ethers.verifyTypedData( + this.domain, + this.types, + { ...this.requests[idx], nonce: this.requests[idx].nonce + 1n }, + this.requests[idx].signature, + ), + this.requests[idx].from, + ); }); it('reverts with at least one valid signature for expired deadline', async function () { - this.requestDatas[this.idx].deadline = this.timestamp.toNumber() - 1; - this.requestDatas[this.idx].signature = this.sign( - this.signers[this.idx].getPrivateKey(), - this.requestDatas[this.idx], - ); - await expectRevertCustomError( - this.forwarder.executeBatch(this.requestDatas, this.refundReceiver, { value: this.msgValue }), - 'ERC2771ForwarderExpiredRequest', - [this.timestamp.toNumber() - 1], + this.requests[idx] = await this.forgeRequest( + { ...this.requests[idx], deadline: (await time.clock.timestamp()) - 1n }, + this.accounts[1], ); + + await expect(this.forwarder.executeBatch(this.requests, this.refundReceiver, { value: this.amount })) + .to.be.revertedWithCustomError(this.forwarder, 'ERC2771ForwarderExpiredRequest') + .withArgs(this.requests[idx].deadline); }); }); - context('when the refund receiver is a known address', function () { + describe('when the refund receiver is a known address', function () { beforeEach(async function () { - this.refundReceiver = refundReceiver; - this.initialRefundReceiverBalance = web3.utils.toBN(await web3.eth.getBalance(this.refundReceiver)); - this.initialTamperedRequestNonce = await this.forwarder.nonces(this.requestDatas[this.idx].from); + this.initialRefundReceiverBalance = await ethers.provider.getBalance(this.refundReceiver); + this.initialTamperedRequestNonce = await this.forwarder.nonces(this.requests[idx].from); }); for (const [key, value] of Object.entries(tamperedValues)) { it(`ignores a request with tampered ${key} and refunds its value`, async function () { - const data = this.forgeData({ ...this.requestDatas[this.idx], [key]: value }); - - this.requestDatas[this.idx] = data.message; - - const receipt = await this.forwarder.executeBatch(this.requestDatas, this.refundReceiver, { - value: batchValue(this.requestDatas), - }); - expect(receipt.logs.filter(({ event }) => event === 'ExecutedForwardRequest').length).to.be.equal(2); + this.requests[idx][key] = typeof value == 'function' ? value(this.requests[idx][key]) : value; + + const events = await this.forwarder + .executeBatch(this.requests, this.refundReceiver, { value: requestsValue(this.requests) }) + .then(tx => tx.wait()) + .then(receipt => + receipt.logs.filter( + log => log?.fragment?.type == 'event' && log?.fragment?.name == 'ExecutedForwardRequest', + ), + ); + + expect(events).to.have.lengthOf(this.requests.length - 1); }); } it('ignores a request with a valid signature for non-current nonce', async function () { // Execute first a request - await this.forwarder.execute(this.requestDatas[this.idx], { value: this.requestDatas[this.idx].value }); + await this.forwarder.execute(this.requests[idx], { value: this.requests[idx].value }); this.initialTamperedRequestNonce++; // Should be already incremented by the individual `execute` // And then ignore the same request in a batch due to an already used nonce - const receipt = await this.forwarder.executeBatch(this.requestDatas, this.refundReceiver, { - value: this.msgValue, - }); - expect(receipt.logs.filter(({ event }) => event === 'ExecutedForwardRequest').length).to.be.equal(2); + const events = await this.forwarder + .executeBatch(this.requests, this.refundReceiver, { value: this.value }) + .then(tx => tx.wait()) + .then(receipt => + receipt.logs.filter( + log => log?.fragment?.type == 'event' && log?.fragment?.name == 'ExecutedForwardRequest', + ), + ); + + expect(events).to.have.lengthOf(this.requests.length - 1); }); it('ignores a request with a valid signature for expired deadline', async function () { - this.requestDatas[this.idx].deadline = this.timestamp.toNumber() - 1; - this.requestDatas[this.idx].signature = this.sign( - this.signers[this.idx].getPrivateKey(), - this.requestDatas[this.idx], + this.requests[idx] = await this.forgeRequest( + { ...this.requests[idx], deadline: (await time.clock.timestamp()) - 1n }, + this.accounts[1], ); - const receipt = await this.forwarder.executeBatch(this.requestDatas, this.refundReceiver, { - value: this.msgValue, - }); - expect(receipt.logs.filter(({ event }) => event === 'ExecutedForwardRequest').length).to.be.equal(2); + const events = await this.forwarder + .executeBatch(this.requests, this.refundReceiver, { value: this.value }) + .then(tx => tx.wait()) + .then(receipt => + receipt.logs.filter( + log => log?.fragment?.type == 'event' && log?.fragment?.name == 'ExecutedForwardRequest', + ), + ); + + expect(events).to.have.lengthOf(this.requests.length - 1); }); afterEach(async function () { // The invalid request value was refunded - expect(await web3.eth.getBalance(this.refundReceiver)).to.be.bignumber.equal( - this.initialRefundReceiverBalance.add(web3.utils.toBN(this.requestDatas[this.idx].value)), + expect(await ethers.provider.getBalance(this.refundReceiver)).to.be.equal( + this.initialRefundReceiverBalance + this.requests[idx].value, ); // The invalid request from's nonce was not incremented - expect(await this.forwarder.nonces(this.requestDatas[this.idx].from)).to.be.bignumber.eq( - web3.utils.toBN(this.initialTamperedRequestNonce), - ); + expect(await this.forwarder.nonces(this.requests[idx].from)).to.be.equal(this.initialTamperedRequestNonce); }); }); it('bubbles out of gas', async function () { - this.requestDatas[this.idx].data = this.receiver.contract.methods.mockFunctionOutOfGas().encodeABI(); - this.requestDatas[this.idx].gas = 1_000_000; - this.requestDatas[this.idx].signature = this.sign( - this.signers[this.idx].getPrivateKey(), - this.requestDatas[this.idx], - ); + this.requests[idx] = await this.forgeRequest({ + data: this.receiver.interface.encodeFunctionData('mockFunctionOutOfGas'), + gas: 1_000_000n, + }); - const gasAvailable = 300_000; - await expectRevert.assertion( - this.forwarder.executeBatch(this.requestDatas, constants.ZERO_ADDRESS, { - gas: gasAvailable, - value: this.requestDatas.reduce((acc, { value }) => acc + Number(value), 0), + const gasLimit = 300_000n; + await expect( + this.forwarder.executeBatch(this.requests, ethers.ZeroAddress, { + gasLimit, + value: requestsValue(this.requests), }), - ); + ).to.be.revertedWithoutReason(); - const { transactions } = await web3.eth.getBlock('latest'); - const { gasUsed } = await web3.eth.getTransactionReceipt(transactions[0]); + const { gasUsed } = await ethers.provider + .getBlock('latest') + .then(block => block.getTransaction(0)) + .then(tx => ethers.provider.getTransactionReceipt(tx.hash)); - expect(gasUsed).to.be.equal(gasAvailable); + expect(gasUsed).to.be.equal(gasLimit); }); it('bubbles out of gas forced by the relayer', async function () { // Similarly to the single execute, a malicious relayer could grief requests. // We estimate until the selected request as if they were executed normally - const estimate = await this.gasUntil(this.requestDatas, this.idx); + const estimate = await Promise.all(this.requests.slice(0, idx + 1).map(this.estimateRequest)).then(gas => + sum(...gas), + ); // We add a Buffer to account for all the gas that's used before the selected call. // Note is slightly bigger because the selected request is not the index 0 and it affects // the buffer needed. - const gasAvailable = estimate + 10_000; + const gasLimit = estimate + 10_000n; // The subcall out of gas should be caught by the contract and then bubbled up consuming // the available gas with an `invalid` opcode. - await expectRevert.outOfGas( - this.forwarder.executeBatch(this.requestDatas, constants.ZERO_ADDRESS, { gas: gasAvailable }), - ); + await expect( + this.forwarder.executeBatch(this.requests, ethers.ZeroAddress, { + gasLimit, + value: requestsValue(this.requests), + }), + ).to.be.revertedWithoutReason(); - const { transactions } = await web3.eth.getBlock('latest'); - const { gasUsed } = await web3.eth.getTransactionReceipt(transactions[0]); + const { gasUsed } = await ethers.provider + .getBlock('latest') + .then(block => block.getTransaction(0)) + .then(tx => ethers.provider.getTransactionReceipt(tx.hash)); // We assert that indeed the gas was totally consumed. - expect(gasUsed).to.be.equal(gasAvailable); + expect(gasUsed).to.be.equal(gasLimit); }); }); }); diff --git a/test/utils/Context.behavior.js b/test/utils/Context.behavior.js index 08f7558d78b..9e986db7afb 100644 --- a/test/utils/Context.behavior.js +++ b/test/utils/Context.behavior.js @@ -1,38 +1,46 @@ -const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const ContextMock = artifacts.require('ContextMock'); +async function fixture() { + return { contextHelper: await ethers.deployContract('ContextMockCaller', []) }; +} +function shouldBehaveLikeRegularContext() { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); -function shouldBehaveLikeRegularContext(sender) { describe('msgSender', function () { it('returns the transaction sender when called from an EOA', async function () { - const receipt = await this.context.msgSender({ from: sender }); - expectEvent(receipt, 'Sender', { sender }); + await expect(this.context.connect(this.sender).msgSender()) + .to.emit(this.context, 'Sender') + .withArgs(this.sender.address); }); - it('returns the transaction sender when from another contract', async function () { - const { tx } = await this.caller.callSender(this.context.address, { from: sender }); - await expectEvent.inTransaction(tx, ContextMock, 'Sender', { sender: this.caller.address }); + it('returns the transaction sender when called from another contract', async function () { + await expect(this.contextHelper.connect(this.sender).callSender(this.context)) + .to.emit(this.context, 'Sender') + .withArgs(this.contextHelper.target); }); }); describe('msgData', function () { - const integerValue = new BN('42'); - const stringValue = 'OpenZeppelin'; - - let callData; - - beforeEach(async function () { - callData = this.context.contract.methods.msgData(integerValue.toString(), stringValue).encodeABI(); - }); + const args = [42n, 'OpenZeppelin']; it('returns the transaction data when called from an EOA', async function () { - const receipt = await this.context.msgData(integerValue, stringValue); - expectEvent(receipt, 'Data', { data: callData, integerValue, stringValue }); + const callData = this.context.interface.encodeFunctionData('msgData', args); + + await expect(this.context.msgData(...args)) + .to.emit(this.context, 'Data') + .withArgs(callData, ...args); }); it('returns the transaction sender when from another contract', async function () { - const { tx } = await this.caller.callData(this.context.address, integerValue, stringValue); - await expectEvent.inTransaction(tx, ContextMock, 'Data', { data: callData, integerValue, stringValue }); + const callData = this.context.interface.encodeFunctionData('msgData', args); + + await expect(this.contextHelper.callData(this.context, ...args)) + .to.emit(this.context, 'Data') + .withArgs(callData, ...args); }); }); } diff --git a/test/utils/Context.test.js b/test/utils/Context.test.js index f372f7420c2..b766729e2f6 100644 --- a/test/utils/Context.test.js +++ b/test/utils/Context.test.js @@ -1,17 +1,18 @@ -require('@openzeppelin/test-helpers'); - -const ContextMock = artifacts.require('ContextMock'); -const ContextMockCaller = artifacts.require('ContextMockCaller'); +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldBehaveLikeRegularContext } = require('./Context.behavior'); -contract('Context', function (accounts) { - const [sender] = accounts; +async function fixture() { + const [sender] = await ethers.getSigners(); + const context = await ethers.deployContract('ContextMock', []); + return { sender, context }; +} +describe('Context', function () { beforeEach(async function () { - this.context = await ContextMock.new(); - this.caller = await ContextMockCaller.new(); + Object.assign(this, await loadFixture(fixture)); }); - shouldBehaveLikeRegularContext(sender); + shouldBehaveLikeRegularContext(); }); From 9702b67ce13d8cd289149226c8c143bb0552b08d Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Thu, 23 Nov 2023 03:35:55 +0000 Subject: [PATCH 11/44] Migrate utils-cryptography to ethers (#4749) Co-authored-by: Hadrien Croubois Co-authored-by: ernestognw --- test/helpers/eip712.js | 5 +- test/helpers/sign.js | 63 ----- test/utils/cryptography/ECDSA.test.js | 249 ++++++++---------- test/utils/cryptography/EIP712.test.js | 1 + .../cryptography/MessageHashUtils.test.js | 75 +++--- .../cryptography/SignatureChecker.test.js | 82 ++---- 6 files changed, 185 insertions(+), 290 deletions(-) delete mode 100644 test/helpers/sign.js diff --git a/test/helpers/eip712.js b/test/helpers/eip712.js index 0dd78b7e05e..f09272b2828 100644 --- a/test/helpers/eip712.js +++ b/test/helpers/eip712.js @@ -46,8 +46,9 @@ function domainType(domain) { } function hashTypedData(domain, structHash) { - return ethers.keccak256( - Buffer.concat(['0x1901', ethers.TypedDataEncoder.hashDomain(domain), structHash].map(ethers.toBeArray)), + return ethers.solidityPackedKeccak256( + ['bytes', 'bytes32', 'bytes32'], + ['0x1901', ethers.TypedDataEncoder.hashDomain(domain), structHash], ); } diff --git a/test/helpers/sign.js b/test/helpers/sign.js deleted file mode 100644 index d537116bbb8..00000000000 --- a/test/helpers/sign.js +++ /dev/null @@ -1,63 +0,0 @@ -function toEthSignedMessageHash(messageHex) { - const messageBuffer = Buffer.from(messageHex.substring(2), 'hex'); - const prefix = Buffer.from(`\u0019Ethereum Signed Message:\n${messageBuffer.length}`); - return web3.utils.sha3(Buffer.concat([prefix, messageBuffer])); -} - -/** - * Create a signed data with intended validator according to the version 0 of EIP-191 - * @param validatorAddress The address of the validator - * @param dataHex The data to be concatenated with the prefix and signed - */ -function toDataWithIntendedValidatorHash(validatorAddress, dataHex) { - const validatorBuffer = Buffer.from(web3.utils.hexToBytes(validatorAddress)); - const dataBuffer = Buffer.from(web3.utils.hexToBytes(dataHex)); - const preambleBuffer = Buffer.from('\x19'); - const versionBuffer = Buffer.from('\x00'); - const ethMessage = Buffer.concat([preambleBuffer, versionBuffer, validatorBuffer, dataBuffer]); - - return web3.utils.sha3(ethMessage); -} - -/** - * Create a signer between a contract and a signer for a voucher of method, args, and redeemer - * Note that `method` is the web3 method, not the truffle-contract method - * @param contract TruffleContract - * @param signer address - * @param redeemer address - * @param methodName string - * @param methodArgs any[] - */ -const getSignFor = - (contract, signer) => - (redeemer, methodName, methodArgs = []) => { - const parts = [contract.address, redeemer]; - - const REAL_SIGNATURE_SIZE = 2 * 65; // 65 bytes in hexadecimal string length - const PADDED_SIGNATURE_SIZE = 2 * 96; // 96 bytes in hexadecimal string length - const DUMMY_SIGNATURE = `0x${web3.utils.padLeft('', REAL_SIGNATURE_SIZE)}`; - - // if we have a method, add it to the parts that we're signing - if (methodName) { - if (methodArgs.length > 0) { - parts.push( - contract.contract.methods[methodName](...methodArgs.concat([DUMMY_SIGNATURE])) - .encodeABI() - .slice(0, -1 * PADDED_SIGNATURE_SIZE), - ); - } else { - const abi = contract.abi.find(abi => abi.name === methodName); - parts.push(abi.signature); - } - } - - // return the signature of the "Ethereum Signed Message" hash of the hash of `parts` - const messageHex = web3.utils.soliditySha3(...parts); - return web3.eth.sign(messageHex, signer); - }; - -module.exports = { - toEthSignedMessageHash, - toDataWithIntendedValidatorHash, - getSignFor, -}; diff --git a/test/utils/cryptography/ECDSA.test.js b/test/utils/cryptography/ECDSA.test.js index f164ef196c7..3c0ce76c6d0 100644 --- a/test/utils/cryptography/ECDSA.test.js +++ b/test/utils/cryptography/ECDSA.test.js @@ -1,104 +1,81 @@ -require('@openzeppelin/test-helpers'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const { toEthSignedMessageHash } = require('../../helpers/sign'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const ECDSA = artifacts.require('$ECDSA'); - -const TEST_MESSAGE = web3.utils.sha3('OpenZeppelin'); -const WRONG_MESSAGE = web3.utils.sha3('Nope'); -const NON_HASH_MESSAGE = '0x' + Buffer.from('abcd').toString('hex'); - -function to2098Format(signature) { - const long = web3.utils.hexToBytes(signature); - if (long.length !== 65) { - throw new Error('invalid signature length (expected long format)'); - } - if (long[32] >> 7 === 1) { - throw new Error("invalid signature 's' value"); - } - const short = long.slice(0, 64); - short[32] |= long[64] % 27 << 7; // set the first bit of the 32nd byte to the v parity bit - return web3.utils.bytesToHex(short); -} +const TEST_MESSAGE = ethers.id('OpenZeppelin'); +const WRONG_MESSAGE = ethers.id('Nope'); +const NON_HASH_MESSAGE = '0xabcd'; -function split(signature) { - const raw = web3.utils.hexToBytes(signature); - switch (raw.length) { - case 64: - return [ - web3.utils.bytesToHex(raw.slice(0, 32)), // r - web3.utils.bytesToHex(raw.slice(32, 64)), // vs - ]; - case 65: - return [ - raw[64], // v - web3.utils.bytesToHex(raw.slice(0, 32)), // r - web3.utils.bytesToHex(raw.slice(32, 64)), // s - ]; - default: - expect.fail('Invalid signature length, cannot split'); - } +function toSignature(signature) { + return ethers.Signature.from(signature); } -contract('ECDSA', function (accounts) { - const [other] = accounts; +async function fixture() { + const [signer] = await ethers.getSigners(); + const mock = await ethers.deployContract('$ECDSA'); + return { signer, mock }; +} +describe('ECDSA', function () { beforeEach(async function () { - this.ecdsa = await ECDSA.new(); + Object.assign(this, await loadFixture(fixture)); }); - context('recover with invalid signature', function () { + describe('recover with invalid signature', function () { it('with short signature', async function () { - await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, '0x1234'), 'ECDSAInvalidSignatureLength', [2]); + await expect(this.mock.$recover(TEST_MESSAGE, '0x1234')) + .to.be.revertedWithCustomError(this.mock, 'ECDSAInvalidSignatureLength') + .withArgs(2); }); it('with long signature', async function () { - await expectRevertCustomError( + await expect( // eslint-disable-next-line max-len - this.ecdsa.$recover( + this.mock.$recover( TEST_MESSAGE, '0x01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789', ), - 'ECDSAInvalidSignatureLength', - [85], - ); + ) + .to.be.revertedWithCustomError(this.mock, 'ECDSAInvalidSignatureLength') + .withArgs(85); }); }); - context('recover with valid signature', function () { - context('using web3.eth.sign', function () { + describe('recover with valid signature', function () { + describe('using .sign', function () { it('returns signer address with correct signature', async function () { // Create the signature - const signature = await web3.eth.sign(TEST_MESSAGE, other); + const signature = await this.signer.signMessage(TEST_MESSAGE); // Recover the signer address from the generated message and signature. - expect(await this.ecdsa.$recover(toEthSignedMessageHash(TEST_MESSAGE), signature)).to.equal(other); + expect(await this.mock.$recover(ethers.hashMessage(TEST_MESSAGE), signature)).to.equal(this.signer.address); }); it('returns signer address with correct signature for arbitrary length message', async function () { // Create the signature - const signature = await web3.eth.sign(NON_HASH_MESSAGE, other); + const signature = await this.signer.signMessage(NON_HASH_MESSAGE); // Recover the signer address from the generated message and signature. - expect(await this.ecdsa.$recover(toEthSignedMessageHash(NON_HASH_MESSAGE), signature)).to.equal(other); + expect(await this.mock.$recover(ethers.hashMessage(NON_HASH_MESSAGE), signature)).to.equal(this.signer.address); }); it('returns a different address', async function () { - const signature = await web3.eth.sign(TEST_MESSAGE, other); - expect(await this.ecdsa.$recover(WRONG_MESSAGE, signature)).to.not.equal(other); + const signature = await this.signer.signMessage(TEST_MESSAGE); + expect(await this.mock.$recover(WRONG_MESSAGE, signature)).to.not.be.equal(this.signer.address); }); it('reverts with invalid signature', async function () { // eslint-disable-next-line max-len const signature = '0x332ce75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e01c'; - await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSAInvalidSignature', []); + await expect(this.mock.$recover(TEST_MESSAGE, signature)).to.be.revertedWithCustomError( + this.mock, + 'ECDSAInvalidSignature', + ); }); }); - context('with v=27 signature', function () { + describe('with v=27 signature', function () { // Signature generated outside ganache with method web3.eth.sign(signer, message) const signer = '0x2cc1166f6212628A0deEf2B33BEFB2187D35b86c'; // eslint-disable-next-line max-len @@ -106,124 +83,112 @@ contract('ECDSA', function (accounts) { '0x5d99b6f7f6d1f73d1a26497f2b1c89b24c0993913f86e9a2d02cd69887d9c94f3c880358579d811b21dd1b7fd9bb01c1d81d10e69f0384e675c32b39643be892'; it('works with correct v value', async function () { - const v = '1b'; // 27 = 1b. - const signature = signatureWithoutV + v; - expect(await this.ecdsa.$recover(TEST_MESSAGE, signature)).to.equal(signer); + const v = '0x1b'; // 27 = 1b. + const signature = ethers.concat([signatureWithoutV, v]); + expect(await this.mock.$recover(TEST_MESSAGE, signature)).to.equal(signer); - expect( - await this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)), - ).to.equal(signer); + const { r, s, yParityAndS: vs } = toSignature(signature); + expect(await this.mock.getFunction('$recover(bytes32,uint8,bytes32,bytes32)')(TEST_MESSAGE, v, r, s)).to.equal( + signer, + ); - expect( - await this.ecdsa.methods['$recover(bytes32,bytes32,bytes32)']( - TEST_MESSAGE, - ...split(to2098Format(signature)), - ), - ).to.equal(signer); + expect(await this.mock.getFunction('$recover(bytes32,bytes32,bytes32)')(TEST_MESSAGE, r, vs)).to.equal(signer); }); it('rejects incorrect v value', async function () { - const v = '1c'; // 28 = 1c. - const signature = signatureWithoutV + v; - expect(await this.ecdsa.$recover(TEST_MESSAGE, signature)).to.not.equal(signer); + const v = '0x1c'; // 28 = 1c. + const signature = ethers.concat([signatureWithoutV, v]); + expect(await this.mock.$recover(TEST_MESSAGE, signature)).to.not.equal(signer); + const { r, s, yParityAndS: vs } = toSignature(signature); expect( - await this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)), + await this.mock.getFunction('$recover(bytes32,uint8,bytes32,bytes32)')(TEST_MESSAGE, v, r, s), ).to.not.equal(signer); - expect( - await this.ecdsa.methods['$recover(bytes32,bytes32,bytes32)']( - TEST_MESSAGE, - ...split(to2098Format(signature)), - ), - ).to.not.equal(signer); + expect(await this.mock.getFunction('$recover(bytes32,bytes32,bytes32)')(TEST_MESSAGE, r, vs)).to.not.equal( + signer, + ); }); it('reverts wrong v values', async function () { - for (const v of ['00', '01']) { - const signature = signatureWithoutV + v; - await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSAInvalidSignature', []); - - await expectRevertCustomError( - this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)), + for (const v of ['0x00', '0x01']) { + const signature = ethers.concat([signatureWithoutV, v]); + await expect(this.mock.$recover(TEST_MESSAGE, signature)).to.be.revertedWithCustomError( + this.mock, 'ECDSAInvalidSignature', - [], ); + + const { r, s } = toSignature(signature); + await expect( + this.mock.getFunction('$recover(bytes32,uint8,bytes32,bytes32)')(TEST_MESSAGE, v, r, s), + ).to.be.revertedWithCustomError(this.mock, 'ECDSAInvalidSignature'); } }); it('rejects short EIP2098 format', async function () { - const v = '1b'; // 27 = 1b. - const signature = signatureWithoutV + v; - await expectRevertCustomError( - this.ecdsa.$recover(TEST_MESSAGE, to2098Format(signature)), - 'ECDSAInvalidSignatureLength', - [64], - ); + const v = '0x1b'; // 27 = 1b. + const signature = ethers.concat([signatureWithoutV, v]); + await expect(this.mock.$recover(TEST_MESSAGE, toSignature(signature).compactSerialized)) + .to.be.revertedWithCustomError(this.mock, 'ECDSAInvalidSignatureLength') + .withArgs(64); }); }); - context('with v=28 signature', function () { + describe('with v=28 signature', function () { const signer = '0x1E318623aB09Fe6de3C9b8672098464Aeda9100E'; // eslint-disable-next-line max-len const signatureWithoutV = '0x331fe75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e0'; it('works with correct v value', async function () { - const v = '1c'; // 28 = 1c. - const signature = signatureWithoutV + v; - expect(await this.ecdsa.$recover(TEST_MESSAGE, signature)).to.equal(signer); + const v = '0x1c'; // 28 = 1c. + const signature = ethers.concat([signatureWithoutV, v]); + expect(await this.mock.$recover(TEST_MESSAGE, signature)).to.equal(signer); - expect( - await this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)), - ).to.equal(signer); + const { r, s, yParityAndS: vs } = toSignature(signature); + expect(await this.mock.getFunction('$recover(bytes32,uint8,bytes32,bytes32)')(TEST_MESSAGE, v, r, s)).to.equal( + signer, + ); - expect( - await this.ecdsa.methods['$recover(bytes32,bytes32,bytes32)']( - TEST_MESSAGE, - ...split(to2098Format(signature)), - ), - ).to.equal(signer); + expect(await this.mock.getFunction('$recover(bytes32,bytes32,bytes32)')(TEST_MESSAGE, r, vs)).to.equal(signer); }); it('rejects incorrect v value', async function () { - const v = '1b'; // 27 = 1b. - const signature = signatureWithoutV + v; - expect(await this.ecdsa.$recover(TEST_MESSAGE, signature)).to.not.equal(signer); + const v = '0x1b'; // 27 = 1b. + const signature = ethers.concat([signatureWithoutV, v]); + expect(await this.mock.$recover(TEST_MESSAGE, signature)).to.not.equal(signer); + const { r, s, yParityAndS: vs } = toSignature(signature); expect( - await this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)), + await this.mock.getFunction('$recover(bytes32,uint8,bytes32,bytes32)')(TEST_MESSAGE, v, r, s), ).to.not.equal(signer); - expect( - await this.ecdsa.methods['$recover(bytes32,bytes32,bytes32)']( - TEST_MESSAGE, - ...split(to2098Format(signature)), - ), - ).to.not.equal(signer); + expect(await this.mock.getFunction('$recover(bytes32,bytes32,bytes32)')(TEST_MESSAGE, r, vs)).to.not.equal( + signer, + ); }); it('reverts invalid v values', async function () { - for (const v of ['00', '01']) { - const signature = signatureWithoutV + v; - await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSAInvalidSignature', []); - - await expectRevertCustomError( - this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)), + for (const v of ['0x00', '0x01']) { + const signature = ethers.concat([signatureWithoutV, v]); + await expect(this.mock.$recover(TEST_MESSAGE, signature)).to.be.revertedWithCustomError( + this.mock, 'ECDSAInvalidSignature', - [], ); + + const { r, s } = toSignature(signature); + await expect( + this.mock.getFunction('$recover(bytes32,uint8,bytes32,bytes32)')(TEST_MESSAGE, v, r, s), + ).to.be.revertedWithCustomError(this.mock, 'ECDSAInvalidSignature'); } }); it('rejects short EIP2098 format', async function () { - const v = '1c'; // 27 = 1b. - const signature = signatureWithoutV + v; - await expectRevertCustomError( - this.ecdsa.$recover(TEST_MESSAGE, to2098Format(signature)), - 'ECDSAInvalidSignatureLength', - [64], - ); + const v = '0x1c'; // 27 = 1b. + const signature = ethers.concat([signatureWithoutV, v]); + await expect(this.mock.$recover(TEST_MESSAGE, toSignature(signature).compactSerialized)) + .to.be.revertedWithCustomError(this.mock, 'ECDSAInvalidSignatureLength') + .withArgs(64); }); }); @@ -232,14 +197,18 @@ contract('ECDSA', function (accounts) { // eslint-disable-next-line max-len const highSSignature = '0xe742ff452d41413616a5bf43fe15dd88294e983d3d36206c2712f39083d638bde0a0fc89be718fbc1033e1d30d78be1c68081562ed2e97af876f286f3453231d1b'; - const [r, v, s] = split(highSSignature); - await expectRevertCustomError(this.ecdsa.$recover(message, highSSignature), 'ECDSAInvalidSignatureS', [s]); - await expectRevertCustomError( - this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, r, v, s), - 'ECDSAInvalidSignatureS', - [s], - ); - expect(() => to2098Format(highSSignature)).to.throw("invalid signature 's' value"); + + const r = ethers.dataSlice(highSSignature, 0, 32); + const s = ethers.dataSlice(highSSignature, 32, 64); + const v = ethers.dataSlice(highSSignature, 64, 65); + + await expect(this.mock.$recover(message, highSSignature)) + .to.be.revertedWithCustomError(this.mock, 'ECDSAInvalidSignatureS') + .withArgs(s); + await expect(this.mock.getFunction('$recover(bytes32,uint8,bytes32,bytes32)')(TEST_MESSAGE, v, r, s)) + .to.be.revertedWithCustomError(this.mock, 'ECDSAInvalidSignatureS') + .withArgs(s); + expect(() => toSignature(highSSignature)).to.throw('non-canonical s'); }); }); }); diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js index dfad67906b3..9d0bdae9a9f 100644 --- a/test/utils/cryptography/EIP712.test.js +++ b/test/utils/cryptography/EIP712.test.js @@ -1,4 +1,5 @@ const { ethers } = require('hardhat'); +const { expect } = require('chai'); const { getDomain, domainType, domainSeparator, hashTypedData } = require('../../helpers/eip712'); const { getChainId } = require('../../helpers/chainid'); const { mapValues } = require('../../helpers/iterate'); diff --git a/test/utils/cryptography/MessageHashUtils.test.js b/test/utils/cryptography/MessageHashUtils.test.js index b38e945daaa..f20f5a3caed 100644 --- a/test/utils/cryptography/MessageHashUtils.test.js +++ b/test/utils/cryptography/MessageHashUtils.test.js @@ -1,55 +1,68 @@ -require('@openzeppelin/test-helpers'); -const { toEthSignedMessageHash, toDataWithIntendedValidatorHash } = require('../../helpers/sign'); -const { domainSeparator, hashTypedData } = require('../../helpers/eip712'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const MessageHashUtils = artifacts.require('$MessageHashUtils'); +const { domainSeparator, hashTypedData } = require('../../helpers/eip712'); -contract('MessageHashUtils', function () { - beforeEach(async function () { - this.messageHashUtils = await MessageHashUtils.new(); +async function fixture() { + const mock = await ethers.deployContract('$MessageHashUtils'); + return { mock }; +} - this.message = '0x' + Buffer.from('abcd').toString('hex'); - this.messageHash = web3.utils.sha3(this.message); - this.verifyingAddress = web3.utils.toChecksumAddress(web3.utils.randomHex(20)); +describe('MessageHashUtils', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); }); - context('toEthSignedMessageHash', function () { + describe('toEthSignedMessageHash', function () { it('prefixes bytes32 data correctly', async function () { - expect(await this.messageHashUtils.methods['$toEthSignedMessageHash(bytes32)'](this.messageHash)).to.equal( - toEthSignedMessageHash(this.messageHash), - ); + const message = ethers.randomBytes(32); + const expectedHash = ethers.hashMessage(message); + + expect(await this.mock.getFunction('$toEthSignedMessageHash(bytes32)')(message)).to.equal(expectedHash); }); it('prefixes dynamic length data correctly', async function () { - expect(await this.messageHashUtils.methods['$toEthSignedMessageHash(bytes)'](this.message)).to.equal( - toEthSignedMessageHash(this.message), - ); + const message = ethers.randomBytes(128); + const expectedHash = ethers.hashMessage(message); + + expect(await this.mock.getFunction('$toEthSignedMessageHash(bytes)')(message)).to.equal(expectedHash); + }); + + it('version match for bytes32', async function () { + const message = ethers.randomBytes(32); + const fixed = await this.mock.getFunction('$toEthSignedMessageHash(bytes32)')(message); + const dynamic = await this.mock.getFunction('$toEthSignedMessageHash(bytes)')(message); + + expect(fixed).to.equal(dynamic); }); }); - context('toDataWithIntendedValidatorHash', function () { + describe('toDataWithIntendedValidatorHash', function () { it('returns the digest correctly', async function () { - expect( - await this.messageHashUtils.$toDataWithIntendedValidatorHash(this.verifyingAddress, this.message), - ).to.equal(toDataWithIntendedValidatorHash(this.verifyingAddress, this.message)); + const verifier = ethers.Wallet.createRandom().address; + const message = ethers.randomBytes(128); + const expectedHash = ethers.solidityPackedKeccak256( + ['string', 'address', 'bytes'], + ['\x19\x00', verifier, message], + ); + + expect(await this.mock.$toDataWithIntendedValidatorHash(verifier, message)).to.equal(expectedHash); }); }); - context('toTypedDataHash', function () { + describe('toTypedDataHash', function () { it('returns the digest correctly', async function () { const domain = { name: 'Test', - version: 1, - chainId: 1, - verifyingContract: this.verifyingAddress, + version: '1', + chainId: 1n, + verifyingContract: ethers.Wallet.createRandom().address, }; - const structhash = web3.utils.randomHex(32); - const expectedDomainSeparator = await domainSeparator(domain); - expect(await this.messageHashUtils.$toTypedDataHash(expectedDomainSeparator, structhash)).to.equal( - hashTypedData(domain, structhash), - ); + const structhash = ethers.randomBytes(32); + const expectedHash = hashTypedData(domain, structhash); + + expect(await this.mock.$toTypedDataHash(domainSeparator(domain), structhash)).to.equal(expectedHash); }); }); }); diff --git a/test/utils/cryptography/SignatureChecker.test.js b/test/utils/cryptography/SignatureChecker.test.js index ba8b100d1a2..e6a08491a51 100644 --- a/test/utils/cryptography/SignatureChecker.test.js +++ b/test/utils/cryptography/SignatureChecker.test.js @@ -1,85 +1,59 @@ -const { toEthSignedMessageHash } = require('../../helpers/sign'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const TEST_MESSAGE = ethers.id('OpenZeppelin'); +const TEST_MESSAGE_HASH = ethers.hashMessage(TEST_MESSAGE); -const SignatureChecker = artifacts.require('$SignatureChecker'); -const ERC1271WalletMock = artifacts.require('ERC1271WalletMock'); -const ERC1271MaliciousMock = artifacts.require('ERC1271MaliciousMock'); +const WRONG_MESSAGE = ethers.id('Nope'); +const WRONG_MESSAGE_HASH = ethers.hashMessage(WRONG_MESSAGE); -const TEST_MESSAGE = web3.utils.sha3('OpenZeppelin'); -const WRONG_MESSAGE = web3.utils.sha3('Nope'); +async function fixture() { + const [signer, other] = await ethers.getSigners(); + const mock = await ethers.deployContract('$SignatureChecker'); + const wallet = await ethers.deployContract('ERC1271WalletMock', [signer]); + const malicious = await ethers.deployContract('ERC1271MaliciousMock'); + const signature = await signer.signMessage(TEST_MESSAGE); -contract('SignatureChecker (ERC1271)', function (accounts) { - const [signer, other] = accounts; + return { signer, other, mock, wallet, malicious, signature }; +} +describe('SignatureChecker (ERC1271)', function () { before('deploying', async function () { - this.signaturechecker = await SignatureChecker.new(); - this.wallet = await ERC1271WalletMock.new(signer); - this.malicious = await ERC1271MaliciousMock.new(); - this.signature = await web3.eth.sign(TEST_MESSAGE, signer); + Object.assign(this, await loadFixture(fixture)); }); - context('EOA account', function () { + describe('EOA account', function () { it('with matching signer and signature', async function () { - expect( - await this.signaturechecker.$isValidSignatureNow(signer, toEthSignedMessageHash(TEST_MESSAGE), this.signature), - ).to.equal(true); + expect(await this.mock.$isValidSignatureNow(this.signer, TEST_MESSAGE_HASH, this.signature)).to.be.true; }); it('with invalid signer', async function () { - expect( - await this.signaturechecker.$isValidSignatureNow(other, toEthSignedMessageHash(TEST_MESSAGE), this.signature), - ).to.equal(false); + expect(await this.mock.$isValidSignatureNow(this.other, TEST_MESSAGE_HASH, this.signature)).to.be.false; }); it('with invalid signature', async function () { - expect( - await this.signaturechecker.$isValidSignatureNow(signer, toEthSignedMessageHash(WRONG_MESSAGE), this.signature), - ).to.equal(false); + expect(await this.mock.$isValidSignatureNow(this.signer, WRONG_MESSAGE_HASH, this.signature)).to.be.false; }); }); - context('ERC1271 wallet', function () { - for (const signature of ['isValidERC1271SignatureNow', 'isValidSignatureNow']) { - context(signature, function () { + describe('ERC1271 wallet', function () { + for (const fn of ['isValidERC1271SignatureNow', 'isValidSignatureNow']) { + describe(fn, function () { it('with matching signer and signature', async function () { - expect( - await this.signaturechecker[`$${signature}`]( - this.wallet.address, - toEthSignedMessageHash(TEST_MESSAGE), - this.signature, - ), - ).to.equal(true); + expect(await this.mock.getFunction(`$${fn}`)(this.wallet, TEST_MESSAGE_HASH, this.signature)).to.be.true; }); it('with invalid signer', async function () { - expect( - await this.signaturechecker[`$${signature}`]( - this.signaturechecker.address, - toEthSignedMessageHash(TEST_MESSAGE), - this.signature, - ), - ).to.equal(false); + expect(await this.mock.getFunction(`$${fn}`)(this.mock, TEST_MESSAGE_HASH, this.signature)).to.be.false; }); it('with invalid signature', async function () { - expect( - await this.signaturechecker[`$${signature}`]( - this.wallet.address, - toEthSignedMessageHash(WRONG_MESSAGE), - this.signature, - ), - ).to.equal(false); + expect(await this.mock.getFunction(`$${fn}`)(this.wallet, WRONG_MESSAGE_HASH, this.signature)).to.be.false; }); it('with malicious wallet', async function () { - expect( - await this.signaturechecker[`$${signature}`]( - this.malicious.address, - toEthSignedMessageHash(TEST_MESSAGE), - this.signature, - ), - ).to.equal(false); + expect(await this.mock.getFunction(`$${fn}`)(this.malicious, TEST_MESSAGE_HASH, this.signature)).to.be.false; }); }); } From 6a56b3b08d9debf6ed12d0eab59629d7bd5d3fd2 Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Thu, 23 Nov 2023 04:40:12 +0000 Subject: [PATCH 12/44] Migrate EIP712 to ethersjs (#4750) Co-authored-by: Hadrien Croubois Co-authored-by: ernestognw --- scripts/upgradeable/upgradeable.patch | 41 ++++---- test/governance/Governor.test.js | 20 ++-- .../extensions/GovernorWithParams.test.js | 15 ++- test/governance/utils/Votes.behavior.js | 12 +-- test/helpers/eip712-types.js | 44 +++++++++ test/helpers/eip712.js | 23 +---- .../ERC20/extensions/ERC20Permit.test.js | 7 +- .../token/ERC20/extensions/ERC20Votes.test.js | 12 +-- test/utils/cryptography/EIP712.test.js | 98 +++++++++---------- 9 files changed, 147 insertions(+), 125 deletions(-) create mode 100644 test/helpers/eip712-types.js diff --git a/scripts/upgradeable/upgradeable.patch b/scripts/upgradeable/upgradeable.patch index 522f82ca48c..c2a5732d986 100644 --- a/scripts/upgradeable/upgradeable.patch +++ b/scripts/upgradeable/upgradeable.patch @@ -59,10 +59,10 @@ index ff596b0c3..000000000 - - diff --git a/README.md b/README.md -index 549891e3f..a6b24078e 100644 +index 9ca41573f..57d6e3b5b 100644 --- a/README.md +++ b/README.md -@@ -23,6 +23,9 @@ +@@ -19,6 +19,9 @@ > [!IMPORTANT] > OpenZeppelin Contracts uses semantic versioning to communicate backwards compatibility of its API and storage layout. For upgradeable contracts, the storage layout of different major versions should be assumed incompatible, for example, it is unsafe to upgrade from 4.9.3 to 5.0.0. Learn more at [Backwards Compatibility](https://docs.openzeppelin.com/contracts/backwards-compatibility). @@ -72,7 +72,7 @@ index 549891e3f..a6b24078e 100644 ## Overview ### Installation -@@ -30,7 +33,7 @@ +@@ -26,7 +29,7 @@ #### Hardhat, Truffle (npm) ``` @@ -81,7 +81,7 @@ index 549891e3f..a6b24078e 100644 ``` #### Foundry (git) -@@ -42,10 +45,10 @@ $ npm install @openzeppelin/contracts +@@ -38,10 +41,10 @@ $ npm install @openzeppelin/contracts > Foundry installs the latest version initially, but subsequent `forge update` commands will use the `master` branch. ``` @@ -94,7 +94,7 @@ index 549891e3f..a6b24078e 100644 ### Usage -@@ -54,10 +57,11 @@ Once installed, you can use the contracts in the library by importing them: +@@ -50,10 +53,11 @@ Once installed, you can use the contracts in the library by importing them: ```solidity pragma solidity ^0.8.20; @@ -110,7 +110,7 @@ index 549891e3f..a6b24078e 100644 } ``` diff --git a/contracts/package.json b/contracts/package.json -index 9017953ca..f51c1d38b 100644 +index be3e741e3..877e942c2 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,5 +1,5 @@ @@ -118,7 +118,7 @@ index 9017953ca..f51c1d38b 100644 - "name": "@openzeppelin/contracts", + "name": "@openzeppelin/contracts-upgradeable", "description": "Secure Smart Contract library for Solidity", - "version": "4.9.2", + "version": "5.0.0", "files": [ @@ -13,7 +13,7 @@ }, @@ -140,7 +140,7 @@ index 9017953ca..f51c1d38b 100644 + } } diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol -index 644f6f531..ab8ba05ff 100644 +index 8e548cdd8..a60ee74fd 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -4,7 +4,6 @@ @@ -307,7 +307,7 @@ index 644f6f531..ab8ba05ff 100644 } } diff --git a/package.json b/package.json -index 3a1617c09..97e59c2d9 100644 +index c2c3a2675..3301b213d 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ @@ -328,12 +328,12 @@ index 304d1386a..a1cd63bee 100644 +@openzeppelin/contracts-upgradeable/=contracts/ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js -index faf01f1a3..b25171a56 100644 +index 75ca00b12..265e6c909 100644 --- a/test/utils/cryptography/EIP712.test.js +++ b/test/utils/cryptography/EIP712.test.js -@@ -47,26 +47,6 @@ contract('EIP712', function (accounts) { +@@ -40,27 +40,6 @@ describe('EIP712', function () { const rebuildDomain = await getDomain(this.eip712); - expect(mapValues(rebuildDomain, String)).to.be.deep.equal(mapValues(this.domain, String)); + expect(rebuildDomain).to.be.deep.equal(this.domain); }); - - if (shortOrLong === 'short') { @@ -341,17 +341,18 @@ index faf01f1a3..b25171a56 100644 - // the upgradeable contract variant is used and the initializer is invoked. - - it('adjusts when behind proxy', async function () { -- const factory = await Clones.new(); -- const cloneReceipt = await factory.$clone(this.eip712.address); -- const cloneAddress = cloneReceipt.logs.find(({ event }) => event === 'return$clone').args.instance; -- const clone = new EIP712Verifier(cloneAddress); +- const factory = await ethers.deployContract('$Clones'); - -- const cloneDomain = { ...this.domain, verifyingContract: clone.address }; +- const clone = await factory +- .$clone(this.eip712) +- .then(tx => tx.wait()) +- .then(receipt => receipt.logs.find(ev => ev.fragment.name == 'return$clone').args.instance) +- .then(address => ethers.getContractAt('$EIP712Verifier', address)); - -- const reportedDomain = await getDomain(clone); -- expect(mapValues(reportedDomain, String)).to.be.deep.equal(mapValues(cloneDomain, String)); +- const expectedDomain = { ...this.domain, verifyingContract: clone.target }; +- expect(await getDomain(clone)).to.be.deep.equal(expectedDomain); - -- const expectedSeparator = await domainSeparator(cloneDomain); +- const expectedSeparator = await domainSeparator(expectedDomain); - expect(await clone.$_domainSeparatorV4()).to.equal(expectedSeparator); - }); - } diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js index 45cceba9a89..b62160eec62 100644 --- a/test/governance/Governor.test.js +++ b/test/governance/Governor.test.js @@ -4,7 +4,11 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; const Enums = require('../helpers/enums'); -const { getDomain, domainType } = require('../helpers/eip712'); +const { + getDomain, + domainType, + types: { Ballot }, +} = require('../helpers/eip712'); const { GovernorHelper, proposalStatesToBitMap } = require('../helpers/governance'); const { clockFromReceipt } = require('../helpers/time'); const { expectRevertCustomError } = require('../helpers/customError'); @@ -209,12 +213,7 @@ contract('Governor', function (accounts) { primaryType: 'Ballot', types: { EIP712Domain: domainType(domain), - Ballot: [ - { name: 'proposalId', type: 'uint256' }, - { name: 'support', type: 'uint8' }, - { name: 'voter', type: 'address' }, - { name: 'nonce', type: 'uint256' }, - ], + Ballot, }, domain, message, @@ -384,12 +383,7 @@ contract('Governor', function (accounts) { primaryType: 'Ballot', types: { EIP712Domain: domainType(domain), - Ballot: [ - { name: 'proposalId', type: 'uint256' }, - { name: 'support', type: 'uint8' }, - { name: 'voter', type: 'address' }, - { name: 'nonce', type: 'uint256' }, - ], + Ballot, }, domain, message, diff --git a/test/governance/extensions/GovernorWithParams.test.js b/test/governance/extensions/GovernorWithParams.test.js index 35da3a0f3e6..da392b3ea98 100644 --- a/test/governance/extensions/GovernorWithParams.test.js +++ b/test/governance/extensions/GovernorWithParams.test.js @@ -4,7 +4,11 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; const Enums = require('../../helpers/enums'); -const { getDomain, domainType } = require('../../helpers/eip712'); +const { + getDomain, + domainType, + types: { ExtendedBallot }, +} = require('../../helpers/eip712'); const { GovernorHelper } = require('../../helpers/governance'); const { expectRevertCustomError } = require('../../helpers/customError'); @@ -130,14 +134,7 @@ contract('GovernorWithParams', function (accounts) { primaryType: 'ExtendedBallot', types: { EIP712Domain: domainType(domain), - ExtendedBallot: [ - { name: 'proposalId', type: 'uint256' }, - { name: 'support', type: 'uint8' }, - { name: 'voter', type: 'address' }, - { name: 'nonce', type: 'uint256' }, - { name: 'reason', type: 'string' }, - { name: 'params', type: 'bytes' }, - ], + ExtendedBallot, }, domain, message, diff --git a/test/governance/utils/Votes.behavior.js b/test/governance/utils/Votes.behavior.js index 5836cc35154..0aea208a936 100644 --- a/test/governance/utils/Votes.behavior.js +++ b/test/governance/utils/Votes.behavior.js @@ -7,16 +7,14 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; const { shouldBehaveLikeEIP6372 } = require('./EIP6372.behavior'); -const { getDomain, domainType } = require('../../helpers/eip712'); +const { + getDomain, + domainType, + types: { Delegation }, +} = require('../../helpers/eip712'); const { clockFromReceipt } = require('../../helpers/time'); const { expectRevertCustomError } = require('../../helpers/customError'); -const Delegation = [ - { name: 'delegatee', type: 'address' }, - { name: 'nonce', type: 'uint256' }, - { name: 'expiry', type: 'uint256' }, -]; - const buildAndSignDelegation = (contract, message, pk) => getDomain(contract) .then(domain => ({ diff --git a/test/helpers/eip712-types.js b/test/helpers/eip712-types.js new file mode 100644 index 00000000000..8aacf5325f6 --- /dev/null +++ b/test/helpers/eip712-types.js @@ -0,0 +1,44 @@ +module.exports = { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + { name: 'salt', type: 'bytes32' }, + ], + Permit: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + Ballot: [ + { name: 'proposalId', type: 'uint256' }, + { name: 'support', type: 'uint8' }, + { name: 'voter', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + ], + ExtendedBallot: [ + { name: 'proposalId', type: 'uint256' }, + { name: 'support', type: 'uint8' }, + { name: 'voter', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'reason', type: 'string' }, + { name: 'params', type: 'bytes' }, + ], + Delegation: [ + { name: 'delegatee', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'expiry', type: 'uint256' }, + ], + ForwardRequest: [ + { name: 'from', type: 'address' }, + { name: 'to', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'gas', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint48' }, + { name: 'data', type: 'bytes' }, + ], +}; diff --git a/test/helpers/eip712.js b/test/helpers/eip712.js index f09272b2828..295c1b95315 100644 --- a/test/helpers/eip712.js +++ b/test/helpers/eip712.js @@ -1,20 +1,5 @@ const { ethers } = require('ethers'); - -const EIP712Domain = [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'verifyingContract', type: 'address' }, - { name: 'salt', type: 'bytes32' }, -]; - -const Permit = [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, -]; +const types = require('./eip712-types'); async function getDomain(contract) { const { fields, name, version, chainId, verifyingContract, salt, extensions } = await contract.eip712Domain(); @@ -32,7 +17,7 @@ async function getDomain(contract) { salt, }; - for (const [i, { name }] of EIP712Domain.entries()) { + for (const [i, { name }] of types.EIP712Domain.entries()) { if (!(fields & (1 << i))) { delete domain[name]; } @@ -42,7 +27,7 @@ async function getDomain(contract) { } function domainType(domain) { - return EIP712Domain.filter(({ name }) => domain[name] !== undefined); + return types.EIP712Domain.filter(({ name }) => domain[name] !== undefined); } function hashTypedData(domain, structHash) { @@ -53,7 +38,7 @@ function hashTypedData(domain, structHash) { } module.exports = { - Permit, + types, getDomain, domainType, domainSeparator: ethers.TypedDataEncoder.hashDomain, diff --git a/test/token/ERC20/extensions/ERC20Permit.test.js b/test/token/ERC20/extensions/ERC20Permit.test.js index 388716d534e..db2363cd26c 100644 --- a/test/token/ERC20/extensions/ERC20Permit.test.js +++ b/test/token/ERC20/extensions/ERC20Permit.test.js @@ -10,7 +10,12 @@ const Wallet = require('ethereumjs-wallet').default; const ERC20Permit = artifacts.require('$ERC20Permit'); -const { Permit, getDomain, domainType, domainSeparator } = require('../../../helpers/eip712'); +const { + types: { Permit }, + getDomain, + domainType, + domainSeparator, +} = require('../../../helpers/eip712'); const { getChainId } = require('../../../helpers/chainid'); const { expectRevertCustomError } = require('../../../helpers/customError'); diff --git a/test/token/ERC20/extensions/ERC20Votes.test.js b/test/token/ERC20/extensions/ERC20Votes.test.js index faf1a15adb4..96d6c4e7708 100644 --- a/test/token/ERC20/extensions/ERC20Votes.test.js +++ b/test/token/ERC20/extensions/ERC20Votes.test.js @@ -10,16 +10,14 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; const { batchInBlock } = require('../../../helpers/txpool'); -const { getDomain, domainType } = require('../../../helpers/eip712'); +const { + getDomain, + domainType, + types: { Delegation }, +} = require('../../../helpers/eip712'); const { clock, clockFromReceipt } = require('../../../helpers/time'); const { expectRevertCustomError } = require('../../../helpers/customError'); -const Delegation = [ - { name: 'delegatee', type: 'address' }, - { name: 'nonce', type: 'uint256' }, - { name: 'expiry', type: 'uint256' }, -]; - const MODES = { blocknumber: artifacts.require('$ERC20Votes'), timestamp: artifacts.require('$ERC20VotesTimestampMock'), diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js index 9d0bdae9a9f..2b88f5f8edc 100644 --- a/test/utils/cryptography/EIP712.test.js +++ b/test/utils/cryptography/EIP712.test.js @@ -1,38 +1,39 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { getDomain, domainType, domainSeparator, hashTypedData } = require('../../helpers/eip712'); -const { getChainId } = require('../../helpers/chainid'); -const { mapValues } = require('../../helpers/iterate'); - -const EIP712Verifier = artifacts.require('$EIP712Verifier'); -const Clones = artifacts.require('$Clones'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -contract('EIP712', function (accounts) { - const [mailTo] = accounts; - - const shortName = 'A Name'; - const shortVersion = '1'; +const { getDomain, domainSeparator, hashTypedData } = require('../../helpers/eip712'); +const { getChainId } = require('../../helpers/chainid'); - const longName = 'A'.repeat(40); - const longVersion = 'B'.repeat(40); +const LENGTHS = { + short: ['A Name', '1'], + long: ['A'.repeat(40), 'B'.repeat(40)], +}; + +const fixture = async () => { + const [from, to] = await ethers.getSigners(); + + const lengths = {}; + for (const [shortOrLong, [name, version]] of Object.entries(LENGTHS)) { + lengths[shortOrLong] = { name, version }; + lengths[shortOrLong].eip712 = await ethers.deployContract('$EIP712Verifier', [name, version]); + lengths[shortOrLong].domain = { + name, + version, + chainId: await getChainId(), + verifyingContract: lengths[shortOrLong].eip712.target, + }; + } - const cases = [ - ['short', shortName, shortVersion], - ['long', longName, longVersion], - ]; + return { from, to, lengths }; +}; - for (const [shortOrLong, name, version] of cases) { +describe('EIP712', function () { + for (const [shortOrLong, [name, version]] of Object.entries(LENGTHS)) { describe(`with ${shortOrLong} name and version`, function () { beforeEach('deploying', async function () { - this.eip712 = await EIP712Verifier.new(name, version); - - this.domain = { - name, - version, - chainId: await getChainId(), - verifyingContract: this.eip712.address, - }; - this.domainType = domainType(this.domain); + Object.assign(this, await loadFixture(fixture)); + Object.assign(this, this.lengths[shortOrLong]); }); describe('domain separator', function () { @@ -44,7 +45,7 @@ contract('EIP712', function (accounts) { it("can be rebuilt using EIP-5267's eip712Domain", async function () { const rebuildDomain = await getDomain(this.eip712); - expect(mapValues(rebuildDomain, String)).to.be.deep.equal(mapValues(this.domain, String)); + expect(rebuildDomain).to.be.deep.equal(this.domain); }); if (shortOrLong === 'short') { @@ -52,33 +53,29 @@ contract('EIP712', function (accounts) { // the upgradeable contract variant is used and the initializer is invoked. it('adjusts when behind proxy', async function () { - const factory = await Clones.new(); - const cloneReceipt = await factory.$clone(this.eip712.address); - const cloneAddress = cloneReceipt.logs.find(({ event }) => event === 'return$clone').args.instance; - const clone = new EIP712Verifier(cloneAddress); + const factory = await ethers.deployContract('$Clones'); - const cloneDomain = { ...this.domain, verifyingContract: clone.address }; + const clone = await factory + .$clone(this.eip712) + .then(tx => tx.wait()) + .then(receipt => receipt.logs.find(ev => ev.fragment.name == 'return$clone').args.instance) + .then(address => ethers.getContractAt('$EIP712Verifier', address)); - const reportedDomain = await getDomain(clone); - expect(mapValues(reportedDomain, String)).to.be.deep.equal(mapValues(cloneDomain, String)); + const expectedDomain = { ...this.domain, verifyingContract: clone.target }; + expect(await getDomain(clone)).to.be.deep.equal(expectedDomain); - const expectedSeparator = await domainSeparator(cloneDomain); + const expectedSeparator = await domainSeparator(expectedDomain); expect(await clone.$_domainSeparatorV4()).to.equal(expectedSeparator); }); } }); it('hash digest', async function () { - const structhash = web3.utils.randomHex(32); - expect(await this.eip712.$_hashTypedDataV4(structhash)).to.be.equal(hashTypedData(this.domain, structhash)); + const structhash = ethers.hexlify(ethers.randomBytes(32)); + expect(await this.eip712.$_hashTypedDataV4(structhash)).to.equal(hashTypedData(this.domain, structhash)); }); it('digest', async function () { - const message = { - to: mailTo, - contents: 'very interesting', - }; - const types = { Mail: [ { name: 'to', type: 'address' }, @@ -86,19 +83,22 @@ contract('EIP712', function (accounts) { ], }; - const signer = ethers.Wallet.createRandom(); - const address = await signer.getAddress(); - const signature = await signer.signTypedData(this.domain, types, message); + const message = { + to: this.to.address, + contents: 'very interesting', + }; + + const signature = await this.from.signTypedData(this.domain, types, message); - await this.eip712.verify(signature, address, message.to, message.contents); + await expect(this.eip712.verify(signature, this.from.address, message.to, message.contents)).to.not.be.reverted; }); it('name', async function () { - expect(await this.eip712.$_EIP712Name()).to.be.equal(name); + expect(await this.eip712.$_EIP712Name()).to.equal(name); }); it('version', async function () { - expect(await this.eip712.$_EIP712Version()).to.be.equal(version); + expect(await this.eip712.$_EIP712Version()).to.equal(version); }); }); } From bf75bccaea1b596250d013991057aef5b515bc6b Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Thu, 23 Nov 2023 04:52:44 +0000 Subject: [PATCH 13/44] Migrate address to ethersjs (#4739) Co-authored-by: Hadrien Croubois Co-authored-by: ernestognw --- test/utils/Address.test.js | 326 ++++++++++++++++--------------------- 1 file changed, 136 insertions(+), 190 deletions(-) diff --git a/test/utils/Address.test.js b/test/utils/Address.test.js index 57453abd580..6186d18a70f 100644 --- a/test/utils/Address.test.js +++ b/test/utils/Address.test.js @@ -1,333 +1,279 @@ -const { balance, constants, ether, expectRevert, send, expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { expectRevertCustomError } = require('../helpers/customError'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); -const Address = artifacts.require('$Address'); -const EtherReceiver = artifacts.require('EtherReceiverMock'); -const CallReceiverMock = artifacts.require('CallReceiverMock'); +const coder = ethers.AbiCoder.defaultAbiCoder(); -contract('Address', function (accounts) { - const [recipient, other] = accounts; +async function fixture() { + const [recipient, other] = await ethers.getSigners(); + const mock = await ethers.deployContract('$Address'); + const target = await ethers.deployContract('CallReceiverMock'); + const targetEther = await ethers.deployContract('EtherReceiverMock'); + + return { recipient, other, mock, target, targetEther }; +} + +describe('Address', function () { beforeEach(async function () { - this.mock = await Address.new(); + Object.assign(this, await loadFixture(fixture)); }); describe('sendValue', function () { - beforeEach(async function () { - this.recipientTracker = await balance.tracker(recipient); - }); - - context('when sender contract has no funds', function () { + describe('when sender contract has no funds', function () { it('sends 0 wei', async function () { - await this.mock.$sendValue(other, 0); - - expect(await this.recipientTracker.delta()).to.be.bignumber.equal('0'); + await expect(this.mock.$sendValue(this.other, 0)).to.changeEtherBalance(this.recipient, 0); }); it('reverts when sending non-zero amounts', async function () { - await expectRevertCustomError(this.mock.$sendValue(other, 1), 'AddressInsufficientBalance', [ - this.mock.address, - ]); + await expect(this.mock.$sendValue(this.other, 1)) + .to.be.revertedWithCustomError(this.mock, 'AddressInsufficientBalance') + .withArgs(this.mock.target); }); }); - context('when sender contract has funds', function () { - const funds = ether('1'); - beforeEach(async function () { - await send.ether(other, this.mock.address, funds); - }); + describe('when sender contract has funds', function () { + const funds = ethers.parseEther('1'); - it('sends 0 wei', async function () { - await this.mock.$sendValue(recipient, 0); - expect(await this.recipientTracker.delta()).to.be.bignumber.equal('0'); + beforeEach(async function () { + await this.other.sendTransaction({ to: this.mock, value: funds }); }); - it('sends non-zero amounts', async function () { - await this.mock.$sendValue(recipient, funds.subn(1)); - expect(await this.recipientTracker.delta()).to.be.bignumber.equal(funds.subn(1)); - }); + describe('with EOA recipient', function () { + it('sends 0 wei', async function () { + await expect(this.mock.$sendValue(this.recipient, 0)).to.changeEtherBalance(this.recipient.address, 0); + }); - it('sends the whole balance', async function () { - await this.mock.$sendValue(recipient, funds); - expect(await this.recipientTracker.delta()).to.be.bignumber.equal(funds); - expect(await balance.current(this.mock.address)).to.be.bignumber.equal('0'); - }); + it('sends non-zero amounts', async function () { + await expect(this.mock.$sendValue(this.recipient, funds - 1n)).to.changeEtherBalance( + this.recipient, + funds - 1n, + ); + }); - it('reverts when sending more than the balance', async function () { - await expectRevertCustomError(this.mock.$sendValue(recipient, funds.addn(1)), 'AddressInsufficientBalance', [ - this.mock.address, - ]); - }); + it('sends the whole balance', async function () { + await expect(this.mock.$sendValue(this.recipient, funds)).to.changeEtherBalance(this.recipient, funds); + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + }); - context('with contract recipient', function () { - beforeEach(async function () { - this.target = await EtherReceiver.new(); + it('reverts when sending more than the balance', async function () { + await expect(this.mock.$sendValue(this.recipient, funds + 1n)) + .to.be.revertedWithCustomError(this.mock, 'AddressInsufficientBalance') + .withArgs(this.mock.target); }); + }); + describe('with contract recipient', function () { it('sends funds', async function () { - const tracker = await balance.tracker(this.target.address); - - await this.target.setAcceptEther(true); - await this.mock.$sendValue(this.target.address, funds); - - expect(await tracker.delta()).to.be.bignumber.equal(funds); + await this.targetEther.setAcceptEther(true); + await expect(this.mock.$sendValue(this.targetEther, funds)).to.changeEtherBalance(this.targetEther, funds); }); it('reverts on recipient revert', async function () { - await this.target.setAcceptEther(false); - await expectRevertCustomError(this.mock.$sendValue(this.target.address, funds), 'FailedInnerCall', []); + await this.targetEther.setAcceptEther(false); + await expect(this.mock.$sendValue(this.targetEther, funds)).to.be.revertedWithCustomError( + this.mock, + 'FailedInnerCall', + ); }); }); }); }); describe('functionCall', function () { - beforeEach(async function () { - this.target = await CallReceiverMock.new(); - }); - - context('with valid contract receiver', function () { + describe('with valid contract receiver', function () { it('calls the requested function', async function () { - const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); + const call = this.target.interface.encodeFunctionData('mockFunction'); - const receipt = await this.mock.$functionCall(this.target.address, abiEncodedCall); - - expectEvent(receipt, 'return$functionCall', { - ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']), - }); - await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); + await expect(this.mock.$functionCall(this.target, call)) + .to.emit(this.target, 'MockFunctionCalled') + .to.emit(this.mock, 'return$functionCall') + .withArgs(coder.encode(['string'], ['0x1234'])); }); it('calls the requested empty return function', async function () { - const abiEncodedCall = this.target.contract.methods.mockFunctionEmptyReturn().encodeABI(); + const call = this.target.interface.encodeFunctionData('mockFunctionEmptyReturn'); - const receipt = await this.mock.$functionCall(this.target.address, abiEncodedCall); - - await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); + await expect(this.mock.$functionCall(this.target, call)).to.emit(this.target, 'MockFunctionCalled'); }); it('reverts when the called function reverts with no reason', async function () { - const abiEncodedCall = this.target.contract.methods.mockFunctionRevertsNoReason().encodeABI(); + const call = this.target.interface.encodeFunctionData('mockFunctionRevertsNoReason'); - await expectRevertCustomError( - this.mock.$functionCall(this.target.address, abiEncodedCall), + await expect(this.mock.$functionCall(this.target, call)).to.be.revertedWithCustomError( + this.mock, 'FailedInnerCall', - [], ); }); it('reverts when the called function reverts, bubbling up the revert reason', async function () { - const abiEncodedCall = this.target.contract.methods.mockFunctionRevertsReason().encodeABI(); + const call = this.target.interface.encodeFunctionData('mockFunctionRevertsReason'); - await expectRevert(this.mock.$functionCall(this.target.address, abiEncodedCall), 'CallReceiverMock: reverting'); + await expect(this.mock.$functionCall(this.target, call)).to.be.revertedWith('CallReceiverMock: reverting'); }); it('reverts when the called function runs out of gas', async function () { - const abiEncodedCall = this.target.contract.methods.mockFunctionOutOfGas().encodeABI(); + const call = this.target.interface.encodeFunctionData('mockFunctionOutOfGas'); - await expectRevertCustomError( - this.mock.$functionCall(this.target.address, abiEncodedCall, { gas: '120000' }), + await expect(this.mock.$functionCall(this.target, call, { gasLimit: 120_000n })).to.be.revertedWithCustomError( + this.mock, 'FailedInnerCall', - [], ); }); it('reverts when the called function throws', async function () { - const abiEncodedCall = this.target.contract.methods.mockFunctionThrows().encodeABI(); + const call = this.target.interface.encodeFunctionData('mockFunctionThrows'); - await expectRevert.unspecified(this.mock.$functionCall(this.target.address, abiEncodedCall)); + await expect(this.mock.$functionCall(this.target, call)).to.be.revertedWithPanic(PANIC_CODES.ASSERTION_ERROR); }); it('reverts when function does not exist', async function () { - const abiEncodedCall = web3.eth.abi.encodeFunctionCall( - { - name: 'mockFunctionDoesNotExist', - type: 'function', - inputs: [], - }, - [], - ); + const interface = new ethers.Interface(['function mockFunctionDoesNotExist()']); + const call = interface.encodeFunctionData('mockFunctionDoesNotExist'); - await expectRevertCustomError( - this.mock.$functionCall(this.target.address, abiEncodedCall), + await expect(this.mock.$functionCall(this.target, call)).to.be.revertedWithCustomError( + this.mock, 'FailedInnerCall', - [], ); }); }); - context('with non-contract receiver', function () { + describe('with non-contract receiver', function () { it('reverts when address is not a contract', async function () { - const [recipient] = accounts; - const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); + const call = this.target.interface.encodeFunctionData('mockFunction'); - await expectRevertCustomError(this.mock.$functionCall(recipient, abiEncodedCall), 'AddressEmptyCode', [ - recipient, - ]); + await expect(this.mock.$functionCall(this.recipient, call)) + .to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode') + .withArgs(this.recipient.address); }); }); }); describe('functionCallWithValue', function () { - beforeEach(async function () { - this.target = await CallReceiverMock.new(); - }); - - context('with zero value', function () { + describe('with zero value', function () { it('calls the requested function', async function () { - const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); + const call = this.target.interface.encodeFunctionData('mockFunction'); - const receipt = await this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, 0); - expectEvent(receipt, 'return$functionCallWithValue', { - ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']), - }); - await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); + await expect(this.mock.$functionCallWithValue(this.target, call, 0)) + .to.emit(this.target, 'MockFunctionCalled') + .to.emit(this.mock, 'return$functionCallWithValue') + .withArgs(coder.encode(['string'], ['0x1234'])); }); }); - context('with non-zero value', function () { - const amount = ether('1.2'); + describe('with non-zero value', function () { + const value = ethers.parseEther('1.2'); it('reverts if insufficient sender balance', async function () { - const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); + const call = this.target.interface.encodeFunctionData('mockFunction'); - await expectRevertCustomError( - this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount), - 'AddressInsufficientBalance', - [this.mock.address], - ); + await expect(this.mock.$functionCallWithValue(this.target, call, value)) + .to.be.revertedWithCustomError(this.mock, 'AddressInsufficientBalance') + .withArgs(this.mock.target); }); it('calls the requested function with existing value', async function () { - const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); + await this.other.sendTransaction({ to: this.mock, value }); - const tracker = await balance.tracker(this.target.address); + const call = this.target.interface.encodeFunctionData('mockFunction'); + const tx = await this.mock.$functionCallWithValue(this.target, call, value); - await send.ether(other, this.mock.address, amount); + await expect(tx).to.changeEtherBalance(this.target, value); - const receipt = await this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount); - expectEvent(receipt, 'return$functionCallWithValue', { - ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']), - }); - await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); - - expect(await tracker.delta()).to.be.bignumber.equal(amount); + await expect(tx) + .to.emit(this.target, 'MockFunctionCalled') + .to.emit(this.mock, 'return$functionCallWithValue') + .withArgs(coder.encode(['string'], ['0x1234'])); }); it('calls the requested function with transaction funds', async function () { - const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); - - const tracker = await balance.tracker(this.target.address); + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); - expect(await balance.current(this.mock.address)).to.be.bignumber.equal('0'); + const call = this.target.interface.encodeFunctionData('mockFunction'); + const tx = await this.mock.connect(this.other).$functionCallWithValue(this.target, call, value, { value }); - const receipt = await this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount, { - from: other, - value: amount, - }); - expectEvent(receipt, 'return$functionCallWithValue', { - ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']), - }); - await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); - - expect(await tracker.delta()).to.be.bignumber.equal(amount); + await expect(tx).to.changeEtherBalance(this.target, value); + await expect(tx) + .to.emit(this.target, 'MockFunctionCalled') + .to.emit(this.mock, 'return$functionCallWithValue') + .withArgs(coder.encode(['string'], ['0x1234'])); }); it('reverts when calling non-payable functions', async function () { - const abiEncodedCall = this.target.contract.methods.mockFunctionNonPayable().encodeABI(); + await this.other.sendTransaction({ to: this.mock, value }); + + const call = this.target.interface.encodeFunctionData('mockFunctionNonPayable'); - await send.ether(other, this.mock.address, amount); - await expectRevertCustomError( - this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount), + await expect(this.mock.$functionCallWithValue(this.target, call, value)).to.be.revertedWithCustomError( + this.mock, 'FailedInnerCall', - [], ); }); }); }); describe('functionStaticCall', function () { - beforeEach(async function () { - this.target = await CallReceiverMock.new(); - }); - it('calls the requested function', async function () { - const abiEncodedCall = this.target.contract.methods.mockStaticFunction().encodeABI(); + const call = this.target.interface.encodeFunctionData('mockStaticFunction'); - expect(await this.mock.$functionStaticCall(this.target.address, abiEncodedCall)).to.be.equal( - web3.eth.abi.encodeParameters(['string'], ['0x1234']), - ); + expect(await this.mock.$functionStaticCall(this.target, call)).to.equal(coder.encode(['string'], ['0x1234'])); }); it('reverts on a non-static function', async function () { - const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); + const call = this.target.interface.encodeFunctionData('mockFunction'); - await expectRevertCustomError( - this.mock.$functionStaticCall(this.target.address, abiEncodedCall), + await expect(this.mock.$functionStaticCall(this.target, call)).to.be.revertedWithCustomError( + this.mock, 'FailedInnerCall', - [], ); }); it('bubbles up revert reason', async function () { - const abiEncodedCall = this.target.contract.methods.mockFunctionRevertsReason().encodeABI(); + const call = this.target.interface.encodeFunctionData('mockFunctionRevertsReason'); - await expectRevert( - this.mock.$functionStaticCall(this.target.address, abiEncodedCall), - 'CallReceiverMock: reverting', - ); + await expect(this.mock.$functionStaticCall(this.target, call)).to.be.revertedWith('CallReceiverMock: reverting'); }); it('reverts when address is not a contract', async function () { - const [recipient] = accounts; - const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); + const call = this.target.interface.encodeFunctionData('mockFunction'); - await expectRevertCustomError(this.mock.$functionStaticCall(recipient, abiEncodedCall), 'AddressEmptyCode', [ - recipient, - ]); + await expect(this.mock.$functionStaticCall(this.recipient, call)) + .to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode') + .withArgs(this.recipient.address); }); }); describe('functionDelegateCall', function () { - beforeEach(async function () { - this.target = await CallReceiverMock.new(); - }); - it('delegate calls the requested function', async function () { - // pseudorandom values - const slot = '0x93e4c53af435ddf777c3de84bb9a953a777788500e229a468ea1036496ab66a0'; - const value = '0x6a465d1c49869f71fb65562bcbd7e08c8044074927f0297127203f2a9924ff5b'; + const slot = ethers.hexlify(ethers.randomBytes(32)); + const value = ethers.hexlify(ethers.randomBytes(32)); - const abiEncodedCall = this.target.contract.methods.mockFunctionWritesStorage(slot, value).encodeABI(); + const call = this.target.interface.encodeFunctionData('mockFunctionWritesStorage', [slot, value]); - expect(await web3.eth.getStorageAt(this.mock.address, slot)).to.be.equal(constants.ZERO_BYTES32); + expect(await ethers.provider.getStorage(this.mock, slot)).to.equal(ethers.ZeroHash); - expectEvent( - await this.mock.$functionDelegateCall(this.target.address, abiEncodedCall), - 'return$functionDelegateCall', - { ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']) }, - ); + await expect(await this.mock.$functionDelegateCall(this.target, call)) + .to.emit(this.mock, 'return$functionDelegateCall') + .withArgs(coder.encode(['string'], ['0x1234'])); - expect(await web3.eth.getStorageAt(this.mock.address, slot)).to.be.equal(value); + expect(await ethers.provider.getStorage(this.mock, slot)).to.equal(value); }); it('bubbles up revert reason', async function () { - const abiEncodedCall = this.target.contract.methods.mockFunctionRevertsReason().encodeABI(); + const call = this.target.interface.encodeFunctionData('mockFunctionRevertsReason'); - await expectRevert( - this.mock.$functionDelegateCall(this.target.address, abiEncodedCall), + await expect(this.mock.$functionDelegateCall(this.target, call)).to.be.revertedWith( 'CallReceiverMock: reverting', ); }); it('reverts when address is not a contract', async function () { - const [recipient] = accounts; - const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); + const call = this.target.interface.encodeFunctionData('mockFunction'); - await expectRevertCustomError(this.mock.$functionDelegateCall(recipient, abiEncodedCall), 'AddressEmptyCode', [ - recipient, - ]); + await expect(this.mock.$functionDelegateCall(this.recipient, call)) + .to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode') + .withArgs(this.recipient.address); }); }); From 7bd2b2aaf68c21277097166a9a51eb72ae239b34 Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Thu, 23 Nov 2023 05:18:04 +0000 Subject: [PATCH 14/44] Use ERC-XXX syntax (#4730) Co-authored-by: Hadrien Croubois Co-authored-by: ernestognw --- GUIDELINES.md | 2 +- certora/specs/ERC20FlashMint.spec | 2 +- contracts/access/IAccessControl.sol | 2 +- .../IAccessControlDefaultAdminRules.sol | 2 +- .../extensions/IAccessControlEnumerable.sol | 2 +- contracts/finance/README.adoc | 2 +- contracts/finance/VestingWallet.sol | 4 +-- contracts/governance/IGovernor.sol | 8 ++--- contracts/governance/README.adoc | 6 ++-- .../governance/extensions/GovernorVotes.sol | 6 ++-- contracts/governance/utils/Votes.sol | 2 +- contracts/interfaces/IERC1271.sol | 2 +- contracts/interfaces/IERC1363.sol | 6 ++-- contracts/interfaces/IERC1363Receiver.sol | 4 +-- contracts/interfaces/IERC1363Spender.sol | 4 +-- contracts/interfaces/IERC1820Implementer.sol | 4 +-- contracts/interfaces/IERC1820Registry.sol | 20 ++++++------- .../interfaces/IERC3156FlashBorrower.sol | 2 +- contracts/interfaces/IERC3156FlashLender.sol | 2 +- contracts/interfaces/IERC4626.sol | 2 +- contracts/interfaces/IERC4906.sol | 2 +- contracts/interfaces/IERC777.sol | 4 +-- contracts/interfaces/IERC777Recipient.sol | 4 +-- contracts/interfaces/IERC777Sender.sol | 4 +-- contracts/interfaces/draft-IERC1822.sol | 2 +- contracts/interfaces/draft-IERC6093.sol | 14 ++++----- contracts/metatx/ERC2771Context.sol | 4 +-- contracts/metatx/ERC2771Forwarder.sol | 2 +- .../ERC165/ERC165InterfacesSupported.sol | 2 +- contracts/mocks/docs/ERC4626Fees.sol | 2 +- contracts/proxy/Clones.sol | 2 +- contracts/proxy/ERC1967/ERC1967Proxy.sol | 4 +-- contracts/proxy/ERC1967/ERC1967Utils.sol | 10 +++---- contracts/proxy/README.adoc | 16 +++++----- contracts/proxy/beacon/BeaconProxy.sol | 2 +- contracts/proxy/utils/UUPSUpgradeable.sol | 10 +++---- contracts/token/ERC1155/ERC1155.sol | 8 ++--- contracts/token/ERC1155/IERC1155.sol | 4 +-- contracts/token/ERC1155/IERC1155Receiver.sol | 4 +-- contracts/token/ERC1155/README.adoc | 8 ++--- .../ERC1155/extensions/ERC1155Pausable.sol | 2 +- .../ERC1155/extensions/ERC1155Supply.sol | 2 +- .../ERC1155/extensions/ERC1155URIStorage.sol | 4 +-- .../extensions/IERC1155MetadataURI.sol | 2 +- .../token/ERC1155/utils/ERC1155Holder.sol | 2 +- contracts/token/ERC20/ERC20.sol | 6 ++-- contracts/token/ERC20/IERC20.sol | 2 +- contracts/token/ERC20/README.adoc | 30 +++++++++---------- .../token/ERC20/extensions/ERC20FlashMint.sol | 2 +- .../token/ERC20/extensions/ERC20Pausable.sol | 2 +- .../token/ERC20/extensions/ERC20Permit.sol | 8 ++--- .../token/ERC20/extensions/ERC20Votes.sol | 2 +- .../token/ERC20/extensions/ERC20Wrapper.sol | 4 +-- contracts/token/ERC20/extensions/ERC4626.sol | 14 ++++----- .../token/ERC20/extensions/IERC20Metadata.sol | 2 +- .../token/ERC20/extensions/IERC20Permit.sol | 6 ++-- contracts/token/ERC20/utils/SafeERC20.sol | 4 +-- contracts/token/ERC721/ERC721.sol | 6 ++-- contracts/token/ERC721/IERC721.sol | 6 ++-- contracts/token/ERC721/IERC721Receiver.sol | 4 +-- contracts/token/ERC721/README.adoc | 16 +++++----- .../ERC721/extensions/ERC721Burnable.sol | 4 +-- .../ERC721/extensions/ERC721Consecutive.sol | 8 ++--- .../ERC721/extensions/ERC721Enumerable.sol | 6 ++-- .../ERC721/extensions/ERC721Pausable.sol | 2 +- .../token/ERC721/extensions/ERC721Royalty.sol | 4 +-- .../ERC721/extensions/ERC721URIStorage.sol | 2 +- .../token/ERC721/extensions/ERC721Votes.sol | 2 +- .../token/ERC721/extensions/ERC721Wrapper.sol | 8 ++--- contracts/token/common/ERC2981.sol | 2 +- contracts/token/common/README.adoc | 4 +-- contracts/utils/README.adoc | 2 +- contracts/utils/StorageSlot.sol | 2 +- contracts/utils/cryptography/ECDSA.sol | 2 +- contracts/utils/cryptography/EIP712.sol | 6 ++-- .../utils/cryptography/MessageHashUtils.sol | 10 +++---- .../utils/cryptography/SignatureChecker.sol | 6 ++-- contracts/utils/introspection/ERC165.sol | 2 +- .../utils/introspection/ERC165Checker.sol | 14 ++++----- contracts/utils/introspection/IERC165.sol | 6 ++-- docs/modules/ROOT/nav.adoc | 8 ++--- docs/modules/ROOT/pages/access-control.adoc | 10 +++---- docs/modules/ROOT/pages/erc1155.adoc | 28 ++++++++--------- docs/modules/ROOT/pages/erc20-supply.adoc | 12 ++++---- docs/modules/ROOT/pages/erc20.adoc | 12 ++++---- docs/modules/ROOT/pages/erc4626.adoc | 10 +++---- docs/modules/ROOT/pages/erc721.adoc | 18 +++++------ docs/modules/ROOT/pages/governance.adoc | 14 ++++----- docs/modules/ROOT/pages/tokens.adoc | 8 ++--- docs/modules/ROOT/pages/utilities.adoc | 14 ++++----- scripts/generate/templates/StorageSlot.js | 2 +- test/governance/Governor.test.js | 4 +-- ...IP6372.behavior.js => ERC6372.behavior.js} | 6 ++-- test/governance/utils/Votes.behavior.js | 4 +-- .../token/ERC20/extensions/ERC20Votes.test.js | 2 +- .../ERC721/extensions/ERC721Votes.test.js | 2 +- 96 files changed, 282 insertions(+), 282 deletions(-) rename test/governance/utils/{EIP6372.behavior.js => ERC6372.behavior.js} (81%) diff --git a/GUIDELINES.md b/GUIDELINES.md index 4b7f5e76e30..97fa7290cfe 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -115,7 +115,7 @@ In addition to the official Solidity Style Guide we have a number of other conve } ``` - Some standards (e.g. ERC20) use present tense, and in those cases the + Some standards (e.g. ERC-20) use present tense, and in those cases the standard specification is used. * Interface names should have a capital I prefix. diff --git a/certora/specs/ERC20FlashMint.spec b/certora/specs/ERC20FlashMint.spec index 5c87f0ded79..4071052ea7f 100644 --- a/certora/specs/ERC20FlashMint.spec +++ b/certora/specs/ERC20FlashMint.spec @@ -4,7 +4,7 @@ import "methods/IERC3156FlashLender.spec"; import "methods/IERC3156FlashBorrower.spec"; methods { - // non standard ERC3156 functions + // non standard ERC-3156 functions function flashFeeReceiver() external returns (address) envfree; // function summaries below diff --git a/contracts/access/IAccessControl.sol b/contracts/access/IAccessControl.sol index 2ac89ca7356..dbcd3957bf8 100644 --- a/contracts/access/IAccessControl.sol +++ b/contracts/access/IAccessControl.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; /** - * @dev External interface of AccessControl declared to support ERC165 detection. + * @dev External interface of AccessControl declared to support ERC-165 detection. */ interface IAccessControl { /** diff --git a/contracts/access/extensions/IAccessControlDefaultAdminRules.sol b/contracts/access/extensions/IAccessControlDefaultAdminRules.sol index 73531fafa3b..b74355aad31 100644 --- a/contracts/access/extensions/IAccessControlDefaultAdminRules.sol +++ b/contracts/access/extensions/IAccessControlDefaultAdminRules.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.20; import {IAccessControl} from "../IAccessControl.sol"; /** - * @dev External interface of AccessControlDefaultAdminRules declared to support ERC165 detection. + * @dev External interface of AccessControlDefaultAdminRules declared to support ERC-165 detection. */ interface IAccessControlDefaultAdminRules is IAccessControl { /** diff --git a/contracts/access/extensions/IAccessControlEnumerable.sol b/contracts/access/extensions/IAccessControlEnumerable.sol index a39d051666d..11429686355 100644 --- a/contracts/access/extensions/IAccessControlEnumerable.sol +++ b/contracts/access/extensions/IAccessControlEnumerable.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.20; import {IAccessControl} from "../IAccessControl.sol"; /** - * @dev External interface of AccessControlEnumerable declared to support ERC165 detection. + * @dev External interface of AccessControlEnumerable declared to support ERC-165 detection. */ interface IAccessControlEnumerable is IAccessControl { /** diff --git a/contracts/finance/README.adoc b/contracts/finance/README.adoc index ac7e4f015fa..c855cbb6a99 100644 --- a/contracts/finance/README.adoc +++ b/contracts/finance/README.adoc @@ -5,7 +5,7 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/ This directory includes primitives for financial systems: -- {VestingWallet} handles the vesting of Ether and ERC20 tokens for a given beneficiary. Custody of multiple tokens can +- {VestingWallet} handles the vesting of Ether and ERC-20 tokens for a given beneficiary. Custody of multiple tokens can be given to this contract, which will release the token to the beneficiary following a given, customizable, vesting schedule. diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol index 5abb7cdad9d..f472b66068b 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -9,7 +9,7 @@ import {Context} from "../utils/Context.sol"; import {Ownable} from "../access/Ownable.sol"; /** - * @dev A vesting wallet is an ownable contract that can receive native currency and ERC20 tokens, and release these + * @dev A vesting wallet is an ownable contract that can receive native currency and ERC-20 tokens, and release these * assets to the wallet owner, also referred to as "beneficiary", according to a vesting schedule. * * Any assets transferred to this contract will follow the vesting schedule as if they were locked from the beginning. @@ -94,7 +94,7 @@ contract VestingWallet is Context, Ownable { /** * @dev Getter for the amount of releasable `token` tokens. `token` should be the address of an - * IERC20 contract. + * {IERC20} contract. */ function releasable(address token) public view virtual returns (uint256) { return vestedAmount(token, uint64(block.timestamp)) - released(token); diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 6cde0e86dc9..85fc281d7f9 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -158,13 +158,13 @@ interface IGovernor is IERC165, IERC6372 { /** * @notice module:core - * @dev Name of the governor instance (used in building the ERC712 domain separator). + * @dev Name of the governor instance (used in building the EIP-712 domain separator). */ function name() external view returns (string memory); /** * @notice module:core - * @dev Version of the governor instance (used in building the ERC712 domain separator). Default: "1" + * @dev Version of the governor instance (used in building the EIP-712 domain separator). Default: "1" */ function version() external view returns (string memory); @@ -254,7 +254,7 @@ interface IGovernor is IERC165, IERC6372 { /** * @notice module:user-config * @dev Delay, between the proposal is created and the vote starts. The unit this duration is expressed in depends - * on the clock (see EIP-6372) this contract uses. + * on the clock (see ERC-6372) this contract uses. * * This can be increased to leave time for users to buy voting power, or delegate it, before the voting of a * proposal starts. @@ -267,7 +267,7 @@ interface IGovernor is IERC165, IERC6372 { /** * @notice module:user-config * @dev Delay between the vote start and vote end. The unit this duration is expressed in depends on the clock - * (see EIP-6372) this contract uses. + * (see ERC-6372) this contract uses. * * NOTE: The {votingDelay} can delay the start of the vote. This must be considered when setting the voting * duration compared to the voting delay. diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index 5b38c4d53f8..323ffcc5cf6 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -44,9 +44,9 @@ Other extensions can customize the behavior or interface in multiple ways. In addition to modules and extensions, the core contract requires a few virtual functions to be implemented to your particular specifications: -* <>: Delay (in EIP-6372 clock) since the proposal is submitted until voting power is fixed and voting starts. This can be used to enforce a delay after a proposal is published for users to buy tokens, or delegate their votes. -* <>: Delay (in EIP-6372 clock) since the proposal starts until voting ends. -* <>: Quorum required for a proposal to be successful. This function includes a `timepoint` argument (see EIP-6372) so the quorum can adapt through time, for example, to follow a token's `totalSupply`. +* <>: Delay (in ERC-6372 clock) since the proposal is submitted until voting power is fixed and voting starts. This can be used to enforce a delay after a proposal is published for users to buy tokens, or delegate their votes. +* <>: Delay (in ERC-6372 clock) since the proposal starts until voting ends. +* <>: Quorum required for a proposal to be successful. This function includes a `timepoint` argument (see ERC-6372) so the quorum can adapt through time, for example, to follow a token's `totalSupply`. NOTE: Functions of the `Governor` contract do not include access control. If you want to restrict access, you should add these checks by overloading the particular functions. Among these, {Governor-_cancel} is internal by default, and you will have to expose it (with the right access control mechanism) yourself if this function is needed. diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol index ec32ba47806..16cd934355e 100644 --- a/contracts/governance/extensions/GovernorVotes.sol +++ b/contracts/governance/extensions/GovernorVotes.sol @@ -28,8 +28,8 @@ abstract contract GovernorVotes is Governor { } /** - * @dev Clock (as specified in EIP-6372) is set to match the token's clock. Fallback to block numbers if the token - * does not implement EIP-6372. + * @dev Clock (as specified in ERC-6372) is set to match the token's clock. Fallback to block numbers if the token + * does not implement ERC-6372. */ function clock() public view virtual override returns (uint48) { try token().clock() returns (uint48 timepoint) { @@ -40,7 +40,7 @@ abstract contract GovernorVotes is Governor { } /** - * @dev Machine-readable description of the clock as specified in EIP-6372. + * @dev Machine-readable description of the clock as specified in ERC-6372. */ // solhint-disable-next-line func-name-mixedcase function CLOCK_MODE() public view virtual override returns (string memory) { diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index 9f96676532b..b4a83a055e2 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -60,7 +60,7 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { } /** - * @dev Machine-readable description of the clock as specified in EIP-6372. + * @dev Machine-readable description of the clock as specified in ERC-6372. */ // solhint-disable-next-line func-name-mixedcase function CLOCK_MODE() public view virtual returns (string memory) { diff --git a/contracts/interfaces/IERC1271.sol b/contracts/interfaces/IERC1271.sol index a56057ba5e7..5f5b326336e 100644 --- a/contracts/interfaces/IERC1271.sol +++ b/contracts/interfaces/IERC1271.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; /** - * @dev Interface of the ERC1271 standard signature validation method for + * @dev Interface of the ERC-1271 standard signature validation method for * contracts as defined in https://eips.ethereum.org/EIPS/eip-1271[ERC-1271]. */ interface IERC1271 { diff --git a/contracts/interfaces/IERC1363.sol b/contracts/interfaces/IERC1363.sol index 8b02aba5e1f..4a655b80c4f 100644 --- a/contracts/interfaces/IERC1363.sol +++ b/contracts/interfaces/IERC1363.sol @@ -7,10 +7,10 @@ import {IERC20} from "./IERC20.sol"; import {IERC165} from "./IERC165.sol"; /** - * @dev Interface of an ERC1363 compliant contract, as defined in the - * https://eips.ethereum.org/EIPS/eip-1363[EIP]. + * @dev Interface of an ERC-1363 compliant contract, as defined in the + * https://eips.ethereum.org/EIPS/eip-1363[ERC]. * - * Defines a interface for ERC20 tokens that supports executing recipient + * Defines a interface for ERC-20 tokens that supports executing recipient * code after `transfer` or `transferFrom`, or spender code after `approve`. */ interface IERC1363 is IERC165, IERC20 { diff --git a/contracts/interfaces/IERC1363Receiver.sol b/contracts/interfaces/IERC1363Receiver.sol index 64d669d4ac3..04e5dce8c7e 100644 --- a/contracts/interfaces/IERC1363Receiver.sol +++ b/contracts/interfaces/IERC1363Receiver.sol @@ -14,8 +14,8 @@ interface IERC1363Receiver { */ /** - * @notice Handle the receipt of ERC1363 tokens - * @dev Any ERC1363 smart contract calls this function on the recipient + * @notice Handle the receipt of ERC-1363 tokens + * @dev Any ERC-1363 smart contract calls this function on the recipient * after a `transfer` or a `transferFrom`. This function MAY throw to revert and reject the * transfer. Return of other than the magic value MUST result in the * transaction being reverted. diff --git a/contracts/interfaces/IERC1363Spender.sol b/contracts/interfaces/IERC1363Spender.sol index f2215418a24..069e4ff80df 100644 --- a/contracts/interfaces/IERC1363Spender.sol +++ b/contracts/interfaces/IERC1363Spender.sol @@ -14,8 +14,8 @@ interface IERC1363Spender { */ /** - * @notice Handle the approval of ERC1363 tokens - * @dev Any ERC1363 smart contract calls this function on the recipient + * @notice Handle the approval of ERC-1363 tokens + * @dev Any ERC-1363 smart contract calls this function on the recipient * after an `approve`. This function MAY throw to revert and reject the * approval. Return of other than the magic value MUST result in the * transaction being reverted. diff --git a/contracts/interfaces/IERC1820Implementer.sol b/contracts/interfaces/IERC1820Implementer.sol index 38e8a4e9b38..9cf941a3343 100644 --- a/contracts/interfaces/IERC1820Implementer.sol +++ b/contracts/interfaces/IERC1820Implementer.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.20; /** - * @dev Interface for an ERC1820 implementer, as defined in the - * https://eips.ethereum.org/EIPS/eip-1820#interface-implementation-erc1820implementerinterface[EIP]. + * @dev Interface for an ERC-1820 implementer, as defined in the + * https://eips.ethereum.org/EIPS/eip-1820#interface-implementation-erc1820implementerinterface[ERC]. * Used by contracts that will be registered as implementers in the * {IERC1820Registry}. */ diff --git a/contracts/interfaces/IERC1820Registry.sol b/contracts/interfaces/IERC1820Registry.sol index bf0140a12c9..b8f3d73998c 100644 --- a/contracts/interfaces/IERC1820Registry.sol +++ b/contracts/interfaces/IERC1820Registry.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.20; /** - * @dev Interface of the global ERC1820 Registry, as defined in the - * https://eips.ethereum.org/EIPS/eip-1820[EIP]. Accounts may register + * @dev Interface of the global ERC-1820 Registry, as defined in the + * https://eips.ethereum.org/EIPS/eip-1820[ERC]. Accounts may register * implementers for interfaces in this registry, as well as query support. * * Implementers may be shared by multiple accounts, and can also implement more @@ -15,7 +15,7 @@ pragma solidity ^0.8.20; * * {IERC165} interfaces can also be queried via the registry. * - * For an in-depth explanation and source code analysis, see the EIP text. + * For an in-depth explanation and source code analysis, see the ERC text. */ interface IERC1820Registry { event InterfaceImplementerSet(address indexed account, bytes32 indexed interfaceHash, address indexed implementer); @@ -80,32 +80,32 @@ interface IERC1820Registry { /** * @dev Returns the interface hash for an `interfaceName`, as defined in the * corresponding - * https://eips.ethereum.org/EIPS/eip-1820#interface-name[section of the EIP]. + * https://eips.ethereum.org/EIPS/eip-1820#interface-name[section of the ERC]. */ function interfaceHash(string calldata interfaceName) external pure returns (bytes32); /** - * @notice Updates the cache with whether the contract implements an ERC165 interface or not. + * @notice Updates the cache with whether the contract implements an ERC-165 interface or not. * @param account Address of the contract for which to update the cache. - * @param interfaceId ERC165 interface for which to update the cache. + * @param interfaceId ERC-165 interface for which to update the cache. */ function updateERC165Cache(address account, bytes4 interfaceId) external; /** - * @notice Checks whether a contract implements an ERC165 interface or not. + * @notice Checks whether a contract implements an ERC-165 interface or not. * If the result is not cached a direct lookup on the contract address is performed. * If the result is not cached or the cached value is out-of-date, the cache MUST be updated manually by calling * {updateERC165Cache} with the contract address. * @param account Address of the contract to check. - * @param interfaceId ERC165 interface to check. + * @param interfaceId ERC-165 interface to check. * @return True if `account` implements `interfaceId`, false otherwise. */ function implementsERC165Interface(address account, bytes4 interfaceId) external view returns (bool); /** - * @notice Checks whether a contract implements an ERC165 interface or not without using or updating the cache. + * @notice Checks whether a contract implements an ERC-165 interface or not without using or updating the cache. * @param account Address of the contract to check. - * @param interfaceId ERC165 interface to check. + * @param interfaceId ERC-165 interface to check. * @return True if `account` implements `interfaceId`, false otherwise. */ function implementsERC165InterfaceNoCache(address account, bytes4 interfaceId) external view returns (bool); diff --git a/contracts/interfaces/IERC3156FlashBorrower.sol b/contracts/interfaces/IERC3156FlashBorrower.sol index 53e17ea634d..4fd10e7cfea 100644 --- a/contracts/interfaces/IERC3156FlashBorrower.sol +++ b/contracts/interfaces/IERC3156FlashBorrower.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; /** - * @dev Interface of the ERC3156 FlashBorrower, as defined in + * @dev Interface of the ERC-3156 FlashBorrower, as defined in * https://eips.ethereum.org/EIPS/eip-3156[ERC-3156]. */ interface IERC3156FlashBorrower { diff --git a/contracts/interfaces/IERC3156FlashLender.sol b/contracts/interfaces/IERC3156FlashLender.sol index cfae3c0b7d9..47208ac3112 100644 --- a/contracts/interfaces/IERC3156FlashLender.sol +++ b/contracts/interfaces/IERC3156FlashLender.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.20; import {IERC3156FlashBorrower} from "./IERC3156FlashBorrower.sol"; /** - * @dev Interface of the ERC3156 FlashLender, as defined in + * @dev Interface of the ERC-3156 FlashLender, as defined in * https://eips.ethereum.org/EIPS/eip-3156[ERC-3156]. */ interface IERC3156FlashLender { diff --git a/contracts/interfaces/IERC4626.sol b/contracts/interfaces/IERC4626.sol index cfff53b9738..9a24507a7fd 100644 --- a/contracts/interfaces/IERC4626.sol +++ b/contracts/interfaces/IERC4626.sol @@ -7,7 +7,7 @@ import {IERC20} from "../token/ERC20/IERC20.sol"; import {IERC20Metadata} from "../token/ERC20/extensions/IERC20Metadata.sol"; /** - * @dev Interface of the ERC4626 "Tokenized Vault Standard", as defined in + * @dev Interface of the ERC-4626 "Tokenized Vault Standard", as defined in * https://eips.ethereum.org/EIPS/eip-4626[ERC-4626]. */ interface IERC4626 is IERC20, IERC20Metadata { diff --git a/contracts/interfaces/IERC4906.sol b/contracts/interfaces/IERC4906.sol index bc008e3975b..cdcace31fcb 100644 --- a/contracts/interfaces/IERC4906.sol +++ b/contracts/interfaces/IERC4906.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.20; import {IERC165} from "./IERC165.sol"; import {IERC721} from "./IERC721.sol"; -/// @title EIP-721 Metadata Update Extension +/// @title ERC-721 Metadata Update Extension interface IERC4906 is IERC165, IERC721 { /// @dev This event emits when the metadata of a token is changed. /// So that the third-party platforms such as NFT market could diff --git a/contracts/interfaces/IERC777.sol b/contracts/interfaces/IERC777.sol index 56dfbef51b7..31f05aa6421 100644 --- a/contracts/interfaces/IERC777.sol +++ b/contracts/interfaces/IERC777.sol @@ -4,10 +4,10 @@ pragma solidity ^0.8.20; /** - * @dev Interface of the ERC777Token standard as defined in the EIP. + * @dev Interface of the ERC-777 Token standard as defined in the ERC. * * This contract uses the - * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 registry standard] to let + * https://eips.ethereum.org/EIPS/eip-1820[ERC-1820 registry standard] to let * token holders and recipients react to token movements by using setting implementers * for the associated interfaces in said registry. See {IERC1820Registry} and * {IERC1820Implementer}. diff --git a/contracts/interfaces/IERC777Recipient.sol b/contracts/interfaces/IERC777Recipient.sol index 6378e140917..1619e112679 100644 --- a/contracts/interfaces/IERC777Recipient.sol +++ b/contracts/interfaces/IERC777Recipient.sol @@ -4,12 +4,12 @@ pragma solidity ^0.8.20; /** - * @dev Interface of the ERC777TokensRecipient standard as defined in the EIP. + * @dev Interface of the ERC-777 Tokens Recipient standard as defined in the ERC. * * Accounts can be notified of {IERC777} tokens being sent to them by having a * contract implement this interface (contract holders can be their own * implementer) and registering it on the - * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 global registry]. + * https://eips.ethereum.org/EIPS/eip-1820[ERC-1820 global registry]. * * See {IERC1820Registry} and {IERC1820Implementer}. */ diff --git a/contracts/interfaces/IERC777Sender.sol b/contracts/interfaces/IERC777Sender.sol index 5c0ec0b57a1..f47a7832399 100644 --- a/contracts/interfaces/IERC777Sender.sol +++ b/contracts/interfaces/IERC777Sender.sol @@ -4,12 +4,12 @@ pragma solidity ^0.8.20; /** - * @dev Interface of the ERC777TokensSender standard as defined in the EIP. + * @dev Interface of the ERC-777 Tokens Sender standard as defined in the ERC. * * {IERC777} Token holders can be notified of operations performed on their * tokens by having a contract implement this interface (contract holders can be * their own implementer) and registering it on the - * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 global registry]. + * https://eips.ethereum.org/EIPS/eip-1820[ERC-1820 global registry]. * * See {IERC1820Registry} and {IERC1820Implementer}. */ diff --git a/contracts/interfaces/draft-IERC1822.sol b/contracts/interfaces/draft-IERC1822.sol index 4d0f0f8852d..ad047dae600 100644 --- a/contracts/interfaces/draft-IERC1822.sol +++ b/contracts/interfaces/draft-IERC1822.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; /** - * @dev ERC1822: Universal Upgradeable Proxy Standard (UUPS) documents a method for upgradeability through a simplified + * @dev ERC-1822: Universal Upgradeable Proxy Standard (UUPS) documents a method for upgradeability through a simplified * proxy whose upgrades are fully controlled by the current implementation. */ interface IERC1822Proxiable { diff --git a/contracts/interfaces/draft-IERC6093.sol b/contracts/interfaces/draft-IERC6093.sol index f6990e607c9..75fd75643e6 100644 --- a/contracts/interfaces/draft-IERC6093.sol +++ b/contracts/interfaces/draft-IERC6093.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.20; /** - * @dev Standard ERC20 Errors - * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC20 tokens. + * @dev Standard ERC-20 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-20 tokens. */ interface IERC20Errors { /** @@ -49,12 +49,12 @@ interface IERC20Errors { } /** - * @dev Standard ERC721 Errors - * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC721 tokens. + * @dev Standard ERC-721 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-721 tokens. */ interface IERC721Errors { /** - * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in EIP-20. + * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in ERC-20. * Used in balance queries. * @param owner Address of the current owner of a token. */ @@ -107,8 +107,8 @@ interface IERC721Errors { } /** - * @dev Standard ERC1155 Errors - * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC1155 tokens. + * @dev Standard ERC-1155 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-1155 tokens. */ interface IERC1155Errors { /** diff --git a/contracts/metatx/ERC2771Context.sol b/contracts/metatx/ERC2771Context.sol index ab9546bc851..0724a0b697c 100644 --- a/contracts/metatx/ERC2771Context.sol +++ b/contracts/metatx/ERC2771Context.sol @@ -6,10 +6,10 @@ pragma solidity ^0.8.20; import {Context} from "../utils/Context.sol"; /** - * @dev Context variant with ERC2771 support. + * @dev Context variant with ERC-2771 support. * * WARNING: Avoid using this pattern in contracts that rely in a specific calldata length as they'll - * be affected by any forwarder whose `msg.data` is suffixed with the `from` address according to the ERC2771 + * be affected by any forwarder whose `msg.data` is suffixed with the `from` address according to the ERC-2771 * specification adding the address size in bytes (20) to the calldata size. An example of an unexpected * behavior could be an unintended fallback (or another function) invocation while trying to invoke the `receive` * function only accessible if `msg.data.length == 0`. diff --git a/contracts/metatx/ERC2771Forwarder.sol b/contracts/metatx/ERC2771Forwarder.sol index 4815c1a1d99..221dabb627c 100644 --- a/contracts/metatx/ERC2771Forwarder.sol +++ b/contracts/metatx/ERC2771Forwarder.sol @@ -10,7 +10,7 @@ import {Nonces} from "../utils/Nonces.sol"; import {Address} from "../utils/Address.sol"; /** - * @dev A forwarder compatible with ERC2771 contracts. See {ERC2771Context}. + * @dev A forwarder compatible with ERC-2771 contracts. See {ERC2771Context}. * * This forwarder operates on forward requests that include: * diff --git a/contracts/mocks/ERC165/ERC165InterfacesSupported.sol b/contracts/mocks/ERC165/ERC165InterfacesSupported.sol index 4010b21030b..dffd6a24ed6 100644 --- a/contracts/mocks/ERC165/ERC165InterfacesSupported.sol +++ b/contracts/mocks/ERC165/ERC165InterfacesSupported.sol @@ -27,7 +27,7 @@ contract SupportsInterfaceWithLookupMock is IERC165 { /** * @dev A contract implementing SupportsInterfaceWithLookup - * implement ERC165 itself. + * implement ERC-165 itself. */ constructor() { _registerInterface(INTERFACE_ID_ERC165); diff --git a/contracts/mocks/docs/ERC4626Fees.sol b/contracts/mocks/docs/ERC4626Fees.sol index 17bc92d7cd1..b4baef5b0b7 100644 --- a/contracts/mocks/docs/ERC4626Fees.sol +++ b/contracts/mocks/docs/ERC4626Fees.sol @@ -7,7 +7,7 @@ import {ERC4626} from "../../token/ERC20/extensions/ERC4626.sol"; import {SafeERC20} from "../../token/ERC20/utils/SafeERC20.sol"; import {Math} from "../../utils/math/Math.sol"; -/// @dev ERC4626 vault with entry/exit fees expressed in https://en.wikipedia.org/wiki/Basis_point[basis point (bp)]. +/// @dev ERC-4626 vault with entry/exit fees expressed in https://en.wikipedia.org/wiki/Basis_point[basis point (bp)]. abstract contract ERC4626Fees is ERC4626 { using Math for uint256; diff --git a/contracts/proxy/Clones.sol b/contracts/proxy/Clones.sol index 95e467d3eb7..92ed339a756 100644 --- a/contracts/proxy/Clones.sol +++ b/contracts/proxy/Clones.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; /** - * @dev https://eips.ethereum.org/EIPS/eip-1167[EIP 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". * * > To simply and cheaply clone contract functionality in an immutable way, this standard specifies diff --git a/contracts/proxy/ERC1967/ERC1967Proxy.sol b/contracts/proxy/ERC1967/ERC1967Proxy.sol index 0fa61b5b3f1..8f6b717a56f 100644 --- a/contracts/proxy/ERC1967/ERC1967Proxy.sol +++ b/contracts/proxy/ERC1967/ERC1967Proxy.sol @@ -9,7 +9,7 @@ import {ERC1967Utils} from "./ERC1967Utils.sol"; /** * @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an * implementation address that can be changed. This address is stored in storage in the location specified by - * https://eips.ethereum.org/EIPS/eip-1967[EIP1967], so that it doesn't conflict with the storage layout of the + * https://eips.ethereum.org/EIPS/eip-1967[ERC-1967], so that it doesn't conflict with the storage layout of the * implementation behind the proxy. */ contract ERC1967Proxy is Proxy { @@ -30,7 +30,7 @@ contract ERC1967Proxy is Proxy { /** * @dev Returns the current implementation address. * - * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using + * TIP: To get this value clients can read directly from the storage slot shown below (specified by ERC-1967) using * the https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` */ diff --git a/contracts/proxy/ERC1967/ERC1967Utils.sol b/contracts/proxy/ERC1967/ERC1967Utils.sol index e55bae20c72..19354814db6 100644 --- a/contracts/proxy/ERC1967/ERC1967Utils.sol +++ b/contracts/proxy/ERC1967/ERC1967Utils.sol @@ -9,7 +9,7 @@ import {StorageSlot} from "../../utils/StorageSlot.sol"; /** * @dev This abstract contract provides getters and event emitting update functions for - * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots. + * https://eips.ethereum.org/EIPS/eip-1967[ERC-1967] slots. */ library ERC1967Utils { // We re-declare ERC-1967 events here because they can't be used directly from IERC1967. @@ -64,7 +64,7 @@ library ERC1967Utils { } /** - * @dev Stores a new address in the EIP1967 implementation slot. + * @dev Stores a new address in the ERC-1967 implementation slot. */ function _setImplementation(address newImplementation) private { if (newImplementation.code.length == 0) { @@ -101,7 +101,7 @@ library ERC1967Utils { /** * @dev Returns the current admin. * - * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using + * TIP: To get this value clients can read directly from the storage slot shown below (specified by ERC-1967) using * the https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` */ @@ -110,7 +110,7 @@ library ERC1967Utils { } /** - * @dev Stores a new address in the EIP1967 admin slot. + * @dev Stores a new address in the ERC-1967 admin slot. */ function _setAdmin(address newAdmin) private { if (newAdmin == address(0)) { @@ -144,7 +144,7 @@ library ERC1967Utils { } /** - * @dev Stores a new beacon in the EIP1967 beacon slot. + * @dev Stores a new beacon in the ERC-1967 beacon slot. */ function _setBeacon(address newBeacon) private { if (newBeacon.code.length == 0) { diff --git a/contracts/proxy/README.adoc b/contracts/proxy/README.adoc index 3c4a78d1905..c6badf0fcba 100644 --- a/contracts/proxy/README.adoc +++ b/contracts/proxy/README.adoc @@ -9,12 +9,12 @@ Most of the proxies below are built on an abstract base contract. - {Proxy}: Abstract contract implementing the core delegation functionality. -In order to avoid clashes with the storage variables of the implementation contract behind a proxy, we use https://eips.ethereum.org/EIPS/eip-1967[EIP1967] storage slots. +In order to avoid clashes with the storage variables of the implementation contract behind a proxy, we use https://eips.ethereum.org/EIPS/eip-1967[ERC-1967] storage slots. -- {ERC1967Utils}: Internal functions to get and set the storage slots defined in EIP1967. -- {ERC1967Proxy}: A proxy using EIP1967 storage slots. Not upgradeable by default. +- {ERC1967Utils}: Internal functions to get and set the storage slots defined in ERC-1967. +- {ERC1967Proxy}: A proxy using ERC-1967 storage slots. Not upgradeable by default. -There are two alternative ways to add upgradeability to an ERC1967 proxy. Their differences are explained below in <>. +There are two alternative ways to add upgradeability to an ERC-1967 proxy. Their differences are explained below in <>. - {TransparentUpgradeableProxy}: A proxy with a built-in immutable admin and upgrade interface. - {UUPSUpgradeable}: An upgradeability mechanism to be included in the implementation contract. @@ -26,7 +26,7 @@ A different family of proxies are beacon proxies. This pattern, popularized by D - {BeaconProxy}: A proxy that retrieves its implementation from a beacon contract. - {UpgradeableBeacon}: A beacon contract with a built in admin that can upgrade the {BeaconProxy} pointing to it. -In this pattern, the proxy contract doesn't hold the implementation address in storage like an ERC1967 proxy. Instead, the address is stored in a separate beacon contract. The `upgrade` operations are sent to the beacon instead of to the proxy contract, and all proxies that follow that beacon are automatically upgraded. +In this pattern, the proxy contract doesn't hold the implementation address in storage like an ERC-1967 proxy. Instead, the address is stored in a separate beacon contract. The `upgrade` operations are sent to the beacon instead of to the proxy contract, and all proxies that follow that beacon are automatically upgraded. Outside the realm of upgradeability, proxies can also be useful to make cheap contract clones, such as those created by an on-chain factory contract that creates many instances of the same contract. These instances are designed to be both cheap to deploy, and cheap to call. @@ -35,7 +35,7 @@ Outside the realm of upgradeability, proxies can also be useful to make cheap co [[transparent-vs-uups]] == Transparent vs UUPS Proxies -The original proxies included in OpenZeppelin followed the https://blog.openzeppelin.com/the-transparent-proxy-pattern/[Transparent Proxy Pattern]. While this pattern is still provided, our recommendation is now shifting towards UUPS proxies, which are both lightweight and versatile. The name UUPS comes from https://eips.ethereum.org/EIPS/eip-1822[EIP1822], which first documented the pattern. +The original proxies included in OpenZeppelin followed the https://blog.openzeppelin.com/the-transparent-proxy-pattern/[Transparent Proxy Pattern]. While this pattern is still provided, our recommendation is now shifting towards UUPS proxies, which are both lightweight and versatile. The name UUPS comes from https://eips.ethereum.org/EIPS/eip-1822[ERC-1822], which first documented the pattern. While both of these share the same interface for upgrades, in UUPS proxies the upgrade is handled by the implementation, and can eventually be removed. Transparent proxies, on the other hand, include the upgrade and admin logic in the proxy itself. This means {TransparentUpgradeableProxy} is more expensive to deploy than what is possible with UUPS proxies. @@ -48,13 +48,13 @@ By default, the upgrade functionality included in {UUPSUpgradeable} contains a s - Adding a flag mechanism in the implementation that will disable the upgrade function when triggered. - Upgrading to an implementation that features an upgrade mechanism without the additional security check, and then upgrading again to another implementation without the upgrade mechanism. -The current implementation of this security mechanism uses https://eips.ethereum.org/EIPS/eip-1822[EIP1822] to detect the storage slot used by the implementation. A previous implementation, now deprecated, relied on a rollback check. It is possible to upgrade from a contract using the old mechanism to a new one. The inverse is however not possible, as old implementations (before version 4.5) did not include the `ERC1822` interface. +The current implementation of this security mechanism uses https://eips.ethereum.org/EIPS/eip-1822[ERC-1822] to detect the storage slot used by the implementation. A previous implementation, now deprecated, relied on a rollback check. It is possible to upgrade from a contract using the old mechanism to a new one. The inverse is however not possible, as old implementations (before version 4.5) did not include the ERC-1822 interface. == Core {{Proxy}} -== ERC1967 +== ERC-1967 {{IERC1967}} diff --git a/contracts/proxy/beacon/BeaconProxy.sol b/contracts/proxy/beacon/BeaconProxy.sol index 05e26e5d544..9b3f627b19a 100644 --- a/contracts/proxy/beacon/BeaconProxy.sol +++ b/contracts/proxy/beacon/BeaconProxy.sol @@ -12,7 +12,7 @@ import {ERC1967Utils} from "../ERC1967/ERC1967Utils.sol"; * * The beacon address can only be set once during construction, and cannot be changed afterwards. It is stored in an * immutable variable to avoid unnecessary storage reads, and also in the beacon storage slot specified by - * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] so that it can be accessed externally. + * https://eips.ethereum.org/EIPS/eip-1967[ERC-1967] so that it can be accessed externally. * * CAUTION: Since the beacon address can never be changed, you must ensure that you either control the beacon, or trust * the beacon to not upgrade the implementation maliciously. diff --git a/contracts/proxy/utils/UUPSUpgradeable.sol b/contracts/proxy/utils/UUPSUpgradeable.sol index 8a4e693ae19..20eb1f72604 100644 --- a/contracts/proxy/utils/UUPSUpgradeable.sol +++ b/contracts/proxy/utils/UUPSUpgradeable.sol @@ -42,9 +42,9 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable { /** * @dev Check that the execution is being performed through a delegatecall call and that the execution context is - * a proxy contract with an implementation (as defined in ERC1967) pointing to self. This should only be the case + * a proxy contract with an implementation (as defined in ERC-1967) pointing to self. This should only be the case * for UUPS and transparent proxies that are using the current contract as their implementation. Execution of a - * function through ERC1167 minimal proxies (clones) would not normally pass this test, but is not guaranteed to + * function through ERC-1167 minimal proxies (clones) would not normally pass this test, but is not guaranteed to * fail. */ modifier onlyProxy() { @@ -62,7 +62,7 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable { } /** - * @dev Implementation of the ERC1822 {proxiableUUID} function. This returns the storage slot used by the + * @dev Implementation of the ERC-1822 {proxiableUUID} function. This returns the storage slot used by the * implementation. It is used to validate the implementation's compatibility when performing an upgrade. * * IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks @@ -90,7 +90,7 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable { /** * @dev Reverts if the execution is not performed via delegatecall or the execution - * context is not of a proxy with an ERC1967-compliant implementation pointing to self. + * context is not of a proxy with an ERC-1967 compliant implementation pointing to self. * See {_onlyProxy}. */ function _checkProxy() internal view virtual { @@ -129,7 +129,7 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable { * @dev Performs an implementation upgrade with a security check for UUPS proxies, and additional setup call. * * As a security check, {proxiableUUID} is invoked in the new implementation, and the return value - * is expected to be the implementation slot in ERC1967. + * is expected to be the implementation slot in ERC-1967. * * Emits an {IERC1967-Upgraded} event. */ diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol index 316f3291e88..2ec3ef7be86 100644 --- a/contracts/token/ERC1155/ERC1155.sol +++ b/contracts/token/ERC1155/ERC1155.sol @@ -49,7 +49,7 @@ abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IER * * This implementation returns the same URI for *all* token types. It relies * on the token type ID substitution mechanism - * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. + * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the ERC]. * * Clients calling this function must replace the `\{id\}` substring with the * actual token type ID. @@ -263,7 +263,7 @@ abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IER /** * @dev Sets a new URI for all token types, by relying on the token type ID * substitution mechanism - * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. + * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the ERC]. * * By this mechanism, any occurrence of the `\{id\}` substring in either the * URI or any of the values in the JSON file at said URI will be replaced by @@ -394,7 +394,7 @@ abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IER } } catch (bytes memory reason) { if (reason.length == 0) { - // non-ERC1155Receiver implementer + // non-IERC1155Receiver implementer revert ERC1155InvalidReceiver(to); } else { /// @solidity memory-safe-assembly @@ -428,7 +428,7 @@ abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IER } } catch (bytes memory reason) { if (reason.length == 0) { - // non-ERC1155Receiver implementer + // non-IERC1155Receiver implementer revert ERC1155InvalidReceiver(to); } else { /// @solidity memory-safe-assembly diff --git a/contracts/token/ERC1155/IERC1155.sol b/contracts/token/ERC1155/IERC1155.sol index 461e48b986e..62ad4a9bd2c 100644 --- a/contracts/token/ERC1155/IERC1155.sol +++ b/contracts/token/ERC1155/IERC1155.sol @@ -6,8 +6,8 @@ pragma solidity ^0.8.20; import {IERC165} from "../../utils/introspection/IERC165.sol"; /** - * @dev Required interface of an ERC1155 compliant contract, as defined in the - * https://eips.ethereum.org/EIPS/eip-1155[EIP]. + * @dev Required interface of an ERC-1155 compliant contract, as defined in the + * https://eips.ethereum.org/EIPS/eip-1155[ERC]. */ interface IERC1155 is IERC165 { /** diff --git a/contracts/token/ERC1155/IERC1155Receiver.sol b/contracts/token/ERC1155/IERC1155Receiver.sol index 0f6e2bf8580..36ad4c75206 100644 --- a/contracts/token/ERC1155/IERC1155Receiver.sol +++ b/contracts/token/ERC1155/IERC1155Receiver.sol @@ -11,7 +11,7 @@ import {IERC165} from "../../utils/introspection/IERC165.sol"; */ interface IERC1155Receiver is IERC165 { /** - * @dev Handles the receipt of a single ERC1155 token type. This function is + * @dev Handles the receipt of a single ERC-1155 token type. This function is * called at the end of a `safeTransferFrom` after the balance has been updated. * * NOTE: To accept the transfer, this must return @@ -34,7 +34,7 @@ interface IERC1155Receiver is IERC165 { ) external returns (bytes4); /** - * @dev Handles the receipt of a multiple ERC1155 token types. This function + * @dev Handles the receipt of a multiple ERC-1155 token types. This function * is called at the end of a `safeBatchTransferFrom` after the balances have * been updated. * diff --git a/contracts/token/ERC1155/README.adoc b/contracts/token/ERC1155/README.adoc index 1a56358ef42..d911d785d85 100644 --- a/contracts/token/ERC1155/README.adoc +++ b/contracts/token/ERC1155/README.adoc @@ -1,11 +1,11 @@ -= ERC 1155 += ERC-1155 [.readme-notice] NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/token/erc1155 -This set of interfaces and contracts are all related to the https://eips.ethereum.org/EIPS/eip-1155[ERC1155 Multi Token Standard]. +This set of interfaces and contracts are all related to the https://eips.ethereum.org/EIPS/eip-1155[ERC-1155 Multi Token Standard]. -The EIP consists of three interfaces which fulfill different roles, found here as {IERC1155}, {IERC1155MetadataURI} and {IERC1155Receiver}. +The ERC consists of three interfaces which fulfill different roles, found here as {IERC1155}, {IERC1155MetadataURI} and {IERC1155Receiver}. {ERC1155} implements the mandatory {IERC1155} interface, as well as the optional extension {IERC1155MetadataURI}, by relying on the substitution mechanism to use the same URI for all token types, dramatically reducing gas costs. @@ -14,7 +14,7 @@ Additionally there are multiple custom extensions, including: * designation of addresses that can pause token transfers for all users ({ERC1155Pausable}). * destruction of own tokens ({ERC1155Burnable}). -NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC1155 (such as <>) and expose them as external functions in the way they prefer. +NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC-1155 (such as <>) and expose them as external functions in the way they prefer. == Core diff --git a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol index 529a465238b..e99cf2aa95d 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol @@ -7,7 +7,7 @@ import {ERC1155} from "../ERC1155.sol"; import {Pausable} from "../../../utils/Pausable.sol"; /** - * @dev ERC1155 token with pausable token transfers, minting and burning. + * @dev ERC-1155 token with pausable token transfers, minting and burning. * * Useful for scenarios such as preventing trades until the end of an evaluation * period, or having an emergency switch for freezing all token transfers in the diff --git a/contracts/token/ERC1155/extensions/ERC1155Supply.sol b/contracts/token/ERC1155/extensions/ERC1155Supply.sol index cef11b4c2f2..4bad08bc072 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Supply.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Supply.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.20; import {ERC1155} from "../ERC1155.sol"; /** - * @dev Extension of ERC1155 that adds tracking of total supply per id. + * @dev Extension of ERC-1155 that adds tracking of total supply per id. * * Useful for scenarios where Fungible and Non-fungible tokens have to be * clearly identified. Note: While a totalSupply of 1 might mean the diff --git a/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol b/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol index c2a5bdcedf1..e8436e830d7 100644 --- a/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol +++ b/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol @@ -7,8 +7,8 @@ import {Strings} from "../../../utils/Strings.sol"; import {ERC1155} from "../ERC1155.sol"; /** - * @dev ERC1155 token with storage based token URI management. - * Inspired by the ERC721URIStorage extension + * @dev ERC-1155 token with storage based token URI management. + * Inspired by the {ERC721URIStorage} extension */ abstract contract ERC1155URIStorage is ERC1155 { using Strings for uint256; diff --git a/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol b/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol index e3fb74df0e7..ea07897b9f2 100644 --- a/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol +++ b/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol @@ -7,7 +7,7 @@ import {IERC1155} from "../IERC1155.sol"; /** * @dev Interface of the optional ERC1155MetadataExtension interface, as defined - * in the https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[EIP]. + * in the https://eips.ethereum.org/EIPS/eip-1155#metadata-extensions[ERC]. */ interface IERC1155MetadataURI is IERC1155 { /** diff --git a/contracts/token/ERC1155/utils/ERC1155Holder.sol b/contracts/token/ERC1155/utils/ERC1155Holder.sol index b108cdbf698..35be58c5238 100644 --- a/contracts/token/ERC1155/utils/ERC1155Holder.sol +++ b/contracts/token/ERC1155/utils/ERC1155Holder.sol @@ -7,7 +7,7 @@ import {IERC165, ERC165} from "../../../utils/introspection/ERC165.sol"; import {IERC1155Receiver} from "../IERC1155Receiver.sol"; /** - * @dev Simple implementation of `IERC1155Receiver` that will allow a contract to hold ERC1155 tokens. + * @dev Simple implementation of `IERC1155Receiver` that will allow a contract to hold ERC-1155 tokens. * * IMPORTANT: When inheriting this contract, you must include a way to use the received tokens, otherwise they will be * stuck. diff --git a/contracts/token/ERC20/ERC20.sol b/contracts/token/ERC20/ERC20.sol index 1fde5279d00..cf7332858c9 100644 --- a/contracts/token/ERC20/ERC20.sol +++ b/contracts/token/ERC20/ERC20.sol @@ -23,12 +23,12 @@ import {IERC20Errors} from "../../interfaces/draft-IERC6093.sol"; * * We have followed general OpenZeppelin Contracts guidelines: functions revert * instead returning `false` on failure. This behavior is nonetheless - * conventional and does not conflict with the expectations of ERC20 + * conventional and does not conflict with the expectations of ERC-20 * applications. * * Additionally, an {Approval} event is emitted on calls to {transferFrom}. * This allows applications to reconstruct the allowance for all accounts just - * by listening to said events. Other implementations of the EIP may not emit + * by listening to said events. Other implementations of the ERC may not emit * these events, as it isn't required by the specification. */ abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { @@ -139,7 +139,7 @@ abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { * @dev See {IERC20-transferFrom}. * * Emits an {Approval} event indicating the updated allowance. This is not - * required by the EIP. See the note at the beginning of {ERC20}. + * required by the ERC. See the note at the beginning of {ERC20}. * * NOTE: Does not update the allowance if the current allowance * is the maximum `uint256`. diff --git a/contracts/token/ERC20/IERC20.sol b/contracts/token/ERC20/IERC20.sol index db01cf4c751..d8907216425 100644 --- a/contracts/token/ERC20/IERC20.sol +++ b/contracts/token/ERC20/IERC20.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; /** - * @dev Interface of the ERC20 standard as defined in the EIP. + * @dev Interface of the ERC-20 standard as defined in the ERC. */ interface IERC20 { /** diff --git a/contracts/token/ERC20/README.adoc b/contracts/token/ERC20/README.adoc index 2c508802dad..6113b08e6ec 100644 --- a/contracts/token/ERC20/README.adoc +++ b/contracts/token/ERC20/README.adoc @@ -1,38 +1,38 @@ -= ERC 20 += ERC-20 [.readme-notice] NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/token/erc20 -This set of interfaces, contracts, and utilities are all related to the https://eips.ethereum.org/EIPS/eip-20[ERC20 Token Standard]. +This set of interfaces, contracts, and utilities are all related to the https://eips.ethereum.org/EIPS/eip-20[ERC-20 Token Standard]. -TIP: For an overview of ERC20 tokens and a walk through on how to create a token contract read our xref:ROOT:erc20.adoc[ERC20 guide]. +TIP: For an overview of ERC-20 tokens and a walk through on how to create a token contract read our xref:ROOT:erc20.adoc[ERC-20 guide]. -There are a few core contracts that implement the behavior specified in the EIP: +There are a few core contracts that implement the behavior specified in the ERC: -* {IERC20}: the interface all ERC20 implementations should conform to. -* {IERC20Metadata}: the extended ERC20 interface including the <>, <> and <> functions. -* {ERC20}: the implementation of the ERC20 interface, including the <>, <> and <> optional standard extension to the base interface. +* {IERC20}: the interface all ERC-20 implementations should conform to. +* {IERC20Metadata}: the extended ERC-20 interface including the <>, <> and <> functions. +* {ERC20}: the implementation of the ERC-20 interface, including the <>, <> and <> optional standard extension to the base interface. Additionally there are multiple custom extensions, including: -* {ERC20Permit}: gasless approval of tokens (standardized as ERC2612). +* {ERC20Permit}: gasless approval of tokens (standardized as ERC-2612). * {ERC20Burnable}: destruction of own tokens. * {ERC20Capped}: enforcement of a cap to the total supply when minting tokens. * {ERC20Pausable}: ability to pause token transfers. -* {ERC20FlashMint}: token level support for flash loans through the minting and burning of ephemeral tokens (standardized as ERC3156). +* {ERC20FlashMint}: token level support for flash loans through the minting and burning of ephemeral tokens (standardized as ERC-3156). * {ERC20Votes}: support for voting and vote delegation. -* {ERC20Wrapper}: wrapper to create an ERC20 backed by another ERC20, with deposit and withdraw methods. Useful in conjunction with {ERC20Votes}. -* {ERC4626}: tokenized vault that manages shares (represented as ERC20) that are backed by assets (another ERC20). +* {ERC20Wrapper}: wrapper to create an ERC-20 backed by another ERC-20, with deposit and withdraw methods. Useful in conjunction with {ERC20Votes}. +* {ERC4626}: tokenized vault that manages shares (represented as ERC-20) that are backed by assets (another ERC-20). -Finally, there are some utilities to interact with ERC20 contracts in various ways: +Finally, there are some utilities to interact with ERC-20 contracts in various ways: * {SafeERC20}: a wrapper around the interface that eliminates the need to handle boolean return values. -Other utilities that support ERC20 assets can be found in codebase: +Other utilities that support ERC-20 assets can be found in codebase: -* ERC20 tokens can be timelocked (held tokens for a beneficiary until a specified time) or vested (released following a given schedule) using a {VestingWallet}. +* ERC-20 tokens can be timelocked (held tokens for a beneficiary until a specified time) or vested (released following a given schedule) using a {VestingWallet}. -NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC20 (such as <>) and expose them as external functions in the way they prefer. +NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC-20 (such as <>) and expose them as external functions in the way they prefer. == Core diff --git a/contracts/token/ERC20/extensions/ERC20FlashMint.sol b/contracts/token/ERC20/extensions/ERC20FlashMint.sol index 0e893127848..5fa33ef24c2 100644 --- a/contracts/token/ERC20/extensions/ERC20FlashMint.sol +++ b/contracts/token/ERC20/extensions/ERC20FlashMint.sol @@ -8,7 +8,7 @@ import {IERC3156FlashLender} from "../../../interfaces/IERC3156FlashLender.sol"; import {ERC20} from "../ERC20.sol"; /** - * @dev Implementation of the ERC3156 Flash loans extension, as defined in + * @dev Implementation of the ERC-3156 Flash loans extension, as defined in * https://eips.ethereum.org/EIPS/eip-3156[ERC-3156]. * * Adds the {flashLoan} method, which provides flash loan support at the token diff --git a/contracts/token/ERC20/extensions/ERC20Pausable.sol b/contracts/token/ERC20/extensions/ERC20Pausable.sol index 8fe832b99ad..9b56f053566 100644 --- a/contracts/token/ERC20/extensions/ERC20Pausable.sol +++ b/contracts/token/ERC20/extensions/ERC20Pausable.sol @@ -7,7 +7,7 @@ import {ERC20} from "../ERC20.sol"; import {Pausable} from "../../../utils/Pausable.sol"; /** - * @dev ERC20 token with pausable token transfers, minting and burning. + * @dev ERC-20 token with pausable token transfers, minting and burning. * * Useful for scenarios such as preventing trades until the end of an evaluation * period, or having an emergency switch for freezing all token transfers in the diff --git a/contracts/token/ERC20/extensions/ERC20Permit.sol b/contracts/token/ERC20/extensions/ERC20Permit.sol index 36667adf1e8..22b19dc0bb7 100644 --- a/contracts/token/ERC20/extensions/ERC20Permit.sol +++ b/contracts/token/ERC20/extensions/ERC20Permit.sol @@ -10,10 +10,10 @@ import {EIP712} from "../../../utils/cryptography/EIP712.sol"; import {Nonces} from "../../../utils/Nonces.sol"; /** - * @dev Implementation of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * @dev Implementation of the ERC-20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[ERC-2612]. * - * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by + * Adds the {permit} method, which can be used to change an account's ERC-20 allowance (see {IERC20-allowance}) by * presenting a message signed by the account. By not relying on `{IERC20-approve}`, the token holder account doesn't * need to send a transaction, and thus is not required to hold Ether at all. */ @@ -34,7 +34,7 @@ abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { /** * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * - * It's a good idea to use the same `name` that is defined as the ERC20 token name. + * It's a good idea to use the same `name` that is defined as the ERC-20 token name. */ constructor(string memory name) EIP712(name, "1") {} diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index 6aa6ed05e5c..8533ca9b6ef 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -8,7 +8,7 @@ import {Votes} from "../../../governance/utils/Votes.sol"; import {Checkpoints} from "../../../utils/structs/Checkpoints.sol"; /** - * @dev Extension of ERC20 to support Compound-like voting and delegation. This version is more generic than Compound's, + * @dev Extension of ERC-20 to support Compound-like voting and delegation. This version is more generic than Compound's, * and supports token supply up to 2^208^ - 1, while COMP is limited to 2^96^ - 1. * * NOTE: This contract does not provide interface compatibility with Compound's COMP token. diff --git a/contracts/token/ERC20/extensions/ERC20Wrapper.sol b/contracts/token/ERC20/extensions/ERC20Wrapper.sol index 61448803bc8..6645ddc05f8 100644 --- a/contracts/token/ERC20/extensions/ERC20Wrapper.sol +++ b/contracts/token/ERC20/extensions/ERC20Wrapper.sol @@ -7,11 +7,11 @@ import {IERC20, IERC20Metadata, ERC20} from "../ERC20.sol"; import {SafeERC20} from "../utils/SafeERC20.sol"; /** - * @dev Extension of the ERC20 token contract to support token wrapping. + * @dev Extension of the ERC-20 token contract to support token wrapping. * * Users can deposit and withdraw "underlying tokens" and receive a matching number of "wrapped tokens". This is useful * in conjunction with other modules. For example, combining this wrapping mechanism with {ERC20Votes} will allow the - * wrapping of an existing "basic" ERC20 into a governance token. + * wrapping of an existing "basic" ERC-20 into a governance token. */ abstract contract ERC20Wrapper is ERC20 { IERC20 private immutable _underlying; diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index dccf6900ade..76c14fc7584 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -9,12 +9,12 @@ import {IERC4626} from "../../../interfaces/IERC4626.sol"; import {Math} from "../../../utils/math/Math.sol"; /** - * @dev Implementation of the ERC4626 "Tokenized Vault Standard" as defined in - * https://eips.ethereum.org/EIPS/eip-4626[EIP-4626]. + * @dev Implementation of the ERC-4626 "Tokenized Vault Standard" as defined in + * https://eips.ethereum.org/EIPS/eip-4626[ERC-4626]. * - * This extension allows the minting and burning of "shares" (represented using the ERC20 inheritance) in exchange for + * This extension allows the minting and burning of "shares" (represented using the ERC-20 inheritance) in exchange for * underlying "assets" through standardized {deposit}, {mint}, {redeem} and {burn} workflows. This contract extends - * the ERC20 standard. Any additional extensions included along it would affect the "shares" token represented by this + * the ERC-20 standard. Any additional extensions included along it would affect the "shares" token represented by this * contract and not the "assets" token which is an independent contract. * * [CAUTION] @@ -72,7 +72,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { error ERC4626ExceededMaxRedeem(address owner, uint256 shares, uint256 max); /** - * @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC20 or ERC777). + * @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC-20 or ERC-777). */ constructor(IERC20 asset_) { (bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_); @@ -241,7 +241,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { * @dev Deposit/mint common workflow. */ function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual { - // If _asset is ERC777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the + // If _asset is ERC-777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the // `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer, // calls the vault, which is assumed not malicious. // @@ -268,7 +268,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { _spendAllowance(owner, caller, shares); } - // If _asset is ERC777, `transfer` can trigger a reentrancy AFTER the transfer happens through the + // If _asset is ERC-777, `transfer` can trigger a reentrancy AFTER the transfer happens through the // `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer, // calls the vault, which is assumed not malicious. // diff --git a/contracts/token/ERC20/extensions/IERC20Metadata.sol b/contracts/token/ERC20/extensions/IERC20Metadata.sol index 1a38cba3e06..e8c020b1fcd 100644 --- a/contracts/token/ERC20/extensions/IERC20Metadata.sol +++ b/contracts/token/ERC20/extensions/IERC20Metadata.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.20; import {IERC20} from "../IERC20.sol"; /** - * @dev Interface for the optional metadata functions from the ERC20 standard. + * @dev Interface for the optional metadata functions from the ERC-20 standard. */ interface IERC20Metadata is IERC20 { /** diff --git a/contracts/token/ERC20/extensions/IERC20Permit.sol b/contracts/token/ERC20/extensions/IERC20Permit.sol index 5af48101ab8..a8ad26ede5a 100644 --- a/contracts/token/ERC20/extensions/IERC20Permit.sol +++ b/contracts/token/ERC20/extensions/IERC20Permit.sol @@ -4,10 +4,10 @@ pragma solidity ^0.8.20; /** - * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in - * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * @dev Interface of the ERC-20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[ERC-2612]. * - * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by + * Adds the {permit} method, which can be used to change an account's ERC-20 allowance (see {IERC20-allowance}) by * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't * need to send a transaction, and thus is not required to hold Ether at all. * diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index bb65709b46b..67fabf4ccb9 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -9,7 +9,7 @@ import {Address} from "../../../utils/Address.sol"; /** * @title SafeERC20 - * @dev Wrappers around ERC20 operations that throw on failure (when the token + * @dev Wrappers around ERC-20 operations that throw on failure (when the token * contract returns false). Tokens that return no value (and instead revert or * throw on failure) are also supported, non-reverting calls are assumed to be * successful. @@ -20,7 +20,7 @@ library SafeERC20 { using Address for address; /** - * @dev An operation with an ERC20 token failed. + * @dev An operation with an ERC-20 token failed. */ error SafeERC20FailedOperation(address token); diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 98a80e52c4c..1b38f06814e 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -12,7 +12,7 @@ import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol"; import {IERC721Errors} from "../../interfaces/draft-IERC6093.sol"; /** - * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including + * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC-721] Non-Fungible Token Standard, including * the Metadata extension, but not including the Enumerable extension, which is available separately as * {ERC721Enumerable}. */ @@ -165,7 +165,7 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er * @dev Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist * * IMPORTANT: Any overrides to this function that add ownership of tokens not tracked by the - * core ERC721 logic MUST be matched with the use of {_increaseBalance} to keep balances + * core ERC-721 logic MUST be matched with the use of {_increaseBalance} to keep balances * consistent with ownership. The invariant to preserve is that for any address `a` the value returned by * `balanceOf(a)` must be equal to the number of tokens such that `_ownerOf(tokenId)` is `a`. */ @@ -357,7 +357,7 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er /** * @dev Safely transfers `tokenId` token from `from` to `to`, checking that contract recipients - * are aware of the ERC721 standard to prevent tokens from being forever locked. + * are aware of the ERC-721 standard to prevent tokens from being forever locked. * * `data` is additional data, it has no specified format and it is sent in call to `to`. * diff --git a/contracts/token/ERC721/IERC721.sol b/contracts/token/ERC721/IERC721.sol index 12f3236342b..d6ab6a47d2a 100644 --- a/contracts/token/ERC721/IERC721.sol +++ b/contracts/token/ERC721/IERC721.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.20; import {IERC165} from "../../utils/introspection/IERC165.sol"; /** - * @dev Required interface of an ERC721 compliant contract. + * @dev Required interface of an ERC-721 compliant contract. */ interface IERC721 is IERC165 { /** @@ -56,7 +56,7 @@ interface IERC721 is IERC165 { /** * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients - * are aware of the ERC721 protocol to prevent tokens from being forever locked. + * are aware of the ERC-721 protocol to prevent tokens from being forever locked. * * Requirements: * @@ -75,7 +75,7 @@ interface IERC721 is IERC165 { /** * @dev Transfers `tokenId` token from `from` to `to`. * - * WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721 + * WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC-721 * or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must * understand this adds an external call which potentially creates a reentrancy vulnerability. * diff --git a/contracts/token/ERC721/IERC721Receiver.sol b/contracts/token/ERC721/IERC721Receiver.sol index f9dc1332bef..a53d0777479 100644 --- a/contracts/token/ERC721/IERC721Receiver.sol +++ b/contracts/token/ERC721/IERC721Receiver.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.20; /** - * @title ERC721 token receiver interface + * @title ERC-721 token receiver interface * @dev Interface for any contract that wants to support safeTransfers - * from ERC721 asset contracts. + * from ERC-721 asset contracts. */ interface IERC721Receiver { /** diff --git a/contracts/token/ERC721/README.adoc b/contracts/token/ERC721/README.adoc index 40ae919d9d0..0f87916f60a 100644 --- a/contracts/token/ERC721/README.adoc +++ b/contracts/token/ERC721/README.adoc @@ -1,13 +1,13 @@ -= ERC 721 += ERC-721 [.readme-notice] NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/token/erc721 -This set of interfaces, contracts, and utilities are all related to the https://eips.ethereum.org/EIPS/eip-721[ERC721 Non-Fungible Token Standard]. +This set of interfaces, contracts, and utilities are all related to the https://eips.ethereum.org/EIPS/eip-721[ERC-721 Non-Fungible Token Standard]. -TIP: For a walk through on how to create an ERC721 token read our xref:ROOT:erc721.adoc[ERC721 guide]. +TIP: For a walk through on how to create an ERC-721 token read our xref:ROOT:erc721.adoc[ERC-721 guide]. -The EIP specifies four interfaces: +The ERC specifies four interfaces: * {IERC721}: Core functionality required in all compliant implementation. * {IERC721Metadata}: Optional extension that adds name, symbol, and token URI, almost always included. @@ -22,15 +22,15 @@ OpenZeppelin Contracts provides implementations of all four interfaces: Additionally there are a few of other extensions: -* {ERC721Consecutive}: An implementation of https://eips.ethereum.org/EIPS/eip-2309[ERC2309] for minting batchs of tokens during construction, in accordance with ERC721. +* {ERC721Consecutive}: An implementation of https://eips.ethereum.org/EIPS/eip-2309[ERC-2309] for minting batchs of tokens during construction, in accordance with ERC-721. * {ERC721URIStorage}: A more flexible but more expensive way of storing metadata. * {ERC721Votes}: Support for voting and vote delegation. -* {ERC721Royalty}: A way to signal royalty information following ERC2981. +* {ERC721Royalty}: A way to signal royalty information following ERC-2981. * {ERC721Pausable}: A primitive to pause contract operation. * {ERC721Burnable}: A way for token holders to burn their own tokens. -* {ERC721Wrapper}: Wrapper to create an ERC721 backed by another ERC721, with deposit and withdraw methods. Useful in conjunction with {ERC721Votes}. +* {ERC721Wrapper}: Wrapper to create an ERC-721 backed by another ERC-721, with deposit and withdraw methods. Useful in conjunction with {ERC721Votes}. -NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC721 (such as <>) and expose them as external functions in the way they prefer. +NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC-721 (such as <>) and expose them as external functions in the way they prefer. == Core diff --git a/contracts/token/ERC721/extensions/ERC721Burnable.sol b/contracts/token/ERC721/extensions/ERC721Burnable.sol index 2a150afb884..65cfc744cba 100644 --- a/contracts/token/ERC721/extensions/ERC721Burnable.sol +++ b/contracts/token/ERC721/extensions/ERC721Burnable.sol @@ -7,8 +7,8 @@ import {ERC721} from "../ERC721.sol"; import {Context} from "../../../utils/Context.sol"; /** - * @title ERC721 Burnable Token - * @dev ERC721 Token that can be burned (destroyed). + * @title ERC-721 Burnable Token + * @dev ERC-721 Token that can be burned (destroyed). */ abstract contract ERC721Burnable is Context, ERC721 { /** diff --git a/contracts/token/ERC721/extensions/ERC721Consecutive.sol b/contracts/token/ERC721/extensions/ERC721Consecutive.sol index 0d6cbc7e4ca..8508a79f4fc 100644 --- a/contracts/token/ERC721/extensions/ERC721Consecutive.sol +++ b/contracts/token/ERC721/extensions/ERC721Consecutive.sol @@ -9,8 +9,8 @@ import {BitMaps} from "../../../utils/structs/BitMaps.sol"; import {Checkpoints} from "../../../utils/structs/Checkpoints.sol"; /** - * @dev Implementation of the ERC2309 "Consecutive Transfer Extension" as defined in - * https://eips.ethereum.org/EIPS/eip-2309[EIP-2309]. + * @dev Implementation of the ERC-2309 "Consecutive Transfer Extension" as defined in + * https://eips.ethereum.org/EIPS/eip-2309[ERC-2309]. * * This extension allows the minting of large batches of tokens, during contract construction only. For upgradeable * contracts this implies that batch minting is only available during proxy deployment, and not in subsequent upgrades. @@ -37,7 +37,7 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { /** * @dev Batch mint is restricted to the constructor. * Any batch mint not emitting the {IERC721-Transfer} event outside of the constructor - * is non-ERC721 compliant. + * is non ERC-721 compliant. */ error ERC721ForbiddenBatchMint(); @@ -94,7 +94,7 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { * - `batchSize` must not be greater than {_maxBatchSize}. * - The function is called in the constructor of the contract (directly or indirectly). * - * CAUTION: Does not emit a `Transfer` event. This is ERC721 compliant as long as it is done inside of the + * CAUTION: Does not emit a `Transfer` event. This is ERC-721 compliant as long as it is done inside of the * constructor, which is enforced by this function. * * CAUTION: Does not invoke `onERC721Received` on the receiver. diff --git a/contracts/token/ERC721/extensions/ERC721Enumerable.sol b/contracts/token/ERC721/extensions/ERC721Enumerable.sol index cbf3e03f702..012e0ffc337 100644 --- a/contracts/token/ERC721/extensions/ERC721Enumerable.sol +++ b/contracts/token/ERC721/extensions/ERC721Enumerable.sol @@ -8,11 +8,11 @@ import {IERC721Enumerable} from "./IERC721Enumerable.sol"; import {IERC165} from "../../../utils/introspection/ERC165.sol"; /** - * @dev This implements an optional extension of {ERC721} defined in the EIP that adds enumerability + * @dev This implements an optional extension of {ERC721} defined in the ERC that adds enumerability * of all the token ids in the contract as well as all token ids owned by each account. * - * CAUTION: `ERC721` extensions that implement custom `balanceOf` logic, such as `ERC721Consecutive`, - * interfere with enumerability and should not be used together with `ERC721Enumerable`. + * CAUTION: {ERC721} extensions that implement custom `balanceOf` logic, such as {ERC721Consecutive}, + * interfere with enumerability and should not be used together with {ERC721Enumerable}. */ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { mapping(address owner => mapping(uint256 index => uint256)) private _ownedTokens; diff --git a/contracts/token/ERC721/extensions/ERC721Pausable.sol b/contracts/token/ERC721/extensions/ERC721Pausable.sol index 0b34fd9c190..81619c7f5dc 100644 --- a/contracts/token/ERC721/extensions/ERC721Pausable.sol +++ b/contracts/token/ERC721/extensions/ERC721Pausable.sol @@ -7,7 +7,7 @@ import {ERC721} from "../ERC721.sol"; import {Pausable} from "../../../utils/Pausable.sol"; /** - * @dev ERC721 token with pausable token transfers, minting and burning. + * @dev ERC-721 token with pausable token transfers, minting and burning. * * Useful for scenarios such as preventing trades until the end of an evaluation * period, or having an emergency switch for freezing all token transfers in the diff --git a/contracts/token/ERC721/extensions/ERC721Royalty.sol b/contracts/token/ERC721/extensions/ERC721Royalty.sol index be98ec7c5e4..1e0b25af43b 100644 --- a/contracts/token/ERC721/extensions/ERC721Royalty.sol +++ b/contracts/token/ERC721/extensions/ERC721Royalty.sol @@ -7,14 +7,14 @@ import {ERC721} from "../ERC721.sol"; import {ERC2981} from "../../common/ERC2981.sol"; /** - * @dev Extension of ERC721 with the ERC2981 NFT Royalty Standard, a standardized way to retrieve royalty payment + * @dev Extension of ERC-721 with the ERC-2981 NFT Royalty Standard, a standardized way to retrieve royalty payment * information. * * Royalty information can be specified globally for all token ids via {ERC2981-_setDefaultRoyalty}, and/or individually * for specific token ids via {ERC2981-_setTokenRoyalty}. The latter takes precedence over the first. * * IMPORTANT: ERC-2981 only specifies a way to signal royalty information and does not enforce its payment. See - * https://eips.ethereum.org/EIPS/eip-2981#optional-royalty-payments[Rationale] in the EIP. Marketplaces are expected to + * https://eips.ethereum.org/EIPS/eip-2981#optional-royalty-payments[Rationale] in the ERC. Marketplaces are expected to * voluntarily pay royalties together with sales, but note that this standard is not yet widely supported. */ abstract contract ERC721Royalty is ERC2981, ERC721 { diff --git a/contracts/token/ERC721/extensions/ERC721URIStorage.sol b/contracts/token/ERC721/extensions/ERC721URIStorage.sol index 2584cb58b67..562f815c0b8 100644 --- a/contracts/token/ERC721/extensions/ERC721URIStorage.sol +++ b/contracts/token/ERC721/extensions/ERC721URIStorage.sol @@ -9,7 +9,7 @@ import {IERC4906} from "../../../interfaces/IERC4906.sol"; import {IERC165} from "../../../interfaces/IERC165.sol"; /** - * @dev ERC721 token with storage based token URI management. + * @dev ERC-721 token with storage based token URI management. */ abstract contract ERC721URIStorage is IERC4906, ERC721 { using Strings for uint256; diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 56287151453..4962cb00c19 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -7,7 +7,7 @@ import {ERC721} from "../ERC721.sol"; import {Votes} from "../../../governance/utils/Votes.sol"; /** - * @dev Extension of ERC721 to support voting and delegation as implemented by {Votes}, where each individual NFT counts + * @dev Extension of ERC-721 to support voting and delegation as implemented by {Votes}, where each individual NFT counts * as 1 vote unit. * * Tokens do not count as votes until they are delegated, because votes must be tracked which incurs an additional cost diff --git a/contracts/token/ERC721/extensions/ERC721Wrapper.sol b/contracts/token/ERC721/extensions/ERC721Wrapper.sol index e091bdd9f7f..0a8acacb87b 100644 --- a/contracts/token/ERC721/extensions/ERC721Wrapper.sol +++ b/contracts/token/ERC721/extensions/ERC721Wrapper.sol @@ -7,17 +7,17 @@ import {IERC721, ERC721} from "../ERC721.sol"; import {IERC721Receiver} from "../IERC721Receiver.sol"; /** - * @dev Extension of the ERC721 token contract to support token wrapping. + * @dev Extension of the ERC-721 token contract to support token wrapping. * * Users can deposit and withdraw an "underlying token" and receive a "wrapped token" with a matching tokenId. This is * useful in conjunction with other modules. For example, combining this wrapping mechanism with {ERC721Votes} will allow - * the wrapping of an existing "basic" ERC721 into a governance token. + * the wrapping of an existing "basic" ERC-721 into a governance token. */ abstract contract ERC721Wrapper is ERC721, IERC721Receiver { IERC721 private immutable _underlying; /** - * @dev The received ERC721 token couldn't be wrapped. + * @dev The received ERC-721 token couldn't be wrapped. */ error ERC721UnsupportedToken(address token); @@ -63,7 +63,7 @@ abstract contract ERC721Wrapper is ERC721, IERC721Receiver { } /** - * @dev Overrides {IERC721Receiver-onERC721Received} to allow minting on direct ERC721 transfers to + * @dev Overrides {IERC721Receiver-onERC721Received} to allow minting on direct ERC-721 transfers to * this contract. * * In case there's data attached, it validates that the operator is this contract, so only trusted data diff --git a/contracts/token/common/ERC2981.sol b/contracts/token/common/ERC2981.sol index fce02514d18..b61c09d28ca 100644 --- a/contracts/token/common/ERC2981.sol +++ b/contracts/token/common/ERC2981.sol @@ -16,7 +16,7 @@ import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol"; * fee is specified in basis points by default. * * IMPORTANT: ERC-2981 only specifies a way to signal royalty information and does not enforce its payment. See - * https://eips.ethereum.org/EIPS/eip-2981#optional-royalty-payments[Rationale] in the EIP. Marketplaces are expected to + * https://eips.ethereum.org/EIPS/eip-2981#optional-royalty-payments[Rationale] in the ERC. Marketplaces are expected to * voluntarily pay royalties together with sales, but note that this standard is not yet widely supported. */ abstract contract ERC2981 is IERC2981, ERC165 { diff --git a/contracts/token/common/README.adoc b/contracts/token/common/README.adoc index af61674645e..a70d90ddd24 100644 --- a/contracts/token/common/README.adoc +++ b/contracts/token/common/README.adoc @@ -2,8 +2,8 @@ Functionality that is common to multiple token standards. -* {ERC2981}: NFT Royalties compatible with both ERC721 and ERC1155. -** For ERC721 consider {ERC721Royalty} which clears the royalty information from storage on burn. +* {ERC2981}: NFT Royalties compatible with both ERC-721 and ERC-1155. +** For ERC-721 consider {ERC721Royalty} which clears the royalty information from storage on burn. == Contracts diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index d88b0019950..d0e70463aee 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -49,7 +49,7 @@ Because Solidity does not support generic types, {EnumerableMap} and {Enumerable This set of interfaces and contracts deal with https://en.wikipedia.org/wiki/Type_introspection[type introspection] of contracts, that is, examining which functions can be called on them. This is usually referred to as a contract's _interface_. -Ethereum contracts have no native concept of an interface, so applications must usually simply trust they are not making an incorrect call. For trusted setups this is a non-issue, but often unknown and untrusted third-party addresses need to be interacted with. There may even not be any direct calls to them! (e.g. `ERC20` tokens may be sent to a contract that lacks a way to transfer them out of it, locking them forever). In these cases, a contract _declaring_ its interface can be very helpful in preventing errors. +Ethereum contracts have no native concept of an interface, so applications must usually simply trust they are not making an incorrect call. For trusted setups this is a non-issue, but often unknown and untrusted third-party addresses need to be interacted with. There may even not be any direct calls to them! (e.g. ERC-20 tokens may be sent to a contract that lacks a way to transfer them out of it, locking them forever). In these cases, a contract _declaring_ its interface can be very helpful in preventing errors. {{IERC165}} diff --git a/contracts/utils/StorageSlot.sol b/contracts/utils/StorageSlot.sol index 08418327a59..4e02bfe746d 100644 --- a/contracts/utils/StorageSlot.sol +++ b/contracts/utils/StorageSlot.sol @@ -12,7 +12,7 @@ pragma solidity ^0.8.20; * * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. * - * Example usage to set ERC1967 implementation slot: + * Example usage to set ERC-1967 implementation slot: * ```solidity * contract ERC1967 { * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; diff --git a/contracts/utils/cryptography/ECDSA.sol b/contracts/utils/cryptography/ECDSA.sol index 04b3e5e0646..864c8ee8766 100644 --- a/contracts/utils/cryptography/ECDSA.sol +++ b/contracts/utils/cryptography/ECDSA.sol @@ -95,7 +95,7 @@ library ECDSA { /** * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately. * - * See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures] + * See https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signatures] */ function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError, bytes32) { unchecked { diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol index 8e548cdd8f0..77c4c8990fe 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -8,14 +8,14 @@ import {ShortStrings, ShortString} from "../ShortStrings.sol"; import {IERC5267} from "../../interfaces/IERC5267.sol"; /** - * @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data. + * @dev https://eips.ethereum.org/EIPS/eip-712[EIP-712] is a standard for hashing and signing of typed structured data. * * The encoding scheme specified in the EIP requires a domain separator and a hash of the typed structured data, whose * encoding is very generic and therefore its implementation in Solidity is not feasible, thus this contract * does not implement the encoding itself. Protocols need to implement the type-specific encoding they need in order to * produce the hash of their typed data using a combination of `abi.encode` and `keccak256`. * - * This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding + * This contract implements the EIP-712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding * scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA * ({_hashTypedDataV4}). * @@ -55,7 +55,7 @@ abstract contract EIP712 is IERC5267 { * @dev Initializes the domain separator and parameter caches. * * The meaning of `name` and `version` is specified in - * https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP 712]: + * https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP-712]: * * - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol. * - `version`: the current major version of the signing domain. diff --git a/contracts/utils/cryptography/MessageHashUtils.sol b/contracts/utils/cryptography/MessageHashUtils.sol index 8836693e79b..45c2421ad96 100644 --- a/contracts/utils/cryptography/MessageHashUtils.sol +++ b/contracts/utils/cryptography/MessageHashUtils.sol @@ -9,12 +9,12 @@ import {Strings} from "../Strings.sol"; * @dev Signature message hash utilities for producing digests to be consumed by {ECDSA} recovery or signing. * * The library provides methods for generating a hash of a message that conforms to the - * https://eips.ethereum.org/EIPS/eip-191[EIP 191] and https://eips.ethereum.org/EIPS/eip-712[EIP 712] + * https://eips.ethereum.org/EIPS/eip-191[ERC-191] and https://eips.ethereum.org/EIPS/eip-712[EIP 712] * specifications. */ library MessageHashUtils { /** - * @dev Returns the keccak256 digest of an EIP-191 signed data with version + * @dev Returns the keccak256 digest of an ERC-191 signed data with version * `0x45` (`personal_sign` messages). * * The digest is calculated by prefixing a bytes32 `messageHash` with @@ -37,7 +37,7 @@ library MessageHashUtils { } /** - * @dev Returns the keccak256 digest of an EIP-191 signed data with version + * @dev Returns the keccak256 digest of an ERC-191 signed data with version * `0x45` (`personal_sign` messages). * * The digest is calculated by prefixing an arbitrary `message` with @@ -52,7 +52,7 @@ library MessageHashUtils { } /** - * @dev Returns the keccak256 digest of an EIP-191 signed data with version + * @dev Returns the keccak256 digest of an ERC-191 signed data with version * `0x00` (data with intended validator). * * The digest is calculated by prefixing an arbitrary `data` with `"\x19\x00"` and the intended @@ -65,7 +65,7 @@ library MessageHashUtils { } /** - * @dev Returns the keccak256 digest of an EIP-712 typed data (EIP-191 version `0x01`). + * @dev Returns the keccak256 digest of an EIP-712 typed data (ERC-191 version `0x01`). * * The digest is calculated from a `domainSeparator` and a `structHash`, by prefixing them with * `\x19\x01` and hashing the result. It corresponds to the hash signed by the diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index 59a2c6d90df..7eb0fea907b 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -8,13 +8,13 @@ import {IERC1271} from "../../interfaces/IERC1271.sol"; /** * @dev Signature verification helper that can be used instead of `ECDSA.recover` to seamlessly support both ECDSA - * signatures from externally owned accounts (EOAs) as well as ERC1271 signatures from smart contract wallets like + * signatures from externally owned accounts (EOAs) as well as ERC-1271 signatures from smart contract wallets like * Argent and Safe Wallet (previously Gnosis Safe). */ library SignatureChecker { /** * @dev Checks if a signature is valid for a given signer and data hash. If the signer is a smart contract, the - * signature is validated against that smart contract using ERC1271, otherwise it's validated using `ECDSA.recover`. + * signature is validated against that smart contract using ERC-1271, otherwise it's validated using `ECDSA.recover`. * * NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus * change through time. It could return true at block N and false at block N+1 (or the opposite). @@ -28,7 +28,7 @@ library SignatureChecker { /** * @dev Checks if a signature is valid for a given signer and data hash. The signature is validated - * against the signer smart contract using ERC1271. + * against the signer smart contract using ERC-1271. * * NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus * change through time. It could return true at block N and false at block N+1 (or the opposite). diff --git a/contracts/utils/introspection/ERC165.sol b/contracts/utils/introspection/ERC165.sol index 1e77b60d739..664b39fc19d 100644 --- a/contracts/utils/introspection/ERC165.sol +++ b/contracts/utils/introspection/ERC165.sol @@ -8,7 +8,7 @@ import {IERC165} from "./IERC165.sol"; /** * @dev Implementation of the {IERC165} interface. * - * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check + * Contracts that want to implement ERC-165 should inherit from this contract and override {supportsInterface} to check * for the additional interface id that will be supported. For example: * * ```solidity diff --git a/contracts/utils/introspection/ERC165Checker.sol b/contracts/utils/introspection/ERC165Checker.sol index 7b52241446d..da729caf815 100644 --- a/contracts/utils/introspection/ERC165Checker.sol +++ b/contracts/utils/introspection/ERC165Checker.sol @@ -13,14 +13,14 @@ import {IERC165} from "./IERC165.sol"; * what to do in these cases. */ library ERC165Checker { - // As per the EIP-165 spec, no interface should ever match 0xffffffff + // As per the ERC-165 spec, no interface should ever match 0xffffffff bytes4 private constant INTERFACE_ID_INVALID = 0xffffffff; /** * @dev Returns true if `account` supports the {IERC165} interface. */ function supportsERC165(address account) internal view returns (bool) { - // Any contract that implements ERC165 must explicitly indicate support of + // Any contract that implements ERC-165 must explicitly indicate support of // InterfaceId_ERC165 and explicitly indicate non-support of InterfaceId_Invalid return supportsERC165InterfaceUnchecked(account, type(IERC165).interfaceId) && @@ -34,7 +34,7 @@ library ERC165Checker { * See {IERC165-supportsInterface}. */ function supportsInterface(address account, bytes4 interfaceId) internal view returns (bool) { - // query support of both ERC165 as per the spec and support of _interfaceId + // query support of both ERC-165 as per the spec and support of _interfaceId return supportsERC165(account) && supportsERC165InterfaceUnchecked(account, interfaceId); } @@ -53,7 +53,7 @@ library ERC165Checker { // an array of booleans corresponding to interfaceIds and whether they're supported or not bool[] memory interfaceIdsSupported = new bool[](interfaceIds.length); - // query support of ERC165 itself + // query support of ERC-165 itself if (supportsERC165(account)) { // query support of each interface in interfaceIds for (uint256 i = 0; i < interfaceIds.length; i++) { @@ -74,7 +74,7 @@ library ERC165Checker { * See {IERC165-supportsInterface}. */ function supportsAllInterfaces(address account, bytes4[] memory interfaceIds) internal view returns (bool) { - // query support of ERC165 itself + // query support of ERC-165 itself if (!supportsERC165(account)) { return false; } @@ -91,12 +91,12 @@ library ERC165Checker { } /** - * @notice Query if a contract implements an interface, does not check ERC165 support + * @notice Query if a contract implements an interface, does not check ERC-165 support * @param account The address of the contract to query for support of an interface * @param interfaceId The interface identifier, as specified in ERC-165 * @return true if the contract at account indicates support of the interface with * identifier interfaceId, false otherwise - * @dev Assumes that account contains a contract that supports ERC165, otherwise + * @dev Assumes that account contains a contract that supports ERC-165, otherwise * the behavior of this method is undefined. This precondition can be checked * with {supportsERC165}. * diff --git a/contracts/utils/introspection/IERC165.sol b/contracts/utils/introspection/IERC165.sol index c09f31fe128..bfbdf5dda39 100644 --- a/contracts/utils/introspection/IERC165.sol +++ b/contracts/utils/introspection/IERC165.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.20; /** - * @dev Interface of the ERC165 standard, as defined in the - * https://eips.ethereum.org/EIPS/eip-165[EIP]. + * @dev Interface of the ERC-165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[ERC]. * * Implementers can declare support of contract interfaces, which can then be * queried by others ({ERC165Checker}). @@ -16,7 +16,7 @@ interface IERC165 { /** * @dev Returns true if this contract implements the interface defined by * `interfaceId`. See the corresponding - * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section] * to learn more about how these ids are created. * * This function call must use less than 30 000 gas. diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 918c60a9581..15af8b40ea3 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -8,11 +8,11 @@ * xref:access-control.adoc[Access Control] * xref:tokens.adoc[Tokens] -** xref:erc20.adoc[ERC20] +** xref:erc20.adoc[ERC-20] *** xref:erc20-supply.adoc[Creating Supply] -** xref:erc721.adoc[ERC721] -** xref:erc1155.adoc[ERC1155] -** xref:erc4626.adoc[ERC4626] +** xref:erc721.adoc[ERC-721] +** xref:erc1155.adoc[ERC-1155] +** xref:erc4626.adoc[ERC-4626] * xref:governance.adoc[Governance] diff --git a/docs/modules/ROOT/pages/access-control.adoc b/docs/modules/ROOT/pages/access-control.adoc index baf5652f8fe..c31f4cf4363 100644 --- a/docs/modules/ROOT/pages/access-control.adoc +++ b/docs/modules/ROOT/pages/access-control.adoc @@ -43,7 +43,7 @@ Most software uses access control systems that are role-based: some users are re OpenZeppelin Contracts provides xref:api:access.adoc#AccessControl[`AccessControl`] for implementing role-based access control. Its usage is straightforward: for each role that you want to define, you will create a new _role identifier_ that is used to grant, revoke, and check if an account has that role. -Here's a simple example of using `AccessControl` in an xref:erc20.adoc[`ERC20` token] to define a 'minter' role, which allows accounts that have it create new tokens: +Here's a simple example of using `AccessControl` in an xref:erc20.adoc[ERC-20 token] to define a 'minter' role, which allows accounts that have it create new tokens: [source,solidity] ---- @@ -54,7 +54,7 @@ NOTE: Make sure you fully understand how xref:api:access.adoc#AccessControl[`Acc While clear and explicit, this isn't anything we wouldn't have been able to achieve with `Ownable`. Indeed, where `AccessControl` shines is in scenarios where granular permissions are required, which can be implemented by defining _multiple_ roles. -Let's augment our ERC20 token example by also defining a 'burner' role, which lets accounts destroy tokens, and by using the `onlyRole` modifier: +Let's augment our ERC-20 token example by also defining a 'burner' role, which lets accounts destroy tokens, and by using the `onlyRole` modifier: [source,solidity] ---- @@ -66,7 +66,7 @@ So clean! By splitting concerns this way, more granular levels of permission may [[granting-and-revoking]] === Granting and Revoking Roles -The ERC20 token example above uses `_grantRole`, an `internal` function that is useful when programmatically assigning roles (such as during construction). But what if we later want to grant the 'minter' role to additional accounts? +The ERC-20 token example above uses `_grantRole`, an `internal` function that is useful when programmatically assigning roles (such as during construction). But what if we later want to grant the 'minter' role to additional accounts? By default, **accounts with a role cannot grant it or revoke it from other accounts**: all having a role does is making the `hasRole` check pass. To grant and revoke roles dynamically, you will need help from the _role's admin_. @@ -76,7 +76,7 @@ This mechanism can be used to create complex permissioning structures resembling Since it is the admin for all roles by default, and in fact it is also its own admin, this role carries significant risk. To mitigate this risk we provide xref:api:access.adoc#AccessControlDefaultAdminRules[`AccessControlDefaultAdminRules`], a recommended extension of `AccessControl` that adds a number of enforced security measures for this role: the admin is restricted to a single account, with a 2-step transfer procedure with a delay in between steps. -Let's take a look at the ERC20 token example, this time taking advantage of the default admin role: +Let's take a look at the ERC-20 token example, this time taking advantage of the default admin role: [source,solidity] ---- @@ -165,7 +165,7 @@ OpenZeppelin Contracts provides xref:api:access.adoc#AccessManager[`AccessManage In order to restrict access to some functions of your contract, you should inherit from the xref:api:access.adoc#AccessManaged[`AccessManaged`] contract provided along with the manager. This provides the `restricted` modifier that can be used to protect any externally facing function. Note that you will have to specify the address of the AccessManager instance (xref:api:access.adoc#AccessManaged-constructor-address-[`initialAuthority`]) in the constructor so the `restricted` modifier knows which manager to use for checking permissions. -Here's a simple example of an xref:tokens.adoc#ERC20[`ERC20` token] that defines a `mint` function that is restricted by an xref:api:access.adoc#AccessManager[`AccessManager`]: +Here's a simple example of an xref:tokens.adoc#ERC20[ERC-20 token] that defines a `mint` function that is restricted by an xref:api:access.adoc#AccessManager[`AccessManager`]: ```solidity include::api:example$access-control/AccessManagedERC20MintBase.sol[] diff --git a/docs/modules/ROOT/pages/erc1155.adoc b/docs/modules/ROOT/pages/erc1155.adoc index 5a4c91670c6..0d771048e17 100644 --- a/docs/modules/ROOT/pages/erc1155.adoc +++ b/docs/modules/ROOT/pages/erc1155.adoc @@ -1,17 +1,17 @@ -= ERC1155 += ERC-1155 -ERC1155 is a novel token standard that aims to take the best from previous standards to create a xref:tokens.adoc#different-kinds-of-tokens[*fungibility-agnostic*] and *gas-efficient* xref:tokens.adoc#but_first_coffee_a_primer_on_token_contracts[token contract]. +ERC-1155 is a novel token standard that aims to take the best from previous standards to create a xref:tokens.adoc#different-kinds-of-tokens[*fungibility-agnostic*] and *gas-efficient* xref:tokens.adoc#but_first_coffee_a_primer_on_token_contracts[token contract]. -TIP: ERC1155 draws ideas from all of xref:erc20.adoc[ERC20], xref:erc721.adoc[ERC721], and https://eips.ethereum.org/EIPS/eip-777[ERC777]. If you're unfamiliar with those standards, head to their guides before moving on. +TIP: ERC-1155 draws ideas from all of xref:erc20.adoc[ERC-20], xref:erc721.adoc[ERC-721], and https://eips.ethereum.org/EIPS/eip-777[ERC-777]. If you're unfamiliar with those standards, head to their guides before moving on. [[multi-token-standard]] == Multi Token Standard -The distinctive feature of ERC1155 is that it uses a single smart contract to represent multiple tokens at once. This is why its xref:api:token/ERC1155.adoc#IERC1155-balanceOf-address-uint256-[`balanceOf`] function differs from ERC20's and ERC777's: it has an additional `id` argument for the identifier of the token that you want to query the balance of. +The distinctive feature of ERC-1155 is that it uses a single smart contract to represent multiple tokens at once. This is why its xref:api:token/ERC1155.adoc#IERC1155-balanceOf-address-uint256-[`balanceOf`] function differs from ERC-20's and ERC-777's: it has an additional `id` argument for the identifier of the token that you want to query the balance of. -This is similar to how ERC721 does things, but in that standard a token `id` has no concept of balance: each token is non-fungible and exists or doesn't. The ERC721 xref:api:token/ERC721.adoc#IERC721-balanceOf-address-[`balanceOf`] function refers to _how many different tokens_ an account has, not how many of each. On the other hand, in ERC1155 accounts have a distinct balance for each token `id`, and non-fungible tokens are implemented by simply minting a single one of them. +This is similar to how ERC-721 does things, but in that standard a token `id` has no concept of balance: each token is non-fungible and exists or doesn't. The ERC-721 xref:api:token/ERC721.adoc#IERC721-balanceOf-address-[`balanceOf`] function refers to _how many different tokens_ an account has, not how many of each. On the other hand, in ERC-1155 accounts have a distinct balance for each token `id`, and non-fungible tokens are implemented by simply minting a single one of them. -This approach leads to massive gas savings for projects that require multiple tokens. Instead of deploying a new contract for each token type, a single ERC1155 token contract can hold the entire system state, reducing deployment costs and complexity. +This approach leads to massive gas savings for projects that require multiple tokens. Instead of deploying a new contract for each token type, a single ERC-1155 token contract can hold the entire system state, reducing deployment costs and complexity. [[batch-operations]] == Batch Operations @@ -20,13 +20,13 @@ Because all state is held in a single contract, it is possible to operate over m In the spirit of the standard, we've also included batch operations in the non-standard functions, such as xref:api:token/ERC1155.adoc#ERC1155-_mintBatch-address-uint256---uint256---bytes-[`_mintBatch`]. -== Constructing an ERC1155 Token Contract +== Constructing an ERC-1155 Token Contract -We'll use ERC1155 to track multiple items in our game, which will each have their own unique attributes. We mint all items to the deployer of the contract, which we can later transfer to players. Players are free to keep their tokens or trade them with other people as they see fit, as they would any other asset on the blockchain! +We'll use ERC-1155 to track multiple items in our game, which will each have their own unique attributes. We mint all items to the deployer of the contract, which we can later transfer to players. Players are free to keep their tokens or trade them with other people as they see fit, as they would any other asset on the blockchain! For simplicity, we will mint all items in the constructor, but you could add minting functionality to the contract to mint on demand to players. -TIP: For an overview of minting mechanisms, check out xref:erc20-supply.adoc[Creating ERC20 Supply]. +TIP: For an overview of minting mechanisms, check out xref:erc20-supply.adoc[Creating ERC-20 Supply]. Here's what a contract for tokenized items might look like: @@ -59,7 +59,7 @@ Note that for our Game Items, Gold is a fungible token whilst Thor's Hammer is a The xref:api:token/ERC1155.adoc#ERC1155[`ERC1155`] contract includes the optional extension xref:api:token/ERC1155.adoc#IERC1155MetadataURI[`IERC1155MetadataURI`]. That's where the xref:api:token/ERC1155.adoc#IERC1155MetadataURI-uri-uint256-[`uri`] function comes from: we use it to retrieve the metadata uri. -Also note that, unlike ERC20, ERC1155 lacks a `decimals` field, since each token is distinct and cannot be partitioned. +Also note that, unlike ERC-20, ERC-1155 lacks a `decimals` field, since each token is distinct and cannot be partitioned. Once deployed, we will be able to query the deployer’s balance: [source,javascript] @@ -114,7 +114,7 @@ For more information about the metadata JSON Schema, check out the https://githu NOTE: You'll notice that the item's information is included in the metadata, but that information isn't on-chain! So a game developer could change the underlying metadata, changing the rules of the game! -TIP: If you'd like to put all item information on-chain, you can extend ERC721 to do so (though it will be rather costly) by providing a xref:utilities.adoc#base64[`Base64`] Data URI with the JSON schema encoded. You could also leverage IPFS to store the URI information, but these techniques are out of the scope of this overview guide +TIP: If you'd like to put all item information on-chain, you can extend ERC-721 to do so (though it will be rather costly) by providing a xref:utilities.adoc#base64[`Base64`] Data URI with the JSON schema encoded. You could also leverage IPFS to store the URI information, but these techniques are out of the scope of this overview guide [[sending-to-contracts]] == Sending Tokens to Contracts @@ -123,12 +123,12 @@ A key difference when using xref:api:token/ERC1155.adoc#IERC1155-safeTransferFro [source,text] ---- -ERC1155: transfer to non ERC1155Receiver implementer +ERC-1155: transfer to non ERC-1155 Receiver implementer ---- -This is a good thing! It means that the recipient contract has not registered itself as aware of the ERC1155 protocol, so transfers to it are disabled to *prevent tokens from being locked forever*. As an example, https://etherscan.io/token/0xa74476443119A942dE498590Fe1f2454d7D4aC0d?a=0xa74476443119A942dE498590Fe1f2454d7D4aC0d[the Golem contract currently holds over 350k `GNT` tokens], worth multiple tens of thousands of dollars, and lacks methods to get them out of there. This has happened to virtually every ERC20-backed project, usually due to user error. +This is a good thing! It means that the recipient contract has not registered itself as aware of the ERC-1155 protocol, so transfers to it are disabled to *prevent tokens from being locked forever*. As an example, https://etherscan.io/token/0xa74476443119A942dE498590Fe1f2454d7D4aC0d?a=0xa74476443119A942dE498590Fe1f2454d7D4aC0d[the Golem contract currently holds over 350k `GNT` tokens], worth multiple tens of thousands of dollars, and lacks methods to get them out of there. This has happened to virtually every ERC20-backed project, usually due to user error. -In order for our contract to receive ERC1155 tokens we can inherit from the convenience contract xref:api:token/ERC1155.adoc#ERC1155Holder[`ERC1155Holder`] which handles the registering for us. Though we need to remember to implement functionality to allow tokens to be transferred out of our contract: +In order for our contract to receive ERC-1155 tokens we can inherit from the convenience contract xref:api:token/ERC1155.adoc#ERC1155Holder[`ERC1155Holder`] which handles the registering for us. Though we need to remember to implement functionality to allow tokens to be transferred out of our contract: [source,solidity] ---- diff --git a/docs/modules/ROOT/pages/erc20-supply.adoc b/docs/modules/ROOT/pages/erc20-supply.adoc index bf6e240583f..ae21e4a8adb 100644 --- a/docs/modules/ROOT/pages/erc20-supply.adoc +++ b/docs/modules/ROOT/pages/erc20-supply.adoc @@ -1,10 +1,10 @@ -= Creating ERC20 Supply += Creating ERC-20 Supply -In this guide, you will learn how to create an ERC20 token with a custom supply mechanism. We will showcase two idiomatic ways to use OpenZeppelin Contracts for this purpose that you will be able to apply to your smart contract development practice. +In this guide, you will learn how to create an ERC-20 token with a custom supply mechanism. We will showcase two idiomatic ways to use OpenZeppelin Contracts for this purpose that you will be able to apply to your smart contract development practice. -The standard interface implemented by tokens built on Ethereum is called ERC20, and Contracts includes a widely used implementation of it: the aptly named xref:api:token/ERC20.adoc[`ERC20`] contract. This contract, like the standard itself, is quite simple and bare-bones. In fact, if you try to deploy an instance of `ERC20` as-is it will be quite literally useless... it will have no supply! What use is a token with no supply? +The standard interface implemented by tokens built on Ethereum is called ERC-20, and Contracts includes a widely used implementation of it: the aptly named xref:api:token/ERC20.adoc[`ERC20`] contract. This contract, like the standard itself, is quite simple and bare-bones. In fact, if you try to deploy an instance of `ERC20` as-is it will be quite literally useless... it will have no supply! What use is a token with no supply? -The way that supply is created is not defined in the ERC20 document. Every token is free to experiment with its own mechanisms, ranging from the most decentralized to the most centralized, from the most naive to the most researched, and more. +The way that supply is created is not defined in the ERC-20 document. Every token is free to experiment with its own mechanisms, ranging from the most decentralized to the most centralized, from the most naive to the most researched, and more. [[fixed-supply]] == Fixed Supply @@ -37,7 +37,7 @@ Encapsulating state like this makes it safer to extend contracts. For instance, [[rewarding-miners]] == Rewarding Miners -The internal xref:api:token/ERC20.adoc#ERC20-_mint-address-uint256-[`_mint`] function is the key building block that allows us to write ERC20 extensions that implement a supply mechanism. +The internal xref:api:token/ERC20.adoc#ERC20-_mint-address-uint256-[`_mint`] function is the key building block that allows us to write ERC-20 extensions that implement a supply mechanism. The mechanism we will implement is a token reward for the miners that produce Ethereum blocks. In Solidity, we can access the address of the current block's miner in the global variable `block.coinbase`. We will mint a token reward to this address whenever someone calls the function `mintMinerReward()` on our token. The mechanism may sound silly, but you never know what kind of dynamic this might result in, and it's worth analyzing and experimenting with! @@ -68,4 +68,4 @@ include::api:example$ERC20WithAutoMinerReward.sol[] [[wrapping-up]] == Wrapping Up -We've seen how to implement a ERC20 supply mechanism: internally through `_mint`. Hopefully this has helped you understand how to use OpenZeppelin Contracts and some of the design principles behind it, and you can apply them to your own smart contracts. +We've seen how to implement a ERC-20 supply mechanism: internally through `_mint`. Hopefully this has helped you understand how to use OpenZeppelin Contracts and some of the design principles behind it, and you can apply them to your own smart contracts. diff --git a/docs/modules/ROOT/pages/erc20.adoc b/docs/modules/ROOT/pages/erc20.adoc index 2b85070a67b..620b85a2627 100644 --- a/docs/modules/ROOT/pages/erc20.adoc +++ b/docs/modules/ROOT/pages/erc20.adoc @@ -1,13 +1,13 @@ -= ERC20 += ERC-20 -An ERC20 token contract keeps track of xref:tokens.adoc#different-kinds-of-tokens[_fungible_ tokens]: any one token is exactly equal to any other token; no tokens have special rights or behavior associated with them. This makes ERC20 tokens useful for things like a *medium of exchange currency*, *voting rights*, *staking*, and more. +An ERC-20 token contract keeps track of xref:tokens.adoc#different-kinds-of-tokens[_fungible_ tokens]: any one token is exactly equal to any other token; no tokens have special rights or behavior associated with them. This makes ERC-20 tokens useful for things like a *medium of exchange currency*, *voting rights*, *staking*, and more. OpenZeppelin Contracts provides many ERC20-related contracts. On the xref:api:token/ERC20.adoc[`API reference`] you'll find detailed information on their properties and usage. [[constructing-an-erc20-token-contract]] -== Constructing an ERC20 Token Contract +== Constructing an ERC-20 Token Contract -Using Contracts, we can easily create our own ERC20 token contract, which will be used to track _Gold_ (GLD), an internal currency in a hypothetical game. +Using Contracts, we can easily create our own ERC-20 token contract, which will be used to track _Gold_ (GLD), an internal currency in a hypothetical game. Here's what our GLD token might look like. @@ -28,7 +28,7 @@ contract GLDToken is ERC20 { Our contracts are often used via https://solidity.readthedocs.io/en/latest/contracts.html#inheritance[inheritance], and here we're reusing xref:api:token/ERC20.adoc#erc20[`ERC20`] for both the basic standard implementation and the xref:api:token/ERC20.adoc#ERC20-name--[`name`], xref:api:token/ERC20.adoc#ERC20-symbol--[`symbol`], and xref:api:token/ERC20.adoc#ERC20-decimals--[`decimals`] optional extensions. Additionally, we're creating an `initialSupply` of tokens, which will be assigned to the address that deploys the contract. -TIP: For a more complete discussion of ERC20 supply mechanisms, see xref:erc20-supply.adoc[Creating ERC20 Supply]. +TIP: For a more complete discussion of ERC-20 supply mechanisms, see xref:erc20-supply.adoc[Creating ERC-20 Supply]. That's it! Once deployed, we will be able to query the deployer's balance: @@ -60,7 +60,7 @@ How can this be achieved? It's actually very simple: a token contract can use la It is important to understand that `decimals` is _only used for display purposes_. All arithmetic inside the contract is still performed on integers, and it is the different user interfaces (wallets, exchanges, etc.) that must adjust the displayed values according to `decimals`. The total token supply and balance of each account are not specified in `GLD`: you need to divide by `10 ** decimals` to get the actual `GLD` amount. -You'll probably want to use a `decimals` value of `18`, just like Ether and most ERC20 token contracts in use, unless you have a very special reason not to. When minting tokens or transferring them around, you will be actually sending the number `num GLD * (10 ** decimals)`. +You'll probably want to use a `decimals` value of `18`, just like Ether and most ERC-20 token contracts in use, unless you have a very special reason not to. When minting tokens or transferring them around, you will be actually sending the number `num GLD * (10 ** decimals)`. NOTE: By default, `ERC20` uses a value of `18` for `decimals`. To use a different value, you will need to override the `decimals()` function in your contract. diff --git a/docs/modules/ROOT/pages/erc4626.adoc b/docs/modules/ROOT/pages/erc4626.adoc index c42426add09..03f3c216c70 100644 --- a/docs/modules/ROOT/pages/erc4626.adoc +++ b/docs/modules/ROOT/pages/erc4626.adoc @@ -1,16 +1,16 @@ -= ERC4626 += ERC-4626 :stem: latexmath -https://eips.ethereum.org/EIPS/eip-4626[ERC4626] is an extension of xref:erc20.adoc[ERC20] that proposes a standard interface for token vaults. This standard interface can be used by widely different contracts (including lending markets, aggregators, and intrinsically interest bearing tokens), which brings a number of subtleties. Navigating these potential issues is essential to implementing a compliant and composable token vault. +https://eips.ethereum.org/EIPS/eip-4626[ERC-4626] is an extension of xref:erc20.adoc[ERC-20] that proposes a standard interface for token vaults. This standard interface can be used by widely different contracts (including lending markets, aggregators, and intrinsically interest bearing tokens), which brings a number of subtleties. Navigating these potential issues is essential to implementing a compliant and composable token vault. -We provide a base implementation of ERC4626 that includes a simple vault. This contract is designed in a way that allows developers to easily re-configure the vault's behavior, with minimal overrides, while staying compliant. In this guide, we will discuss some security considerations that affect ERC4626. We will also discuss common customizations of the vault. +We provide a base implementation of ERC-4626 that includes a simple vault. This contract is designed in a way that allows developers to easily re-configure the vault's behavior, with minimal overrides, while staying compliant. In this guide, we will discuss some security considerations that affect ERC-4626. We will also discuss common customizations of the vault. [[inflation-attack]] == Security concern: Inflation attack === Visualizing the vault -In exchange for the assets deposited into an ERC4626 vault, a user receives shares. These shares can later be burned to redeem the corresponding underlying assets. The number of shares a user gets depends on the amount of assets they put in and on the exchange rate of the vault. This exchange rate is defined by the current liquidity held by the vault. +In exchange for the assets deposited into an ERC-4626 vault, a user receives shares. These shares can later be burned to redeem the corresponding underlying assets. The number of shares a user gets depends on the amount of assets they put in and on the exchange rate of the vault. This exchange rate is defined by the current liquidity held by the vault. - If a vault has 100 tokens to back 200 shares, then each share is worth 0.5 assets. - If a vault has 200 tokens to back 100 shares, then each share is worth 2.0 assets. @@ -195,7 +195,7 @@ stem:[\delta = 6], stem:[a_0 = 1], stem:[a_1 = 10^5] [[fees]] == Custom behavior: Adding fees to the vault -In an ERC4626 vaults, fees can be captured during the deposit/mint and/or during the withdraw/redeem steps. In both cases it is essential to remain compliant with the ERC4626 requirements with regard to the preview functions. +In an ERC-4626 vaults, fees can be captured during the deposit/mint and/or during the withdraw/redeem steps. In both cases it is essential to remain compliant with the ERC-4626 requirements with regard to the preview functions. For example, if calling `deposit(100, receiver)`, the caller should deposit exactly 100 underlying tokens, including fees, and the receiver should receive a number of shares that matches the value returned by `previewDeposit(100)`. Similarly, `previewMint` should account for the fees that the user will have to pay on top of share's cost. diff --git a/docs/modules/ROOT/pages/erc721.adoc b/docs/modules/ROOT/pages/erc721.adoc index 7481c6b6254..269a78c6db2 100644 --- a/docs/modules/ROOT/pages/erc721.adoc +++ b/docs/modules/ROOT/pages/erc721.adoc @@ -1,12 +1,12 @@ -= ERC721 += ERC-721 -We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC20], but what if not all tokens are alike? This comes up in situations like *real estate*, *voting rights*, or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. +We've discussed how you can make a _fungible_ token using xref:erc20.adoc[ERC-20], but what if not all tokens are alike? This comes up in situations like *real estate*, *voting rights*, or *collectibles*, where some items are valued more than others, due to their usefulness, rarity, etc. ERC-721 is a standard for representing ownership of xref:tokens.adoc#different-kinds-of-tokens[_non-fungible_ tokens], that is, where each token is unique. -ERC721 is a more complex standard than ERC20, with multiple optional extensions, and is split across a number of contracts. The OpenZeppelin Contracts provide flexibility regarding how these are combined, along with custom useful extensions. Check out the xref:api:token/ERC721.adoc[API Reference] to learn more about these. +ERC-721 is a more complex standard than ERC-20, with multiple optional extensions, and is split across a number of contracts. The OpenZeppelin Contracts provide flexibility regarding how these are combined, along with custom useful extensions. Check out the xref:api:token/ERC721.adoc[API Reference] to learn more about these. -== Constructing an ERC721 Token Contract +== Constructing an ERC-721 Token Contract -We'll use ERC721 to track items in our game, which will each have their own unique attributes. Whenever one is to be awarded to a player, it will be minted and sent to them. Players are free to keep their token or trade it with other people as they see fit, as they would any other asset on the blockchain! Please note any account can call `awardItem` to mint items. To restrict what accounts can mint items we can add xref:access-control.adoc[Access Control]. +We'll use ERC-721 to track items in our game, which will each have their own unique attributes. Whenever one is to be awarded to a player, it will be minted and sent to them. Players are free to keep their token or trade it with other people as they see fit, as they would any other asset on the blockchain! Please note any account can call `awardItem` to mint items. To restrict what accounts can mint items we can add xref:access-control.adoc[Access Control]. Here's what a contract for tokenized items might look like: @@ -36,9 +36,9 @@ contract GameItem is ERC721URIStorage { } ---- -The xref:api:token/ERC721.adoc#ERC721URIStorage[`ERC721URIStorage`] contract is an implementation of ERC721 that includes the metadata standard extensions (xref:api:token/ERC721.adoc#IERC721Metadata[`IERC721Metadata`]) as well as a mechanism for per-token metadata. That's where the xref:api:token/ERC721.adoc#ERC721-_setTokenURI-uint256-string-[`_setTokenURI`] method comes from: we use it to store an item's metadata. +The xref:api:token/ERC721.adoc#ERC721URIStorage[`ERC721URIStorage`] contract is an implementation of ERC-721 that includes the metadata standard extensions (xref:api:token/ERC721.adoc#IERC721Metadata[`IERC721Metadata`]) as well as a mechanism for per-token metadata. That's where the xref:api:token/ERC721.adoc#ERC721-_setTokenURI-uint256-string-[`_setTokenURI`] method comes from: we use it to store an item's metadata. -Also note that, unlike ERC20, ERC721 lacks a `decimals` field, since each token is distinct and cannot be partitioned. +Also note that, unlike ERC-20, ERC-721 lacks a `decimals` field, since each token is distinct and cannot be partitioned. New items can be created: @@ -72,8 +72,8 @@ This `tokenURI` should resolve to a JSON document that might look something like } ---- -For more information about the `tokenURI` metadata JSON Schema, check out the https://eips.ethereum.org/EIPS/eip-721[ERC721 specification]. +For more information about the `tokenURI` metadata JSON Schema, check out the https://eips.ethereum.org/EIPS/eip-721[ERC-721 specification]. NOTE: You'll notice that the item's information is included in the metadata, but that information isn't on-chain! So a game developer could change the underlying metadata, changing the rules of the game! -TIP: If you'd like to put all item information on-chain, you can extend ERC721 to do so (though it will be rather costly) by providing a xref:utilities.adoc#base64[`Base64`] Data URI with the JSON schema encoded. You could also leverage IPFS to store the tokenURI information, but these techniques are out of the scope of this overview guide. +TIP: If you'd like to put all item information on-chain, you can extend ERC-721 to do so (though it will be rather costly) by providing a xref:utilities.adoc#base64[`Base64`] Data URI with the JSON schema encoded. You could also leverage IPFS to store the tokenURI information, but these techniques are out of the scope of this overview guide. diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index b8db6423005..18c335ff400 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -16,7 +16,7 @@ OpenZeppelin’s Governor system was designed with a concern for compatibility w === ERC20Votes & ERC20VotesComp -The ERC20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. +The ERC-20 extension to keep track of votes and vote delegation is one such case. The shorter one is the more generic version because it can support token supplies greater than 2^96, while the “Comp” variant is limited in that regard, but exactly fits the interface of the COMP token that is used by GovernorAlpha and Bravo. Both contract variants share the same events, so they are fully compatible when looking at events only. === Governor & GovernorStorage @@ -40,7 +40,7 @@ In the rest of this guide, we will focus on a fresh deploy of the vanilla OpenZe === Token -The voting power of each account in our governance setup will be determined by an ERC20 token. The token has to implement the ERC20Votes extension. This extension will keep track of historical balances so that voting power is retrieved from past snapshots rather than current balance, which is an important protection that prevents double voting. +The voting power of each account in our governance setup will be determined by an ERC-20 token. The token has to implement the ERC20Votes extension. This extension will keep track of historical balances so that voting power is retrieved from past snapshots rather than current balance, which is an important protection that prevents double voting. ```solidity include::api:example$governance/MyToken.sol[] @@ -52,7 +52,7 @@ If your project already has a live token that does not include ERC20Votes and is include::api:example$governance/MyTokenWrapped.sol[] ``` -NOTE: The only other source of voting power available in OpenZeppelin Contracts currently is xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`]. ERC721 tokens that don't provide this functionality can be wrapped into a voting tokens using a combination of xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`] and xref:api:token/ERC721Wrapper.adoc#ERC721Wrapper[`ERC721Wrapper`]. +NOTE: The only other source of voting power available in OpenZeppelin Contracts currently is xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`]. ERC-721 tokens that don't provide this functionality can be wrapped into a voting tokens using a combination of xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`] and xref:api:token/ERC721Wrapper.adoc#ERC721Wrapper[`ERC721Wrapper`]. NOTE: The internal clock used by the token to store voting balances will dictate the operating mode of the Governor contract attached to it. By default, block numbers are used. Since v4.9, developers can override the xref:api:interfaces.adoc#IERC6372[IERC6372] clock to use timestamps instead of block numbers. @@ -60,7 +60,7 @@ NOTE: The internal clock used by the token to store voting balances will dictate Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, 3) what options people have when casting a vote and how those votes are counted, and 4) what type of token should be used to vote. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. -For 1) we will use the GovernorVotes module, which hooks to an IVotes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. This module also discovers the clock mode (ERC6372) used by the token and applies it to the Governor. +For 1) we will use the GovernorVotes module, which hooks to an IVotes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. This module also discovers the clock mode (ERC-6372) used by the token and applies it to the Governor. For 2) we will use GovernorVotesQuorumFraction which works together with ERC20Votes to define quorum as a percentage of the total supply at the block a proposal’s voting power is retrieved. This requires a constructor parameter to set the percentage. Most Governors nowadays use 4%, so we will initialize the module with parameter 4 (this indicates the percentage, resulting in 4%). @@ -100,7 +100,7 @@ A proposal is a sequence of actions that the Governor contract will perform if i === Create a Proposal -Let’s say we want to create a proposal to give a team a grant, in the form of ERC20 tokens from the governance treasury. This proposal will consist of a single action where the target is the ERC20 token, calldata is the encoded function call `transfer(, )`, and with 0 ETH attached. +Let’s say we want to create a proposal to give a team a grant, in the form of ERC-20 tokens from the governance treasury. This proposal will consist of a single action where the target is the ERC-20 token, calldata is the encoded function call `transfer(, )`, and with 0 ETH attached. Generally a proposal will be created with the help of an interface such as Tally or Defender. Here we will show how to create the proposal using Ethers.js. @@ -172,7 +172,7 @@ await governor.execute( ); ``` -Executing the proposal will transfer the ERC20 tokens to the chosen recipient. To wrap up: we set up a system where a treasury is controlled by the collective decision of the token holders of a project, and all actions are executed via proposals enforced by on-chain votes. +Executing the proposal will transfer the ERC-20 tokens to the chosen recipient. To wrap up: we set up a system where a treasury is controlled by the collective decision of the token holders of a project, and all actions are executed via proposals enforced by on-chain votes. == Timestamp based governance @@ -235,6 +235,6 @@ contract MyGovernor is Governor, GovernorCountingSimple, GovernorVotes, Governor === Disclaimer -Timestamp based voting is a recent feature that was formalized in EIP-6372 and EIP-5805, and introduced in v4.9. At the time this feature is released, governance tooling such as https://www.tally.xyz[Tally] does not support it yet. While support for timestamps should come soon, users can expect invalid reporting of deadlines & durations. This invalid reporting by offchain tools does not affect the onchain security and functionality of the governance contract. +Timestamp based voting is a recent feature that was formalized in ERC-6372 and ERC-5805, and introduced in v4.9. At the time this feature is released, governance tooling such as https://www.tally.xyz[Tally] does not support it yet. While support for timestamps should come soon, users can expect invalid reporting of deadlines & durations. This invalid reporting by offchain tools does not affect the onchain security and functionality of the governance contract. Governors with timestamp support (v4.9 and above) are compatible with old tokens (before v4.9) and will operate in "block number" mode (which is the mode all old tokens operate on). On the other hand, old Governor instances (before v4.9) are not compatible with new tokens operating using timestamps. If you update your token code to use timestamps, make sure to also update your Governor code. diff --git a/docs/modules/ROOT/pages/tokens.adoc b/docs/modules/ROOT/pages/tokens.adoc index 10626f54801..217c5e04756 100644 --- a/docs/modules/ROOT/pages/tokens.adoc +++ b/docs/modules/ROOT/pages/tokens.adoc @@ -24,8 +24,8 @@ In a nutshell, when dealing with non-fungibles (like your house) you care about Even though the concept of a token is simple, they have a variety of complexities in the implementation. Because everything in Ethereum is just a smart contract, and there are no rules about what smart contracts have to do, the community has developed a variety of *standards* (called EIPs or ERCs) for documenting how a contract can interoperate with other contracts. -You've probably heard of the ERC20 or ERC721 token standards, and that's why you're here. Head to our specialized guides to learn more about these: +You've probably heard of the ERC-20 or ERC-721 token standards, and that's why you're here. Head to our specialized guides to learn more about these: - * xref:erc20.adoc[ERC20]: the most widespread token standard for fungible assets, albeit somewhat limited by its simplicity. - * xref:erc721.adoc[ERC721]: the de-facto solution for non-fungible tokens, often used for collectibles and games. - * xref:erc1155.adoc[ERC1155]: a novel standard for multi-tokens, allowing for a single contract to represent multiple fungible and non-fungible tokens, along with batched operations for increased gas efficiency. + * xref:erc20.adoc[ERC-20]: the most widespread token standard for fungible assets, albeit somewhat limited by its simplicity. + * xref:erc721.adoc[ERC-721]: the de-facto solution for non-fungible tokens, often used for collectibles and games. + * xref:erc1155.adoc[ERC-1155]: a novel standard for multi-tokens, allowing for a single contract to represent multiple fungible and non-fungible tokens, along with batched operations for increased gas efficiency. diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index c194a470553..f940d0d2259 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -36,10 +36,10 @@ xref:api:utils.adoc#MerkleProof[`MerkleProof`] provides: [[introspection]] == Introspection -In Solidity, it's frequently helpful to know whether or not a contract supports an interface you'd like to use. ERC165 is a standard that helps do runtime interface detection. Contracts provide helpers both for implementing ERC165 in your contracts and querying other contracts: +In Solidity, it's frequently helpful to know whether or not a contract supports an interface you'd like to use. ERC-165 is a standard that helps do runtime interface detection. Contracts provide helpers both for implementing ERC-165 in your contracts and querying other contracts: -* xref:api:utils.adoc#IERC165[`IERC165`] — this is the ERC165 interface that defines xref:api:utils.adoc#IERC165-supportsInterface-bytes4-[`supportsInterface`]. When implementing ERC165, you'll conform to this interface. -* xref:api:utils.adoc#ERC165[`ERC165`] — inherit this contract if you'd like to support interface detection using a lookup table in contract storage. You can register interfaces using xref:api:utils.adoc#ERC165-_registerInterface-bytes4-[`_registerInterface(bytes4)`]: check out example usage as part of the ERC721 implementation. +* xref:api:utils.adoc#IERC165[`IERC165`] — this is the ERC-165 interface that defines xref:api:utils.adoc#IERC165-supportsInterface-bytes4-[`supportsInterface`]. When implementing ERC-165, you'll conform to this interface. +* xref:api:utils.adoc#ERC165[`ERC165`] — inherit this contract if you'd like to support interface detection using a lookup table in contract storage. You can register interfaces using xref:api:utils.adoc#ERC165-_registerInterface-bytes4-[`_registerInterface(bytes4)`]: check out example usage as part of the ERC-721 implementation. * xref:api:utils.adoc#ERC165Checker[`ERC165Checker`] — ERC165Checker simplifies the process of checking whether or not a contract supports an interface you care about. * include with `using ERC165Checker for address;` * xref:api:utils.adoc#ERC165Checker-_supportsInterface-address-bytes4-[`myAddress._supportsInterface(bytes4)`] @@ -53,7 +53,7 @@ contract MyContract { bytes4 private InterfaceId_ERC721 = 0x80ac58cd; /** - * @dev transfer an ERC721 token from this contract to someone else + * @dev transfer an ERC-721 token from this contract to someone else */ function transferERC721( address token, @@ -118,9 +118,9 @@ The `Enumerable*` structures are similar to mappings in that they store and remo xref:api:utils.adoc#Base64[`Base64`] util allows you to transform `bytes32` data into its Base64 `string` representation. -This is especially useful for building URL-safe tokenURIs for both xref:api:token/ERC721.adoc#IERC721Metadata-tokenURI-uint256-[`ERC721`] or xref:api:token/ERC1155.adoc#IERC1155MetadataURI-uri-uint256-[`ERC1155`]. This library provides a clever way to serve URL-safe https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URIs/[Data URI] compliant strings to serve on-chain data structures. +This is especially useful for building URL-safe tokenURIs for both xref:api:token/ERC721.adoc#IERC721Metadata-tokenURI-uint256-[`ERC-721`] or xref:api:token/ERC1155.adoc#IERC1155MetadataURI-uri-uint256-[`ERC-1155`]. This library provides a clever way to serve URL-safe https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URIs/[Data URI] compliant strings to serve on-chain data structures. -Here is an example to send JSON Metadata through a Base64 Data URI using an ERC721: +Here is an example to send JSON Metadata through a Base64 Data URI using an ERC-721: [source, solidity] ---- @@ -147,7 +147,7 @@ contract My721Token is ERC721 { bytes memory dataURI = abi.encodePacked( '{', '"name": "My721Token #', tokenId.toString(), '"', - // Replace with extra ERC721 Metadata properties + // Replace with extra ERC-721 Metadata properties '}' ); diff --git a/scripts/generate/templates/StorageSlot.js b/scripts/generate/templates/StorageSlot.js index 1c90b5e75c3..3d2a62a9230 100644 --- a/scripts/generate/templates/StorageSlot.js +++ b/scripts/generate/templates/StorageSlot.js @@ -21,7 +21,7 @@ pragma solidity ^0.8.20; * * The functions in this library return Slot structs that contain a \`value\` member that can be used to read or write. * - * Example usage to set ERC1967 implementation slot: + * Example usage to set ERC-1967 implementation slot: * \`\`\`solidity * contract ERC1967 { * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js index b62160eec62..71e80d7379f 100644 --- a/test/governance/Governor.test.js +++ b/test/governance/Governor.test.js @@ -14,7 +14,7 @@ const { clockFromReceipt } = require('../helpers/time'); const { expectRevertCustomError } = require('../helpers/customError'); const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); -const { shouldBehaveLikeEIP6372 } = require('./utils/EIP6372.behavior'); +const { shouldBehaveLikeERC6372 } = require('./utils/ERC6372.behavior'); const { ZERO_BYTES32 } = require('@openzeppelin/test-helpers/src/constants'); const Governor = artifacts.require('$GovernorMock'); @@ -84,7 +84,7 @@ contract('Governor', function (accounts) { }); shouldSupportInterfaces(['ERC165', 'ERC1155Receiver', 'Governor']); - shouldBehaveLikeEIP6372(mode); + shouldBehaveLikeERC6372(mode); it('deployment check', async function () { expect(await this.mock.name()).to.be.equal(name); diff --git a/test/governance/utils/EIP6372.behavior.js b/test/governance/utils/ERC6372.behavior.js similarity index 81% rename from test/governance/utils/EIP6372.behavior.js rename to test/governance/utils/ERC6372.behavior.js index 022ec35686f..5e8633f01cd 100644 --- a/test/governance/utils/EIP6372.behavior.js +++ b/test/governance/utils/ERC6372.behavior.js @@ -1,7 +1,7 @@ const { clock } = require('../../helpers/time'); -function shouldBehaveLikeEIP6372(mode = 'blocknumber') { - describe('should implement EIP6372', function () { +function shouldBehaveLikeERC6372(mode = 'blocknumber') { + describe('should implement ERC6372', function () { beforeEach(async function () { this.mock = this.mock ?? this.token ?? this.votes; }); @@ -19,5 +19,5 @@ function shouldBehaveLikeEIP6372(mode = 'blocknumber') { } module.exports = { - shouldBehaveLikeEIP6372, + shouldBehaveLikeERC6372, }; diff --git a/test/governance/utils/Votes.behavior.js b/test/governance/utils/Votes.behavior.js index 0aea208a936..68445f0fb65 100644 --- a/test/governance/utils/Votes.behavior.js +++ b/test/governance/utils/Votes.behavior.js @@ -6,7 +6,7 @@ const { fromRpcSig } = require('ethereumjs-util'); const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; -const { shouldBehaveLikeEIP6372 } = require('./EIP6372.behavior'); +const { shouldBehaveLikeERC6372 } = require('./ERC6372.behavior'); const { getDomain, domainType, @@ -26,7 +26,7 @@ const buildAndSignDelegation = (contract, message, pk) => .then(data => fromRpcSig(ethSigUtil.signTypedMessage(pk, { data }))); function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungible = true }) { - shouldBehaveLikeEIP6372(mode); + shouldBehaveLikeERC6372(mode); const getWeight = token => web3.utils.toBN(fungible ? token : 1); diff --git a/test/token/ERC20/extensions/ERC20Votes.test.js b/test/token/ERC20/extensions/ERC20Votes.test.js index 96d6c4e7708..a0da162a425 100644 --- a/test/token/ERC20/extensions/ERC20Votes.test.js +++ b/test/token/ERC20/extensions/ERC20Votes.test.js @@ -38,7 +38,7 @@ contract('ERC20Votes', function (accounts) { this.votes = this.token; }); - // includes EIP6372 behavior check + // includes ERC6372 behavior check shouldBehaveLikeVotes(accounts, [1, 17, 42], { mode, fungible: true }); it('initial nonce is 0', async function () { diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 45020baf9d1..ba9a2a8cb6c 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -26,7 +26,7 @@ contract('ERC721Votes', function (accounts) { this.votes = await artifact.new(name, symbol, name, version); }); - // includes EIP6372 behavior check + // includes ERC6372 behavior check shouldBehaveLikeVotes(accounts, tokens, { mode, fungible: false }); describe('balanceOf', function () { From 0950532d9a89ac652dbbb2aed56678bdfd624c21 Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Thu, 23 Nov 2023 05:38:20 +0000 Subject: [PATCH 15/44] Migrate utils-structs tests to ethersjs (#4748) Co-authored-by: Hadrien Croubois Co-authored-by: ernestognw --- package-lock.json | 7 - package.json | 1 - test/helpers/random.js | 14 ++ test/utils/structs/BitMap.test.js | 76 +++--- test/utils/structs/Checkpoints.test.js | 128 +++++----- test/utils/structs/DoubleEndedQueue.test.js | 100 ++++---- test/utils/structs/EnumerableMap.behavior.js | 147 +++++------- test/utils/structs/EnumerableMap.test.js | 234 +++++++++---------- test/utils/structs/EnumerableSet.behavior.js | 97 ++++---- test/utils/structs/EnumerableSet.test.js | 125 ++++++---- 10 files changed, 455 insertions(+), 474 deletions(-) create mode 100644 test/helpers/random.js diff --git a/package-lock.json b/package-lock.json index 9242999c35c..3c3272ea355 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,6 @@ "hardhat-gas-reporter": "^1.0.9", "hardhat-ignore-warnings": "^0.2.0", "lodash.startcase": "^4.4.0", - "lodash.zip": "^4.2.0", "micromatch": "^4.0.2", "p-limit": "^3.1.0", "prettier": "^3.0.0", @@ -10546,12 +10545,6 @@ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true }, - "node_modules/lodash.zip": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", - "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==", - "dev": true - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", diff --git a/package.json b/package.json index c2c3a26750e..50ba8f47838 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,6 @@ "hardhat-gas-reporter": "^1.0.9", "hardhat-ignore-warnings": "^0.2.0", "lodash.startcase": "^4.4.0", - "lodash.zip": "^4.2.0", "micromatch": "^4.0.2", "p-limit": "^3.1.0", "prettier": "^3.0.0", diff --git a/test/helpers/random.js b/test/helpers/random.js new file mode 100644 index 00000000000..883667fa073 --- /dev/null +++ b/test/helpers/random.js @@ -0,0 +1,14 @@ +const { ethers } = require('hardhat'); + +const randomArray = (generator, arrayLength = 3) => Array(arrayLength).fill().map(generator); + +const generators = { + address: () => ethers.Wallet.createRandom().address, + bytes32: () => ethers.hexlify(ethers.randomBytes(32)), + uint256: () => ethers.toBigInt(ethers.randomBytes(32)), +}; + +module.exports = { + randomArray, + generators, +}; diff --git a/test/utils/structs/BitMap.test.js b/test/utils/structs/BitMap.test.js index 8a1470c5cec..133f1f734b3 100644 --- a/test/utils/structs/BitMap.test.js +++ b/test/utils/structs/BitMap.test.js @@ -1,15 +1,19 @@ -const { BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const BitMap = artifacts.require('$BitMaps'); +async function fixture() { + const bitmap = await ethers.deployContract('$BitMaps'); + return { bitmap }; +} -contract('BitMap', function () { - const keyA = new BN('7891'); - const keyB = new BN('451'); - const keyC = new BN('9592328'); +describe('BitMap', function () { + const keyA = 7891n; + const keyB = 451n; + const keyC = 9592328n; beforeEach(async function () { - this.bitmap = await BitMap.new(); + Object.assign(this, await loadFixture(fixture)); }); it('starts empty', async function () { @@ -35,18 +39,18 @@ contract('BitMap', function () { }); it('set several consecutive keys', async function () { - await this.bitmap.$setTo(0, keyA.addn(0), true); - await this.bitmap.$setTo(0, keyA.addn(1), true); - await this.bitmap.$setTo(0, keyA.addn(2), true); - await this.bitmap.$setTo(0, keyA.addn(3), true); - await this.bitmap.$setTo(0, keyA.addn(4), true); - await this.bitmap.$setTo(0, keyA.addn(2), false); - await this.bitmap.$setTo(0, keyA.addn(4), false); - expect(await this.bitmap.$get(0, keyA.addn(0))).to.equal(true); - expect(await this.bitmap.$get(0, keyA.addn(1))).to.equal(true); - expect(await this.bitmap.$get(0, keyA.addn(2))).to.equal(false); - expect(await this.bitmap.$get(0, keyA.addn(3))).to.equal(true); - expect(await this.bitmap.$get(0, keyA.addn(4))).to.equal(false); + await this.bitmap.$setTo(0, keyA + 0n, true); + await this.bitmap.$setTo(0, keyA + 1n, true); + await this.bitmap.$setTo(0, keyA + 2n, true); + await this.bitmap.$setTo(0, keyA + 3n, true); + await this.bitmap.$setTo(0, keyA + 4n, true); + await this.bitmap.$setTo(0, keyA + 2n, false); + await this.bitmap.$setTo(0, keyA + 4n, false); + expect(await this.bitmap.$get(0, keyA + 0n)).to.equal(true); + expect(await this.bitmap.$get(0, keyA + 1n)).to.equal(true); + expect(await this.bitmap.$get(0, keyA + 2n)).to.equal(false); + expect(await this.bitmap.$get(0, keyA + 3n)).to.equal(true); + expect(await this.bitmap.$get(0, keyA + 4n)).to.equal(false); }); }); @@ -67,14 +71,14 @@ contract('BitMap', function () { }); it('adds several consecutive keys', async function () { - await this.bitmap.$set(0, keyA.addn(0)); - await this.bitmap.$set(0, keyA.addn(1)); - await this.bitmap.$set(0, keyA.addn(3)); - expect(await this.bitmap.$get(0, keyA.addn(0))).to.equal(true); - expect(await this.bitmap.$get(0, keyA.addn(1))).to.equal(true); - expect(await this.bitmap.$get(0, keyA.addn(2))).to.equal(false); - expect(await this.bitmap.$get(0, keyA.addn(3))).to.equal(true); - expect(await this.bitmap.$get(0, keyA.addn(4))).to.equal(false); + await this.bitmap.$set(0, keyA + 0n); + await this.bitmap.$set(0, keyA + 1n); + await this.bitmap.$set(0, keyA + 3n); + expect(await this.bitmap.$get(0, keyA + 0n)).to.equal(true); + expect(await this.bitmap.$get(0, keyA + 1n)).to.equal(true); + expect(await this.bitmap.$get(0, keyA + 2n)).to.equal(false); + expect(await this.bitmap.$get(0, keyA + 3n)).to.equal(true); + expect(await this.bitmap.$get(0, keyA + 4n)).to.equal(false); }); }); @@ -89,15 +93,15 @@ contract('BitMap', function () { }); it('removes consecutive added keys', async function () { - await this.bitmap.$set(0, keyA.addn(0)); - await this.bitmap.$set(0, keyA.addn(1)); - await this.bitmap.$set(0, keyA.addn(3)); - await this.bitmap.$unset(0, keyA.addn(1)); - expect(await this.bitmap.$get(0, keyA.addn(0))).to.equal(true); - expect(await this.bitmap.$get(0, keyA.addn(1))).to.equal(false); - expect(await this.bitmap.$get(0, keyA.addn(2))).to.equal(false); - expect(await this.bitmap.$get(0, keyA.addn(3))).to.equal(true); - expect(await this.bitmap.$get(0, keyA.addn(4))).to.equal(false); + await this.bitmap.$set(0, keyA + 0n); + await this.bitmap.$set(0, keyA + 1n); + await this.bitmap.$set(0, keyA + 3n); + await this.bitmap.$unset(0, keyA + 1n); + expect(await this.bitmap.$get(0, keyA + 0n)).to.equal(true); + expect(await this.bitmap.$get(0, keyA + 1n)).to.equal(false); + expect(await this.bitmap.$get(0, keyA + 2n)).to.equal(false); + expect(await this.bitmap.$get(0, keyA + 3n)).to.equal(true); + expect(await this.bitmap.$get(0, keyA + 4n)).to.equal(false); }); it('adds and removes multiple keys', async function () { diff --git a/test/utils/structs/Checkpoints.test.js b/test/utils/structs/Checkpoints.test.js index 936ac565af6..c5b9e65a05e 100644 --- a/test/utils/structs/Checkpoints.test.js +++ b/test/utils/structs/Checkpoints.test.js @@ -1,71 +1,65 @@ -require('@openzeppelin/test-helpers'); - const { expect } = require('chai'); +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { VALUE_SIZES } = require('../../../scripts/generate/templates/Checkpoints.opts.js'); -const { expectRevertCustomError } = require('../../helpers/customError.js'); -const { expectRevert } = require('@openzeppelin/test-helpers'); - -const $Checkpoints = artifacts.require('$Checkpoints'); - -// The library name may be 'Checkpoints' or 'CheckpointsUpgradeable' -const libraryName = $Checkpoints._json.contractName.replace(/^\$/, ''); -const first = array => (array.length ? array[0] : undefined); const last = array => (array.length ? array[array.length - 1] : undefined); -contract('Checkpoints', function () { - beforeEach(async function () { - this.mock = await $Checkpoints.new(); - }); - +describe('Checkpoints', function () { for (const length of VALUE_SIZES) { describe(`Trace${length}`, function () { - beforeEach(async function () { - this.methods = { - at: (...args) => this.mock.methods[`$at_${libraryName}_Trace${length}(uint256,uint32)`](0, ...args), - latest: (...args) => this.mock.methods[`$latest_${libraryName}_Trace${length}(uint256)`](0, ...args), - latestCheckpoint: (...args) => - this.mock.methods[`$latestCheckpoint_${libraryName}_Trace${length}(uint256)`](0, ...args), - length: (...args) => this.mock.methods[`$length_${libraryName}_Trace${length}(uint256)`](0, ...args), - push: (...args) => this.mock.methods[`$push(uint256,uint${256 - length},uint${length})`](0, ...args), - lowerLookup: (...args) => this.mock.methods[`$lowerLookup(uint256,uint${256 - length})`](0, ...args), - upperLookup: (...args) => this.mock.methods[`$upperLookup(uint256,uint${256 - length})`](0, ...args), + const fixture = async () => { + const mock = await ethers.deployContract('$Checkpoints'); + const methods = { + at: (...args) => mock.getFunction(`$at_Checkpoints_Trace${length}`)(0, ...args), + latest: (...args) => mock.getFunction(`$latest_Checkpoints_Trace${length}`)(0, ...args), + latestCheckpoint: (...args) => mock.getFunction(`$latestCheckpoint_Checkpoints_Trace${length}`)(0, ...args), + length: (...args) => mock.getFunction(`$length_Checkpoints_Trace${length}`)(0, ...args), + push: (...args) => mock.getFunction(`$push(uint256,uint${256 - length},uint${length})`)(0, ...args), + lowerLookup: (...args) => mock.getFunction(`$lowerLookup(uint256,uint${256 - length})`)(0, ...args), + upperLookup: (...args) => mock.getFunction(`$upperLookup(uint256,uint${256 - length})`)(0, ...args), upperLookupRecent: (...args) => - this.mock.methods[`$upperLookupRecent(uint256,uint${256 - length})`](0, ...args), + mock.getFunction(`$upperLookupRecent(uint256,uint${256 - length})`)(0, ...args), }; + + return { mock, methods }; + }; + + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); }); describe('without checkpoints', function () { it('at zero reverts', async function () { // Reverts with array out of bound access, which is unspecified - await expectRevert.unspecified(this.methods.at(0)); + await expect(this.methods.at(0)).to.be.reverted; }); it('returns zero as latest value', async function () { - expect(await this.methods.latest()).to.be.bignumber.equal('0'); + expect(await this.methods.latest()).to.equal(0n); const ckpt = await this.methods.latestCheckpoint(); - expect(ckpt[0]).to.be.equal(false); - expect(ckpt[1]).to.be.bignumber.equal('0'); - expect(ckpt[2]).to.be.bignumber.equal('0'); + expect(ckpt[0]).to.be.false; + expect(ckpt[1]).to.equal(0n); + expect(ckpt[2]).to.equal(0n); }); it('lookup returns 0', async function () { - expect(await this.methods.lowerLookup(0)).to.be.bignumber.equal('0'); - expect(await this.methods.upperLookup(0)).to.be.bignumber.equal('0'); - expect(await this.methods.upperLookupRecent(0)).to.be.bignumber.equal('0'); + expect(await this.methods.lowerLookup(0)).to.equal(0n); + expect(await this.methods.upperLookup(0)).to.equal(0n); + expect(await this.methods.upperLookupRecent(0)).to.equal(0n); }); }); describe('with checkpoints', function () { beforeEach('pushing checkpoints', async function () { this.checkpoints = [ - { key: '2', value: '17' }, - { key: '3', value: '42' }, - { key: '5', value: '101' }, - { key: '7', value: '23' }, - { key: '11', value: '99' }, + { key: 2n, value: 17n }, + { key: 3n, value: 42n }, + { key: 5n, value: 101n }, + { key: 7n, value: 23n }, + { key: 11n, value: 99n }, ]; for (const { key, value } of this.checkpoints) { await this.methods.push(key, value); @@ -75,70 +69,66 @@ contract('Checkpoints', function () { it('at keys', async function () { for (const [index, { key, value }] of this.checkpoints.entries()) { const at = await this.methods.at(index); - expect(at._value).to.be.bignumber.equal(value); - expect(at._key).to.be.bignumber.equal(key); + expect(at._value).to.equal(value); + expect(at._key).to.equal(key); } }); it('length', async function () { - expect(await this.methods.length()).to.be.bignumber.equal(this.checkpoints.length.toString()); + expect(await this.methods.length()).to.equal(this.checkpoints.length); }); it('returns latest value', async function () { - expect(await this.methods.latest()).to.be.bignumber.equal(last(this.checkpoints).value); - - const ckpt = await this.methods.latestCheckpoint(); - expect(ckpt[0]).to.be.equal(true); - expect(ckpt[1]).to.be.bignumber.equal(last(this.checkpoints).key); - expect(ckpt[2]).to.be.bignumber.equal(last(this.checkpoints).value); + const latest = this.checkpoints.at(-1); + expect(await this.methods.latest()).to.equal(latest.value); + expect(await this.methods.latestCheckpoint()).to.have.ordered.members([true, latest.key, latest.value]); }); it('cannot push values in the past', async function () { - await expectRevertCustomError( - this.methods.push(last(this.checkpoints).key - 1, '0'), + await expect(this.methods.push(this.checkpoints.at(-1).key - 1n, 0n)).to.be.revertedWithCustomError( + this.mock, 'CheckpointUnorderedInsertion', - [], ); }); it('can update last value', async function () { - const newValue = '42'; + const newValue = 42n; // check length before the update - expect(await this.methods.length()).to.be.bignumber.equal(this.checkpoints.length.toString()); + expect(await this.methods.length()).to.equal(this.checkpoints.length); // update last key - await this.methods.push(last(this.checkpoints).key, newValue); - expect(await this.methods.latest()).to.be.bignumber.equal(newValue); + await this.methods.push(this.checkpoints.at(-1).key, newValue); + expect(await this.methods.latest()).to.equal(newValue); // check that length did not change - expect(await this.methods.length()).to.be.bignumber.equal(this.checkpoints.length.toString()); + expect(await this.methods.length()).to.equal(this.checkpoints.length); }); it('lower lookup', async function () { for (let i = 0; i < 14; ++i) { - const value = first(this.checkpoints.filter(x => i <= x.key))?.value || '0'; + const value = this.checkpoints.find(x => i <= x.key)?.value || 0n; - expect(await this.methods.lowerLookup(i)).to.be.bignumber.equal(value); + expect(await this.methods.lowerLookup(i)).to.equal(value); } }); it('upper lookup & upperLookupRecent', async function () { for (let i = 0; i < 14; ++i) { - const value = last(this.checkpoints.filter(x => i >= x.key))?.value || '0'; + const value = last(this.checkpoints.filter(x => i >= x.key))?.value || 0n; - expect(await this.methods.upperLookup(i)).to.be.bignumber.equal(value); - expect(await this.methods.upperLookupRecent(i)).to.be.bignumber.equal(value); + expect(await this.methods.upperLookup(i)).to.equal(value); + expect(await this.methods.upperLookupRecent(i)).to.equal(value); } }); it('upperLookupRecent with more than 5 checkpoints', async function () { const moreCheckpoints = [ - { key: '12', value: '22' }, - { key: '13', value: '131' }, - { key: '17', value: '45' }, - { key: '19', value: '31452' }, - { key: '21', value: '0' }, + { key: 12n, value: 22n }, + { key: 13n, value: 131n }, + { key: 17n, value: 45n }, + { key: 19n, value: 31452n }, + { key: 21n, value: 0n }, ]; const allCheckpoints = [].concat(this.checkpoints, moreCheckpoints); @@ -147,9 +137,9 @@ contract('Checkpoints', function () { } for (let i = 0; i < 25; ++i) { - const value = last(allCheckpoints.filter(x => i >= x.key))?.value || '0'; - expect(await this.methods.upperLookup(i)).to.be.bignumber.equal(value); - expect(await this.methods.upperLookupRecent(i)).to.be.bignumber.equal(value); + const value = last(allCheckpoints.filter(x => i >= x.key))?.value || 0n; + expect(await this.methods.upperLookup(i)).to.equal(value); + expect(await this.methods.upperLookupRecent(i)).to.equal(value); } }); }); diff --git a/test/utils/structs/DoubleEndedQueue.test.js b/test/utils/structs/DoubleEndedQueue.test.js index cbf37d76b79..92d9f530c1e 100644 --- a/test/utils/structs/DoubleEndedQueue.test.js +++ b/test/utils/structs/DoubleEndedQueue.test.js @@ -1,99 +1,105 @@ -const { expectEvent } = require('@openzeppelin/test-helpers'); -const { expectRevertCustomError } = require('../../helpers/customError'); - -const DoubleEndedQueue = artifacts.require('$DoubleEndedQueue'); - -/** Rebuild the content of the deque as a JS array. */ -const getContent = deque => - deque.$length(0).then(bn => - Promise.all( - Array(bn.toNumber()) - .fill() - .map((_, i) => deque.$at(0, i)), - ), - ); - -contract('DoubleEndedQueue', function () { - const bytesA = '0xdeadbeef'.padEnd(66, '0'); - const bytesB = '0x0123456789'.padEnd(66, '0'); - const bytesC = '0x42424242'.padEnd(66, '0'); - const bytesD = '0x171717'.padEnd(66, '0'); +const { expect } = require('chai'); +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +async function fixture() { + const mock = await ethers.deployContract('$DoubleEndedQueue'); + + /** Rebuild the content of the deque as a JS array. */ + const getContent = () => + mock.$length(0).then(length => + Promise.all( + Array(Number(length)) + .fill() + .map((_, i) => mock.$at(0, i)), + ), + ); + + return { mock, getContent }; +} + +describe('DoubleEndedQueue', function () { + const coder = ethers.AbiCoder.defaultAbiCoder(); + const bytesA = coder.encode(['uint256'], [0xdeadbeef]); + const bytesB = coder.encode(['uint256'], [0x0123456789]); + const bytesC = coder.encode(['uint256'], [0x42424242]); + const bytesD = coder.encode(['uint256'], [0x171717]); beforeEach(async function () { - this.deque = await DoubleEndedQueue.new(); + Object.assign(this, await loadFixture(fixture)); }); describe('when empty', function () { it('getters', async function () { - expect(await this.deque.$empty(0)).to.be.equal(true); - expect(await getContent(this.deque)).to.have.ordered.members([]); + expect(await this.mock.$empty(0)).to.be.true; + expect(await this.getContent()).to.have.ordered.members([]); }); it('reverts on accesses', async function () { - await expectRevertCustomError(this.deque.$popBack(0), 'QueueEmpty', []); - await expectRevertCustomError(this.deque.$popFront(0), 'QueueEmpty', []); - await expectRevertCustomError(this.deque.$back(0), 'QueueEmpty', []); - await expectRevertCustomError(this.deque.$front(0), 'QueueEmpty', []); + await expect(this.mock.$popBack(0)).to.be.revertedWithCustomError(this.mock, 'QueueEmpty'); + await expect(this.mock.$popFront(0)).to.be.revertedWithCustomError(this.mock, 'QueueEmpty'); + await expect(this.mock.$back(0)).to.be.revertedWithCustomError(this.mock, 'QueueEmpty'); + await expect(this.mock.$front(0)).to.be.revertedWithCustomError(this.mock, 'QueueEmpty'); }); }); describe('when not empty', function () { beforeEach(async function () { - await this.deque.$pushBack(0, bytesB); - await this.deque.$pushFront(0, bytesA); - await this.deque.$pushBack(0, bytesC); + await this.mock.$pushBack(0, bytesB); + await this.mock.$pushFront(0, bytesA); + await this.mock.$pushBack(0, bytesC); this.content = [bytesA, bytesB, bytesC]; }); it('getters', async function () { - expect(await this.deque.$empty(0)).to.be.equal(false); - expect(await this.deque.$length(0)).to.be.bignumber.equal(this.content.length.toString()); - expect(await this.deque.$front(0)).to.be.equal(this.content[0]); - expect(await this.deque.$back(0)).to.be.equal(this.content[this.content.length - 1]); - expect(await getContent(this.deque)).to.have.ordered.members(this.content); + expect(await this.mock.$empty(0)).to.be.false; + expect(await this.mock.$length(0)).to.equal(this.content.length); + expect(await this.mock.$front(0)).to.equal(this.content[0]); + expect(await this.mock.$back(0)).to.equal(this.content[this.content.length - 1]); + expect(await this.getContent()).to.have.ordered.members(this.content); }); it('out of bounds access', async function () { - await expectRevertCustomError(this.deque.$at(0, this.content.length), 'QueueOutOfBounds', []); + await expect(this.mock.$at(0, this.content.length)).to.be.revertedWithCustomError(this.mock, 'QueueOutOfBounds'); }); describe('push', function () { it('front', async function () { - await this.deque.$pushFront(0, bytesD); + await this.mock.$pushFront(0, bytesD); this.content.unshift(bytesD); // add element at the beginning - expect(await getContent(this.deque)).to.have.ordered.members(this.content); + expect(await this.getContent()).to.have.ordered.members(this.content); }); it('back', async function () { - await this.deque.$pushBack(0, bytesD); + await this.mock.$pushBack(0, bytesD); this.content.push(bytesD); // add element at the end - expect(await getContent(this.deque)).to.have.ordered.members(this.content); + expect(await this.getContent()).to.have.ordered.members(this.content); }); }); describe('pop', function () { it('front', async function () { const value = this.content.shift(); // remove first element - expectEvent(await this.deque.$popFront(0), 'return$popFront', { value }); + await expect(this.mock.$popFront(0)).to.emit(this.mock, 'return$popFront').withArgs(value); - expect(await getContent(this.deque)).to.have.ordered.members(this.content); + expect(await this.getContent()).to.have.ordered.members(this.content); }); it('back', async function () { const value = this.content.pop(); // remove last element - expectEvent(await this.deque.$popBack(0), 'return$popBack', { value }); + await expect(this.mock.$popBack(0)).to.emit(this.mock, 'return$popBack').withArgs(value); - expect(await getContent(this.deque)).to.have.ordered.members(this.content); + expect(await this.getContent()).to.have.ordered.members(this.content); }); }); it('clear', async function () { - await this.deque.$clear(0); + await this.mock.$clear(0); - expect(await this.deque.$empty(0)).to.be.equal(true); - expect(await getContent(this.deque)).to.have.ordered.members([]); + expect(await this.mock.$empty(0)).to.be.true; + expect(await this.getContent()).to.have.ordered.members([]); }); }); }); diff --git a/test/utils/structs/EnumerableMap.behavior.js b/test/utils/structs/EnumerableMap.behavior.js index 67b19e39a2c..fb967b34c93 100644 --- a/test/utils/structs/EnumerableMap.behavior.js +++ b/test/utils/structs/EnumerableMap.behavior.js @@ -1,173 +1,146 @@ -const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { ethers } = require('hardhat'); -const zip = require('lodash.zip'); -const { expectRevertCustomError } = require('../../helpers/customError'); +const zip = (array1, array2) => array1.map((item, index) => [item, array2[index]]); -function shouldBehaveLikeMap(keys, values, zeroValue, methods, events) { - const [keyA, keyB, keyC] = keys; - const [valueA, valueB, valueC] = values; - - async function expectMembersMatch(map, keys, values) { +function shouldBehaveLikeMap(zeroValue, keyType, events) { + async function expectMembersMatch(methods, keys, values) { expect(keys.length).to.equal(values.length); + expect(await methods.length()).to.equal(keys.length); + expect([...(await methods.keys())]).to.have.members(keys); + + for (const [key, value] of zip(keys, values)) { + expect(await methods.contains(key)).to.be.true; + expect(await methods.get(key)).to.equal(value); + } - await Promise.all(keys.map(async key => expect(await methods.contains(map, key)).to.equal(true))); - - expect(await methods.length(map)).to.bignumber.equal(keys.length.toString()); - - expect((await Promise.all(keys.map(key => methods.get(map, key)))).map(k => k.toString())).to.have.same.members( - values.map(value => value.toString()), - ); - - // To compare key-value pairs, we zip keys and values, and convert BNs to - // strings to workaround Chai limitations when dealing with nested arrays - expect( - await Promise.all( - [...Array(keys.length).keys()].map(async index => { - const entry = await methods.at(map, index); - return [entry[0].toString(), entry[1].toString()]; - }), - ), - ).to.have.same.deep.members( - zip( - keys.map(k => k.toString()), - values.map(v => v.toString()), - ), - ); - - // This also checks that both arrays have the same length - expect((await methods.keys(map)).map(k => k.toString())).to.have.same.members(keys.map(key => key.toString())); + expect(await Promise.all(keys.map((_, index) => methods.at(index)))).to.have.deep.members(zip(keys, values)); } it('starts empty', async function () { - expect(await methods.contains(this.map, keyA)).to.equal(false); + expect(await this.methods.contains(this.keyA)).to.be.false; - await expectMembersMatch(this.map, [], []); + await expectMembersMatch(this.methods, [], []); }); describe('set', function () { it('adds a key', async function () { - const receipt = await methods.set(this.map, keyA, valueA); - expectEvent(receipt, events.setReturn, { ret0: true }); + await expect(this.methods.set(this.keyA, this.valueA)).to.emit(this.mock, events.setReturn).withArgs(true); - await expectMembersMatch(this.map, [keyA], [valueA]); + await expectMembersMatch(this.methods, [this.keyA], [this.valueA]); }); it('adds several keys', async function () { - await methods.set(this.map, keyA, valueA); - await methods.set(this.map, keyB, valueB); + await this.methods.set(this.keyA, this.valueA); + await this.methods.set(this.keyB, this.valueB); - await expectMembersMatch(this.map, [keyA, keyB], [valueA, valueB]); - expect(await methods.contains(this.map, keyC)).to.equal(false); + await expectMembersMatch(this.methods, [this.keyA, this.keyB], [this.valueA, this.valueB]); + expect(await this.methods.contains(this.keyC)).to.be.false; }); it('returns false when adding keys already in the set', async function () { - await methods.set(this.map, keyA, valueA); + await this.methods.set(this.keyA, this.valueA); - const receipt = await methods.set(this.map, keyA, valueA); - expectEvent(receipt, events.setReturn, { ret0: false }); + await expect(this.methods.set(this.keyA, this.valueA)).to.emit(this.mock, events.setReturn).withArgs(false); - await expectMembersMatch(this.map, [keyA], [valueA]); + await expectMembersMatch(this.methods, [this.keyA], [this.valueA]); }); it('updates values for keys already in the set', async function () { - await methods.set(this.map, keyA, valueA); - await methods.set(this.map, keyA, valueB); + await this.methods.set(this.keyA, this.valueA); + await this.methods.set(this.keyA, this.valueB); - await expectMembersMatch(this.map, [keyA], [valueB]); + await expectMembersMatch(this.methods, [this.keyA], [this.valueB]); }); }); describe('remove', function () { it('removes added keys', async function () { - await methods.set(this.map, keyA, valueA); + await this.methods.set(this.keyA, this.valueA); - const receipt = await methods.remove(this.map, keyA); - expectEvent(receipt, events.removeReturn, { ret0: true }); + await expect(this.methods.remove(this.keyA)).to.emit(this.mock, events.removeReturn).withArgs(true); - expect(await methods.contains(this.map, keyA)).to.equal(false); - await expectMembersMatch(this.map, [], []); + expect(await this.methods.contains(this.keyA)).to.be.false; + await expectMembersMatch(this.methods, [], []); }); it('returns false when removing keys not in the set', async function () { - const receipt = await methods.remove(this.map, keyA); - expectEvent(receipt, events.removeReturn, { ret0: false }); + await expect(await this.methods.remove(this.keyA)) + .to.emit(this.mock, events.removeReturn) + .withArgs(false); - expect(await methods.contains(this.map, keyA)).to.equal(false); + expect(await this.methods.contains(this.keyA)).to.be.false; }); it('adds and removes multiple keys', async function () { // [] - await methods.set(this.map, keyA, valueA); - await methods.set(this.map, keyC, valueC); + await this.methods.set(this.keyA, this.valueA); + await this.methods.set(this.keyC, this.valueC); // [A, C] - await methods.remove(this.map, keyA); - await methods.remove(this.map, keyB); + await this.methods.remove(this.keyA); + await this.methods.remove(this.keyB); // [C] - await methods.set(this.map, keyB, valueB); + await this.methods.set(this.keyB, this.valueB); // [C, B] - await methods.set(this.map, keyA, valueA); - await methods.remove(this.map, keyC); + await this.methods.set(this.keyA, this.valueA); + await this.methods.remove(this.keyC); // [A, B] - await methods.set(this.map, keyA, valueA); - await methods.set(this.map, keyB, valueB); + await this.methods.set(this.keyA, this.valueA); + await this.methods.set(this.keyB, this.valueB); // [A, B] - await methods.set(this.map, keyC, valueC); - await methods.remove(this.map, keyA); + await this.methods.set(this.keyC, this.valueC); + await this.methods.remove(this.keyA); // [B, C] - await methods.set(this.map, keyA, valueA); - await methods.remove(this.map, keyB); + await this.methods.set(this.keyA, this.valueA); + await this.methods.remove(this.keyB); // [A, C] - await expectMembersMatch(this.map, [keyA, keyC], [valueA, valueC]); + await expectMembersMatch(this.methods, [this.keyA, this.keyC], [this.valueA, this.valueC]); - expect(await methods.contains(this.map, keyA)).to.equal(true); - expect(await methods.contains(this.map, keyB)).to.equal(false); - expect(await methods.contains(this.map, keyC)).to.equal(true); + expect(await this.methods.contains(this.keyA)).to.be.true; + expect(await this.methods.contains(this.keyB)).to.be.false; + expect(await this.methods.contains(this.keyC)).to.be.true; }); }); describe('read', function () { beforeEach(async function () { - await methods.set(this.map, keyA, valueA); + await this.methods.set(this.keyA, this.valueA); }); describe('get', function () { it('existing value', async function () { - expect(await methods.get(this.map, keyA).then(r => r.toString())).to.be.equal(valueA.toString()); + expect(await this.methods.get(this.keyA)).to.be.equal(this.valueA); }); + it('missing value', async function () { - const key = web3.utils.toHex(keyB); - await expectRevertCustomError(methods.get(this.map, keyB), 'EnumerableMapNonexistentKey', [ - key.length == 66 ? key : web3.utils.padLeft(key, 64, '0'), - ]); + await expect(this.methods.get(this.keyB)) + .to.be.revertedWithCustomError(this.mock, 'EnumerableMapNonexistentKey') + .withArgs(ethers.AbiCoder.defaultAbiCoder().encode([keyType], [this.keyB])); }); }); describe('tryGet', function () { it('existing value', async function () { - const result = await methods.tryGet(this.map, keyA); - expect(result['0']).to.be.equal(true); - expect(result['1'].toString()).to.be.equal(valueA.toString()); + expect(await this.methods.tryGet(this.keyA)).to.have.ordered.members([true, this.valueA]); }); + it('missing value', async function () { - const result = await methods.tryGet(this.map, keyB); - expect(result['0']).to.be.equal(false); - expect(result['1'].toString()).to.be.equal(zeroValue.toString()); + expect(await this.methods.tryGet(this.keyB)).to.have.ordered.members([false, zeroValue]); }); }); }); diff --git a/test/utils/structs/EnumerableMap.test.js b/test/utils/structs/EnumerableMap.test.js index 545e12a4f33..accdb52fabc 100644 --- a/test/utils/structs/EnumerableMap.test.js +++ b/test/utils/structs/EnumerableMap.test.js @@ -1,149 +1,145 @@ -const { BN, constants } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { mapValues } = require('../../helpers/iterate'); - -const EnumerableMap = artifacts.require('$EnumerableMap'); +const { randomArray, generators } = require('../../helpers/random'); const { shouldBehaveLikeMap } = require('./EnumerableMap.behavior'); -const getMethods = ms => { +const getMethods = (mock, fnSigs) => { return mapValues( - ms, - m => - (self, ...args) => - self.methods[m](0, ...args), + fnSigs, + fnSig => + (...args) => + mock.getFunction(fnSig)(0, ...args), ); }; -// Get the name of the library. In the transpiled code it will be EnumerableMapUpgradeable. -const library = EnumerableMap._json.contractName.replace(/^\$/, ''); +describe('EnumerableMap', function () { + // UintToAddressMap + describe('UintToAddressMap', function () { + const fixture = async () => { + const mock = await ethers.deployContract('$EnumerableMap'); -contract('EnumerableMap', function (accounts) { - const [accountA, accountB, accountC] = accounts; + const [keyA, keyB, keyC] = randomArray(generators.uint256); + const [valueA, valueB, valueC] = randomArray(generators.address); - const keyA = new BN('7891'); - const keyB = new BN('451'); - const keyC = new BN('9592328'); + const methods = getMethods(mock, { + set: '$set(uint256,uint256,address)', + get: '$get_EnumerableMap_UintToAddressMap(uint256,uint256)', + tryGet: '$tryGet_EnumerableMap_UintToAddressMap(uint256,uint256)', + remove: '$remove_EnumerableMap_UintToAddressMap(uint256,uint256)', + length: '$length_EnumerableMap_UintToAddressMap(uint256)', + at: '$at_EnumerableMap_UintToAddressMap(uint256,uint256)', + contains: '$contains_EnumerableMap_UintToAddressMap(uint256,uint256)', + keys: '$keys_EnumerableMap_UintToAddressMap(uint256)', + }); - const bytesA = '0xdeadbeef'.padEnd(66, '0'); - const bytesB = '0x0123456789'.padEnd(66, '0'); - const bytesC = '0x42424242'.padEnd(66, '0'); + return { mock, keyA, keyB, keyC, valueA, valueB, valueC, methods }; + }; - beforeEach(async function () { - this.map = await EnumerableMap.new(); - }); + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); - // AddressToUintMap - describe('AddressToUintMap', function () { - shouldBehaveLikeMap( - [accountA, accountB, accountC], - [keyA, keyB, keyC], - new BN('0'), - getMethods({ - set: '$set(uint256,address,uint256)', - get: '$get(uint256,address)', - tryGet: '$tryGet(uint256,address)', - remove: '$remove(uint256,address)', - length: `$length_${library}_AddressToUintMap(uint256)`, - at: `$at_${library}_AddressToUintMap(uint256,uint256)`, - contains: '$contains(uint256,address)', - keys: `$keys_${library}_AddressToUintMap(uint256)`, - }), - { - setReturn: `return$set_${library}_AddressToUintMap_address_uint256`, - removeReturn: `return$remove_${library}_AddressToUintMap_address`, - }, - ); - }); - - // UintToAddressMap - describe('UintToAddressMap', function () { - shouldBehaveLikeMap( - [keyA, keyB, keyC], - [accountA, accountB, accountC], - constants.ZERO_ADDRESS, - getMethods({ - set: '$set(uint256,uint256,address)', - get: `$get_${library}_UintToAddressMap(uint256,uint256)`, - tryGet: `$tryGet_${library}_UintToAddressMap(uint256,uint256)`, - remove: `$remove_${library}_UintToAddressMap(uint256,uint256)`, - length: `$length_${library}_UintToAddressMap(uint256)`, - at: `$at_${library}_UintToAddressMap(uint256,uint256)`, - contains: `$contains_${library}_UintToAddressMap(uint256,uint256)`, - keys: `$keys_${library}_UintToAddressMap(uint256)`, - }), - { - setReturn: `return$set_${library}_UintToAddressMap_uint256_address`, - removeReturn: `return$remove_${library}_UintToAddressMap_uint256`, - }, - ); + shouldBehaveLikeMap(ethers.ZeroAddress, 'uint256', { + setReturn: 'return$set_EnumerableMap_UintToAddressMap_uint256_address', + removeReturn: 'return$remove_EnumerableMap_UintToAddressMap_uint256', + }); }); // Bytes32ToBytes32Map describe('Bytes32ToBytes32Map', function () { - shouldBehaveLikeMap( - [keyA, keyB, keyC].map(k => '0x' + k.toString(16).padEnd(64, '0')), - [bytesA, bytesB, bytesC], - constants.ZERO_BYTES32, - getMethods({ + const fixture = async () => { + const mock = await ethers.deployContract('$EnumerableMap'); + + const [keyA, keyB, keyC] = randomArray(generators.bytes32); + const [valueA, valueB, valueC] = randomArray(generators.bytes32); + + const methods = getMethods(mock, { set: '$set(uint256,bytes32,bytes32)', - get: `$get_${library}_Bytes32ToBytes32Map(uint256,bytes32)`, - tryGet: `$tryGet_${library}_Bytes32ToBytes32Map(uint256,bytes32)`, - remove: `$remove_${library}_Bytes32ToBytes32Map(uint256,bytes32)`, - length: `$length_${library}_Bytes32ToBytes32Map(uint256)`, - at: `$at_${library}_Bytes32ToBytes32Map(uint256,uint256)`, - contains: `$contains_${library}_Bytes32ToBytes32Map(uint256,bytes32)`, - keys: `$keys_${library}_Bytes32ToBytes32Map(uint256)`, - }), - { - setReturn: `return$set_${library}_Bytes32ToBytes32Map_bytes32_bytes32`, - removeReturn: `return$remove_${library}_Bytes32ToBytes32Map_bytes32`, - }, - ); + get: '$get_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32)', + tryGet: '$tryGet_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32)', + remove: '$remove_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32)', + length: '$length_EnumerableMap_Bytes32ToBytes32Map(uint256)', + at: '$at_EnumerableMap_Bytes32ToBytes32Map(uint256,uint256)', + contains: '$contains_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32)', + keys: '$keys_EnumerableMap_Bytes32ToBytes32Map(uint256)', + }); + + return { mock, keyA, keyB, keyC, valueA, valueB, valueC, methods }; + }; + + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeMap(ethers.ZeroHash, 'bytes32', { + setReturn: 'return$set_EnumerableMap_Bytes32ToBytes32Map_bytes32_bytes32', + removeReturn: 'return$remove_EnumerableMap_Bytes32ToBytes32Map_bytes32', + }); }); // UintToUintMap describe('UintToUintMap', function () { - shouldBehaveLikeMap( - [keyA, keyB, keyC], - [keyA, keyB, keyC].map(k => k.add(new BN('1332'))), - new BN('0'), - getMethods({ + const fixture = async () => { + const mock = await ethers.deployContract('$EnumerableMap'); + + const [keyA, keyB, keyC] = randomArray(generators.uint256); + const [valueA, valueB, valueC] = randomArray(generators.uint256); + + const methods = getMethods(mock, { set: '$set(uint256,uint256,uint256)', - get: `$get_${library}_UintToUintMap(uint256,uint256)`, - tryGet: `$tryGet_${library}_UintToUintMap(uint256,uint256)`, - remove: `$remove_${library}_UintToUintMap(uint256,uint256)`, - length: `$length_${library}_UintToUintMap(uint256)`, - at: `$at_${library}_UintToUintMap(uint256,uint256)`, - contains: `$contains_${library}_UintToUintMap(uint256,uint256)`, - keys: `$keys_${library}_UintToUintMap(uint256)`, - }), - { - setReturn: `return$set_${library}_UintToUintMap_uint256_uint256`, - removeReturn: `return$remove_${library}_UintToUintMap_uint256`, - }, - ); + get: '$get_EnumerableMap_UintToUintMap(uint256,uint256)', + tryGet: '$tryGet_EnumerableMap_UintToUintMap(uint256,uint256)', + remove: '$remove_EnumerableMap_UintToUintMap(uint256,uint256)', + length: '$length_EnumerableMap_UintToUintMap(uint256)', + at: '$at_EnumerableMap_UintToUintMap(uint256,uint256)', + contains: '$contains_EnumerableMap_UintToUintMap(uint256,uint256)', + keys: '$keys_EnumerableMap_UintToUintMap(uint256)', + }); + + return { mock, keyA, keyB, keyC, valueA, valueB, valueC, methods }; + }; + + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeMap(0n, 'uint256', { + setReturn: 'return$set_EnumerableMap_UintToUintMap_uint256_uint256', + removeReturn: 'return$remove_EnumerableMap_UintToUintMap_uint256', + }); }); // Bytes32ToUintMap describe('Bytes32ToUintMap', function () { - shouldBehaveLikeMap( - [bytesA, bytesB, bytesC], - [keyA, keyB, keyC], - new BN('0'), - getMethods({ + const fixture = async () => { + const mock = await ethers.deployContract('$EnumerableMap'); + + const [keyA, keyB, keyC] = randomArray(generators.bytes32); + const [valueA, valueB, valueC] = randomArray(generators.uint256); + + const methods = getMethods(mock, { set: '$set(uint256,bytes32,uint256)', - get: `$get_${library}_Bytes32ToUintMap(uint256,bytes32)`, - tryGet: `$tryGet_${library}_Bytes32ToUintMap(uint256,bytes32)`, - remove: `$remove_${library}_Bytes32ToUintMap(uint256,bytes32)`, - length: `$length_${library}_Bytes32ToUintMap(uint256)`, - at: `$at_${library}_Bytes32ToUintMap(uint256,uint256)`, - contains: `$contains_${library}_Bytes32ToUintMap(uint256,bytes32)`, - keys: `$keys_${library}_Bytes32ToUintMap(uint256)`, - }), - { - setReturn: `return$set_${library}_Bytes32ToUintMap_bytes32_uint256`, - removeReturn: `return$remove_${library}_Bytes32ToUintMap_bytes32`, - }, - ); + get: '$get_EnumerableMap_Bytes32ToUintMap(uint256,bytes32)', + tryGet: '$tryGet_EnumerableMap_Bytes32ToUintMap(uint256,bytes32)', + remove: '$remove_EnumerableMap_Bytes32ToUintMap(uint256,bytes32)', + length: '$length_EnumerableMap_Bytes32ToUintMap(uint256)', + at: '$at_EnumerableMap_Bytes32ToUintMap(uint256,uint256)', + contains: '$contains_EnumerableMap_Bytes32ToUintMap(uint256,bytes32)', + keys: '$keys_EnumerableMap_Bytes32ToUintMap(uint256)', + }); + + return { mock, keyA, keyB, keyC, valueA, valueB, valueC, methods }; + }; + + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeMap(0n, 'bytes32', { + setReturn: 'return$set_EnumerableMap_Bytes32ToUintMap_bytes32_uint256', + removeReturn: 'return$remove_EnumerableMap_Bytes32ToUintMap_bytes32', + }); }); }); diff --git a/test/utils/structs/EnumerableSet.behavior.js b/test/utils/structs/EnumerableSet.behavior.js index f80eb816984..5b9e067e8de 100644 --- a/test/utils/structs/EnumerableSet.behavior.js +++ b/test/utils/structs/EnumerableSet.behavior.js @@ -1,125 +1,106 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); -function shouldBehaveLikeSet(values, methods, events) { - const [valueA, valueB, valueC] = values; +function shouldBehaveLikeSet(events) { + async function expectMembersMatch(methods, values) { + expect(await methods.length()).to.equal(values.length); + for (const value of values) expect(await methods.contains(value)).to.be.true; - async function expectMembersMatch(set, values) { - const contains = await Promise.all(values.map(value => methods.contains(set, value))); - expect(contains.every(Boolean)).to.be.equal(true); - - const length = await methods.length(set); - expect(length).to.bignumber.equal(values.length.toString()); - - // To compare values we convert to strings to workaround Chai - // limitations when dealing with nested arrays (required for BNs) - const indexedValues = await Promise.all( - Array(values.length) - .fill() - .map((_, index) => methods.at(set, index)), - ); - expect(indexedValues.map(v => v.toString())).to.have.same.members(values.map(v => v.toString())); - - const returnedValues = await methods.values(set); - expect(returnedValues.map(v => v.toString())).to.have.same.members(values.map(v => v.toString())); + expect(await Promise.all(values.map((_, index) => methods.at(index)))).to.have.deep.members(values); + expect([...(await methods.values())]).to.have.deep.members(values); } it('starts empty', async function () { - expect(await methods.contains(this.set, valueA)).to.equal(false); + expect(await this.methods.contains(this.valueA)).to.be.false; - await expectMembersMatch(this.set, []); + await expectMembersMatch(this.methods, []); }); describe('add', function () { it('adds a value', async function () { - const receipt = await methods.add(this.set, valueA); - expectEvent(receipt, events.addReturn, { ret0: true }); + await expect(this.methods.add(this.valueA)).to.emit(this.mock, events.addReturn).withArgs(true); - await expectMembersMatch(this.set, [valueA]); + await expectMembersMatch(this.methods, [this.valueA]); }); it('adds several values', async function () { - await methods.add(this.set, valueA); - await methods.add(this.set, valueB); + await this.methods.add(this.valueA); + await this.methods.add(this.valueB); - await expectMembersMatch(this.set, [valueA, valueB]); - expect(await methods.contains(this.set, valueC)).to.equal(false); + await expectMembersMatch(this.methods, [this.valueA, this.valueB]); + expect(await this.methods.contains(this.valueC)).to.be.false; }); it('returns false when adding values already in the set', async function () { - await methods.add(this.set, valueA); + await this.methods.add(this.valueA); - const receipt = await methods.add(this.set, valueA); - expectEvent(receipt, events.addReturn, { ret0: false }); + await expect(this.methods.add(this.valueA)).to.emit(this.mock, events.addReturn).withArgs(false); - await expectMembersMatch(this.set, [valueA]); + await expectMembersMatch(this.methods, [this.valueA]); }); }); describe('at', function () { it('reverts when retrieving non-existent elements', async function () { - await expectRevert.unspecified(methods.at(this.set, 0)); + await expect(this.methods.at(0)).to.be.reverted; }); }); describe('remove', function () { it('removes added values', async function () { - await methods.add(this.set, valueA); + await this.methods.add(this.valueA); - const receipt = await methods.remove(this.set, valueA); - expectEvent(receipt, events.removeReturn, { ret0: true }); + await expect(this.methods.remove(this.valueA)).to.emit(this.mock, events.removeReturn).withArgs(true); - expect(await methods.contains(this.set, valueA)).to.equal(false); - await expectMembersMatch(this.set, []); + expect(await this.methods.contains(this.valueA)).to.be.false; + await expectMembersMatch(this.methods, []); }); it('returns false when removing values not in the set', async function () { - const receipt = await methods.remove(this.set, valueA); - expectEvent(receipt, events.removeReturn, { ret0: false }); + await expect(this.methods.remove(this.valueA)).to.emit(this.mock, events.removeReturn).withArgs(false); - expect(await methods.contains(this.set, valueA)).to.equal(false); + expect(await this.methods.contains(this.valueA)).to.be.false; }); it('adds and removes multiple values', async function () { // [] - await methods.add(this.set, valueA); - await methods.add(this.set, valueC); + await this.methods.add(this.valueA); + await this.methods.add(this.valueC); // [A, C] - await methods.remove(this.set, valueA); - await methods.remove(this.set, valueB); + await this.methods.remove(this.valueA); + await this.methods.remove(this.valueB); // [C] - await methods.add(this.set, valueB); + await this.methods.add(this.valueB); // [C, B] - await methods.add(this.set, valueA); - await methods.remove(this.set, valueC); + await this.methods.add(this.valueA); + await this.methods.remove(this.valueC); // [A, B] - await methods.add(this.set, valueA); - await methods.add(this.set, valueB); + await this.methods.add(this.valueA); + await this.methods.add(this.valueB); // [A, B] - await methods.add(this.set, valueC); - await methods.remove(this.set, valueA); + await this.methods.add(this.valueC); + await this.methods.remove(this.valueA); // [B, C] - await methods.add(this.set, valueA); - await methods.remove(this.set, valueB); + await this.methods.add(this.valueA); + await this.methods.remove(this.valueB); // [A, C] - await expectMembersMatch(this.set, [valueA, valueC]); + await expectMembersMatch(this.methods, [this.valueA, this.valueC]); - expect(await methods.contains(this.set, valueB)).to.equal(false); + expect(await this.methods.contains(this.valueB)).to.be.false; }); }); } diff --git a/test/utils/structs/EnumerableSet.test.js b/test/utils/structs/EnumerableSet.test.js index a1840257ba9..726ea0ee379 100644 --- a/test/utils/structs/EnumerableSet.test.js +++ b/test/utils/structs/EnumerableSet.test.js @@ -1,79 +1,104 @@ -const EnumerableSet = artifacts.require('$EnumerableSet'); +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { mapValues } = require('../../helpers/iterate'); +const { randomArray, generators } = require('../../helpers/random'); const { shouldBehaveLikeSet } = require('./EnumerableSet.behavior'); -const getMethods = ms => { +const getMethods = (mock, fnSigs) => { return mapValues( - ms, - m => - (self, ...args) => - self.methods[m](0, ...args), + fnSigs, + fnSig => + (...args) => + mock.getFunction(fnSig)(0, ...args), ); }; -// Get the name of the library. In the transpiled code it will be EnumerableSetUpgradeable. -const library = EnumerableSet._json.contractName.replace(/^\$/, ''); - -contract('EnumerableSet', function (accounts) { - beforeEach(async function () { - this.set = await EnumerableSet.new(); - }); - +describe('EnumerableSet', function () { // Bytes32Set describe('EnumerableBytes32Set', function () { - shouldBehaveLikeSet( - ['0xdeadbeef', '0x0123456789', '0x42424242'].map(e => e.padEnd(66, '0')), - getMethods({ + const fixture = async () => { + const mock = await ethers.deployContract('$EnumerableSet'); + + const [valueA, valueB, valueC] = randomArray(generators.bytes32); + + const methods = getMethods(mock, { add: '$add(uint256,bytes32)', remove: '$remove(uint256,bytes32)', contains: '$contains(uint256,bytes32)', - length: `$length_${library}_Bytes32Set(uint256)`, - at: `$at_${library}_Bytes32Set(uint256,uint256)`, - values: `$values_${library}_Bytes32Set(uint256)`, - }), - { - addReturn: `return$add_${library}_Bytes32Set_bytes32`, - removeReturn: `return$remove_${library}_Bytes32Set_bytes32`, - }, - ); + length: `$length_EnumerableSet_Bytes32Set(uint256)`, + at: `$at_EnumerableSet_Bytes32Set(uint256,uint256)`, + values: `$values_EnumerableSet_Bytes32Set(uint256)`, + }); + + return { mock, valueA, valueB, valueC, methods }; + }; + + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeSet({ + addReturn: `return$add_EnumerableSet_Bytes32Set_bytes32`, + removeReturn: `return$remove_EnumerableSet_Bytes32Set_bytes32`, + }); }); // AddressSet describe('EnumerableAddressSet', function () { - shouldBehaveLikeSet( - accounts, - getMethods({ + const fixture = async () => { + const mock = await ethers.deployContract('$EnumerableSet'); + + const [valueA, valueB, valueC] = randomArray(generators.address); + + const methods = getMethods(mock, { add: '$add(uint256,address)', remove: '$remove(uint256,address)', contains: '$contains(uint256,address)', - length: `$length_${library}_AddressSet(uint256)`, - at: `$at_${library}_AddressSet(uint256,uint256)`, - values: `$values_${library}_AddressSet(uint256)`, - }), - { - addReturn: `return$add_${library}_AddressSet_address`, - removeReturn: `return$remove_${library}_AddressSet_address`, - }, - ); + length: `$length_EnumerableSet_AddressSet(uint256)`, + at: `$at_EnumerableSet_AddressSet(uint256,uint256)`, + values: `$values_EnumerableSet_AddressSet(uint256)`, + }); + + return { mock, valueA, valueB, valueC, methods }; + }; + + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeSet({ + addReturn: `return$add_EnumerableSet_AddressSet_address`, + removeReturn: `return$remove_EnumerableSet_AddressSet_address`, + }); }); // UintSet describe('EnumerableUintSet', function () { - shouldBehaveLikeSet( - [1234, 5678, 9101112].map(e => web3.utils.toBN(e)), - getMethods({ + const fixture = async () => { + const mock = await ethers.deployContract('$EnumerableSet'); + + const [valueA, valueB, valueC] = randomArray(generators.uint256); + + const methods = getMethods(mock, { add: '$add(uint256,uint256)', remove: '$remove(uint256,uint256)', contains: '$contains(uint256,uint256)', - length: `$length_${library}_UintSet(uint256)`, - at: `$at_${library}_UintSet(uint256,uint256)`, - values: `$values_${library}_UintSet(uint256)`, - }), - { - addReturn: `return$add_${library}_UintSet_uint256`, - removeReturn: `return$remove_${library}_UintSet_uint256`, - }, - ); + length: `$length_EnumerableSet_UintSet(uint256)`, + at: `$at_EnumerableSet_UintSet(uint256,uint256)`, + values: `$values_EnumerableSet_UintSet(uint256)`, + }); + + return { mock, valueA, valueB, valueC, methods }; + }; + + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeSet({ + addReturn: `return$add_EnumerableSet_UintSet_uint256`, + removeReturn: `return$remove_EnumerableSet_UintSet_uint256`, + }); }); }); From e5fb718d4071b4be5e8dc980a3733616605c570a Mon Sep 17 00:00:00 2001 From: carter-ya Date: Thu, 23 Nov 2023 23:31:14 +0800 Subject: [PATCH 16/44] Optimized gas costs in `ceilDiv` (#4553) --- .changeset/angry-dodos-grow.md | 5 +++++ contracts/utils/math/Math.sol | 10 ++++++++-- test/utils/math/Math.t.sol | 9 +++++---- 3 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 .changeset/angry-dodos-grow.md diff --git a/.changeset/angry-dodos-grow.md b/.changeset/angry-dodos-grow.md new file mode 100644 index 00000000000..ab2b60104b8 --- /dev/null +++ b/.changeset/angry-dodos-grow.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Math`: Optimized gas cost of `ceilDiv` by using `unchecked`. diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index 9681524529b..86316cb2971 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -110,8 +110,14 @@ library Math { return a / b; } - // (a + b - 1) / b can overflow on addition, so we distribute. - return a == 0 ? 0 : (a - 1) / b + 1; + // The following calculation ensures accurate ceiling division without overflow. + // Since a is non-zero, (a - 1) / b will not overflow. + // The largest possible result occurs when (a - 1) / b is type(uint256).max, + // but the largest value we can obtain is type(uint256).max - 1, which happens + // when a = type(uint256).max and b = 1. + unchecked { + return a == 0 ? 0 : (a - 1) / b + 1; + } } /** diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index 0b497a858c7..a757833796f 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -16,10 +16,11 @@ contract MathTest is Test { if (result == 0) { assertEq(a, 0); } else { - uint256 maxdiv = UINT256_MAX / b; - bool overflow = maxdiv * b < a; - assertTrue(a > b * (result - 1)); - assertTrue(overflow ? result == maxdiv + 1 : a <= b * result); + uint256 expect = a / b; + if (expect * b < a) { + expect += 1; + } + assertEq(result, expect); } } From 330c39b66218de5fa4f0f62d4824ae3c710bb40f Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Thu, 23 Nov 2023 20:45:45 +0000 Subject: [PATCH 17/44] Implement revert tests for VestingWallet (#4733) Co-authored-by: ernestognw Co-authored-by: Hadrien Croubois --- test/finance/VestingWallet.behavior.js | 10 ++++ test/finance/VestingWallet.test.js | 78 +++++++++++++++++--------- 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/test/finance/VestingWallet.behavior.js b/test/finance/VestingWallet.behavior.js index c1e0f8013a5..d4863bc52d5 100644 --- a/test/finance/VestingWallet.behavior.js +++ b/test/finance/VestingWallet.behavior.js @@ -34,6 +34,16 @@ function shouldBehaveLikeVesting() { released = vested; } }); + + it('should revert on transaction failure', async function () { + const { args, error } = await this.setupFailure(); + + for (const timestamp of this.schedule) { + await time.forward.timestamp(timestamp); + + await expect(this.mock.release(...args)).to.be.revertedWithCustomError(...error); + } + }); } module.exports = { diff --git a/test/finance/VestingWallet.test.js b/test/finance/VestingWallet.test.js index e3739dd9071..fa60faea723 100644 --- a/test/finance/VestingWallet.test.js +++ b/test/finance/VestingWallet.test.js @@ -13,7 +13,54 @@ async function fixture() { const [sender, beneficiary] = await ethers.getSigners(); const mock = await ethers.deployContract('VestingWallet', [beneficiary, start, duration]); - return { mock, amount, duration, start, sender, beneficiary }; + + const token = await ethers.deployContract('$ERC20', ['Name', 'Symbol']); + await token.$_mint(mock, amount); + await sender.sendTransaction({ to: mock, value: amount }); + + const pausableToken = await ethers.deployContract('$ERC20Pausable', ['Name', 'Symbol']); + const beneficiaryMock = await ethers.deployContract('EtherReceiverMock'); + + const env = { + eth: { + checkRelease: async (tx, amount) => { + await expect(tx).to.emit(mock, 'EtherReleased').withArgs(amount); + await expect(tx).to.changeEtherBalances([mock, beneficiary], [-amount, amount]); + }, + setupFailure: async () => { + await beneficiaryMock.setAcceptEther(false); + await mock.connect(beneficiary).transferOwnership(beneficiaryMock); + return { args: [], error: [mock, 'FailedInnerCall'] }; + }, + releasedEvent: 'EtherReleased', + argsVerify: [], + args: [], + }, + token: { + checkRelease: async (tx, amount) => { + await expect(tx).to.emit(token, 'Transfer').withArgs(mock.target, beneficiary.address, amount); + await expect(tx).to.changeTokenBalances(token, [mock, beneficiary], [-amount, amount]); + }, + setupFailure: async () => { + await pausableToken.$_pause(); + return { + args: [ethers.Typed.address(pausableToken)], + error: [pausableToken, 'EnforcedPause'], + }; + }, + releasedEvent: 'ERC20Released', + argsVerify: [token.target], + args: [ethers.Typed.address(token.target)], + }, + }; + + const schedule = Array(64) + .fill() + .map((_, i) => (BigInt(i) * duration) / 60n + start); + + const vestingFn = timestamp => min(amount, (amount * (timestamp - start)) / duration); + + return { mock, duration, start, beneficiary, schedule, vestingFn, env }; } describe('VestingWallet', function () { @@ -35,23 +82,9 @@ describe('VestingWallet', function () { }); describe('vesting schedule', function () { - beforeEach(function () { - this.schedule = Array(64) - .fill() - .map((_, i) => (BigInt(i) * this.duration) / 60n + this.start); - this.vestingFn = timestamp => min(this.amount, (this.amount * (timestamp - this.start)) / this.duration); - }); - describe('Eth vesting', function () { beforeEach(async function () { - await this.sender.sendTransaction({ to: this.mock, value: this.amount }); - - this.getBalance = signer => ethers.provider.getBalance(signer); - this.checkRelease = (tx, amount) => expect(tx).to.changeEtherBalances([this.beneficiary], [amount]); - - this.releasedEvent = 'EtherReleased'; - this.args = []; - this.argsVerify = []; + Object.assign(this, this.env.eth); }); shouldBehaveLikeVesting(); @@ -59,18 +92,7 @@ describe('VestingWallet', function () { describe('ERC20 vesting', function () { beforeEach(async function () { - this.token = await ethers.deployContract('$ERC20', ['Name', 'Symbol']); - await this.token.$_mint(this.mock, this.amount); - - this.getBalance = account => this.token.balanceOf(account); - this.checkRelease = async (tx, amount) => { - await expect(tx).to.emit(this.token, 'Transfer').withArgs(this.mock.target, this.beneficiary.address, amount); - await expect(tx).to.changeTokenBalances(this.token, [this.mock, this.beneficiary], [-amount, amount]); - }; - - this.releasedEvent = 'ERC20Released'; - this.args = [ethers.Typed.address(this.token.target)]; - this.argsVerify = [this.token.target]; + Object.assign(this, this.env.token); }); shouldBehaveLikeVesting(); From 78d5708340b5ad1aed18c1e97e6d089b7c6f07fd Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Fri, 24 Nov 2023 01:32:30 +0000 Subject: [PATCH 18/44] Migrate utils to ethersjs v6 (#4736) Co-authored-by: Hadrien Croubois Co-authored-by: ernestognw --- ...{MulticallTest.sol => MulticallHelper.sol} | 2 +- contracts/mocks/StorageSlotMock.sol | 24 +- test/helpers/random.js | 1 + test/utils/Arrays.test.js | 211 ++++++++------- test/utils/Base64.test.js | 42 ++- test/utils/Create2.test.js | 122 +++++---- test/utils/Multicall.test.js | 95 +++---- test/utils/Nonces.test.js | 70 ++--- test/utils/Pausable.test.js | 68 ++--- test/utils/ReentrancyGuard.test.js | 37 +-- test/utils/ShortStrings.test.js | 63 +++-- test/utils/StorageSlot.test.js | 242 ++++-------------- test/utils/Strings.test.js | 112 ++++---- 13 files changed, 492 insertions(+), 597 deletions(-) rename contracts/mocks/{MulticallTest.sol => MulticallHelper.sol} (96%) diff --git a/contracts/mocks/MulticallTest.sol b/contracts/mocks/MulticallHelper.sol similarity index 96% rename from contracts/mocks/MulticallTest.sol rename to contracts/mocks/MulticallHelper.sol index 74be7d8b413..d70f3bf4ead 100644 --- a/contracts/mocks/MulticallTest.sol +++ b/contracts/mocks/MulticallHelper.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; import {ERC20MulticallMock} from "./token/ERC20MulticallMock.sol"; -contract MulticallTest { +contract MulticallHelper { function checkReturnValues( ERC20MulticallMock multicallToken, address[] calldata recipients, diff --git a/contracts/mocks/StorageSlotMock.sol b/contracts/mocks/StorageSlotMock.sol index dbdad7a2a8e..36f0f5af022 100644 --- a/contracts/mocks/StorageSlotMock.sol +++ b/contracts/mocks/StorageSlotMock.sol @@ -7,41 +7,41 @@ import {StorageSlot} from "../utils/StorageSlot.sol"; contract StorageSlotMock { using StorageSlot for *; - function setBoolean(bytes32 slot, bool value) public { + function setBooleanSlot(bytes32 slot, bool value) public { slot.getBooleanSlot().value = value; } - function setAddress(bytes32 slot, address value) public { + function setAddressSlot(bytes32 slot, address value) public { slot.getAddressSlot().value = value; } - function setBytes32(bytes32 slot, bytes32 value) public { + function setBytes32Slot(bytes32 slot, bytes32 value) public { slot.getBytes32Slot().value = value; } - function setUint256(bytes32 slot, uint256 value) public { + function setUint256Slot(bytes32 slot, uint256 value) public { slot.getUint256Slot().value = value; } - function getBoolean(bytes32 slot) public view returns (bool) { + function getBooleanSlot(bytes32 slot) public view returns (bool) { return slot.getBooleanSlot().value; } - function getAddress(bytes32 slot) public view returns (address) { + function getAddressSlot(bytes32 slot) public view returns (address) { return slot.getAddressSlot().value; } - function getBytes32(bytes32 slot) public view returns (bytes32) { + function getBytes32Slot(bytes32 slot) public view returns (bytes32) { return slot.getBytes32Slot().value; } - function getUint256(bytes32 slot) public view returns (uint256) { + function getUint256Slot(bytes32 slot) public view returns (uint256) { return slot.getUint256Slot().value; } mapping(uint256 key => string) public stringMap; - function setString(bytes32 slot, string calldata value) public { + function setStringSlot(bytes32 slot, string calldata value) public { slot.getStringSlot().value = value; } @@ -49,7 +49,7 @@ contract StorageSlotMock { stringMap[key].getStringSlot().value = value; } - function getString(bytes32 slot) public view returns (string memory) { + function getStringSlot(bytes32 slot) public view returns (string memory) { return slot.getStringSlot().value; } @@ -59,7 +59,7 @@ contract StorageSlotMock { mapping(uint256 key => bytes) public bytesMap; - function setBytes(bytes32 slot, bytes calldata value) public { + function setBytesSlot(bytes32 slot, bytes calldata value) public { slot.getBytesSlot().value = value; } @@ -67,7 +67,7 @@ contract StorageSlotMock { bytesMap[key].getBytesSlot().value = value; } - function getBytes(bytes32 slot) public view returns (bytes memory) { + function getBytesSlot(bytes32 slot) public view returns (bytes memory) { return slot.getBytesSlot().value; } diff --git a/test/helpers/random.js b/test/helpers/random.js index 883667fa073..c1d02f2614c 100644 --- a/test/helpers/random.js +++ b/test/helpers/random.js @@ -6,6 +6,7 @@ const generators = { address: () => ethers.Wallet.createRandom().address, bytes32: () => ethers.hexlify(ethers.randomBytes(32)), uint256: () => ethers.toBigInt(ethers.randomBytes(32)), + hexBytes: length => ethers.hexlify(ethers.randomBytes(length)), }; module.exports = { diff --git a/test/utils/Arrays.test.js b/test/utils/Arrays.test.js index d939d59bdf2..375b9422f4a 100644 --- a/test/utils/Arrays.test.js +++ b/test/utils/Arrays.test.js @@ -1,121 +1,120 @@ -require('@openzeppelin/test-helpers'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const AddressArraysMock = artifacts.require('AddressArraysMock'); -const Bytes32ArraysMock = artifacts.require('Bytes32ArraysMock'); -const Uint256ArraysMock = artifacts.require('Uint256ArraysMock'); - -contract('Arrays', function () { - describe('findUpperBound', function () { - context('Even number of elements', function () { - const EVEN_ELEMENTS_ARRAY = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; - - beforeEach(async function () { - this.arrays = await Uint256ArraysMock.new(EVEN_ELEMENTS_ARRAY); - }); - - it('returns correct index for the basic case', async function () { - expect(await this.arrays.findUpperBound(16)).to.be.bignumber.equal('5'); - }); - - it('returns 0 for the first element', async function () { - expect(await this.arrays.findUpperBound(11)).to.be.bignumber.equal('0'); - }); - - it('returns index of the last element', async function () { - expect(await this.arrays.findUpperBound(20)).to.be.bignumber.equal('9'); - }); - - it('returns first index after last element if searched value is over the upper boundary', async function () { - expect(await this.arrays.findUpperBound(32)).to.be.bignumber.equal('10'); - }); - - it('returns 0 for the element under the lower boundary', async function () { - expect(await this.arrays.findUpperBound(2)).to.be.bignumber.equal('0'); - }); - }); - - context('Odd number of elements', function () { - const ODD_ELEMENTS_ARRAY = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]; - - beforeEach(async function () { - this.arrays = await Uint256ArraysMock.new(ODD_ELEMENTS_ARRAY); - }); - - it('returns correct index for the basic case', async function () { - expect(await this.arrays.findUpperBound(16)).to.be.bignumber.equal('5'); - }); - - it('returns 0 for the first element', async function () { - expect(await this.arrays.findUpperBound(11)).to.be.bignumber.equal('0'); - }); - - it('returns index of the last element', async function () { - expect(await this.arrays.findUpperBound(21)).to.be.bignumber.equal('10'); - }); - - it('returns first index after last element if searched value is over the upper boundary', async function () { - expect(await this.arrays.findUpperBound(32)).to.be.bignumber.equal('11'); - }); +const { randomArray, generators } = require('../helpers/random'); - it('returns 0 for the element under the lower boundary', async function () { - expect(await this.arrays.findUpperBound(2)).to.be.bignumber.equal('0'); - }); - }); +// See https://en.cppreference.com/w/cpp/algorithm/ranges/lower_bound +const lowerBound = (array, value) => { + const i = array.findIndex(element => value <= element); + return i == -1 ? array.length : i; +}; - context('Array with gap', function () { - const WITH_GAP_ARRAY = [11, 12, 13, 14, 15, 20, 21, 22, 23, 24]; +// See https://en.cppreference.com/w/cpp/algorithm/upper_bound +// const upperBound = (array, value) => { +// const i = array.findIndex(element => value < element); +// return i == -1 ? array.length : i; +// }; - beforeEach(async function () { - this.arrays = await Uint256ArraysMock.new(WITH_GAP_ARRAY); - }); +const hasDuplicates = array => array.some((v, i) => array.indexOf(v) != i); - it('returns index of first element in next filled range', async function () { - expect(await this.arrays.findUpperBound(17)).to.be.bignumber.equal('5'); - }); - }); - - context('Empty array', function () { - beforeEach(async function () { - this.arrays = await Uint256ArraysMock.new([]); - }); - - it('always returns 0 for empty array', async function () { - expect(await this.arrays.findUpperBound(10)).to.be.bignumber.equal('0'); +describe('Arrays', function () { + describe('findUpperBound', function () { + for (const [title, { array, tests }] of Object.entries({ + 'Even number of elements': { + array: [11n, 12n, 13n, 14n, 15n, 16n, 17n, 18n, 19n, 20n], + tests: { + 'basic case': 16n, + 'first element': 11n, + 'last element': 20n, + 'searched value is over the upper boundary': 32n, + 'searched value is under the lower boundary': 2n, + }, + }, + 'Odd number of elements': { + array: [11n, 12n, 13n, 14n, 15n, 16n, 17n, 18n, 19n, 20n, 21n], + tests: { + 'basic case': 16n, + 'first element': 11n, + 'last element': 21n, + 'searched value is over the upper boundary': 32n, + 'searched value is under the lower boundary': 2n, + }, + }, + 'Array with gap': { + array: [11n, 12n, 13n, 14n, 15n, 20n, 21n, 22n, 23n, 24n], + tests: { + 'search value in gap': 17n, + }, + }, + 'Array with duplicated elements': { + array: [0n, 10n, 10n, 10n, 10n, 10n, 10n, 10n, 20n], + tests: { + 'search value is duplicated': 10n, + }, + }, + 'Array with duplicated first element': { + array: [10n, 10n, 10n, 10n, 10n, 10n, 10n, 20n], + tests: { + 'search value is duplicated first element': 10n, + }, + }, + 'Array with duplicated last element': { + array: [0n, 10n, 10n, 10n, 10n, 10n, 10n, 10n], + tests: { + 'search value is duplicated last element': 10n, + }, + }, + 'Empty array': { + array: [], + tests: { + 'always returns 0 for empty array': 10n, + }, + }, + })) { + describe(title, function () { + const fixture = async () => { + return { mock: await ethers.deployContract('Uint256ArraysMock', [array]) }; + }; + + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + for (const [name, input] of Object.entries(tests)) { + it(name, async function () { + // findUpperBound does not support duplicated + if (hasDuplicates(array)) this.skip(); + expect(await this.mock.findUpperBound(input)).to.be.equal(lowerBound(array, input)); + }); + } }); - }); + } }); describe('unsafeAccess', function () { - for (const { type, artifact, elements } of [ - { - type: 'address', - artifact: AddressArraysMock, - elements: Array(10) - .fill() - .map(() => web3.utils.randomHex(20)), - }, - { - type: 'bytes32', - artifact: Bytes32ArraysMock, - elements: Array(10) - .fill() - .map(() => web3.utils.randomHex(32)), - }, - { - type: 'uint256', - artifact: Uint256ArraysMock, - elements: Array(10) - .fill() - .map(() => web3.utils.randomHex(32)), - }, - ]) { - it(type, async function () { - const contract = await artifact.new(elements); + const contractCases = { + address: { artifact: 'AddressArraysMock', elements: randomArray(generators.address, 10) }, + bytes32: { artifact: 'Bytes32ArraysMock', elements: randomArray(generators.bytes32, 10) }, + uint256: { artifact: 'Uint256ArraysMock', elements: randomArray(generators.uint256, 10) }, + }; + + const fixture = async () => { + const contracts = {}; + for (const [name, { artifact, elements }] of Object.entries(contractCases)) { + contracts[name] = await ethers.deployContract(artifact, [elements]); + } + return { contracts }; + }; + + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + for (const [name, { elements }] of Object.entries(contractCases)) { + it(name, async function () { for (const i in elements) { - expect(await contract.unsafeAccess(i)).to.be.bignumber.equal(elements[i]); + expect(await this.contracts[name].unsafeAccess(i)).to.be.equal(elements[i]); } }); } diff --git a/test/utils/Base64.test.js b/test/utils/Base64.test.js index dfff0b0d0bb..4707db0c349 100644 --- a/test/utils/Base64.test.js +++ b/test/utils/Base64.test.js @@ -1,33 +1,29 @@ +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const Base64 = artifacts.require('$Base64'); +async function fixture() { + const mock = await ethers.deployContract('$Base64'); + return { mock }; +} -contract('Strings', function () { +describe('Strings', function () { beforeEach(async function () { - this.base64 = await Base64.new(); + Object.assign(this, await loadFixture(fixture)); }); describe('from bytes - base64', function () { - it('converts to base64 encoded string with double padding', async function () { - const TEST_MESSAGE = 'test'; - const input = web3.utils.asciiToHex(TEST_MESSAGE); - expect(await this.base64.$encode(input)).to.equal('dGVzdA=='); - }); + for (const { title, input, expected } of [ + { title: 'converts to base64 encoded string with double padding', input: 'test', expected: 'dGVzdA==' }, + { title: 'converts to base64 encoded string with single padding', input: 'test1', expected: 'dGVzdDE=' }, + { title: 'converts to base64 encoded string without padding', input: 'test12', expected: 'dGVzdDEy' }, + { title: 'empty bytes', input: '0x', expected: '' }, + ]) + it(title, async function () { + const raw = ethers.isBytesLike(input) ? input : ethers.toUtf8Bytes(input); - it('converts to base64 encoded string with single padding', async function () { - const TEST_MESSAGE = 'test1'; - const input = web3.utils.asciiToHex(TEST_MESSAGE); - expect(await this.base64.$encode(input)).to.equal('dGVzdDE='); - }); - - it('converts to base64 encoded string without padding', async function () { - const TEST_MESSAGE = 'test12'; - const input = web3.utils.asciiToHex(TEST_MESSAGE); - expect(await this.base64.$encode(input)).to.equal('dGVzdDEy'); - }); - - it('empty bytes', async function () { - expect(await this.base64.$encode([])).to.equal(''); - }); + expect(await this.mock.$encode(raw)).to.equal(ethers.encodeBase64(raw)); + expect(await this.mock.$encode(raw)).to.equal(expected); + }); }); }); diff --git a/test/utils/Create2.test.js b/test/utils/Create2.test.js index a4ad992370d..df807e7572a 100644 --- a/test/utils/Create2.test.js +++ b/test/utils/Create2.test.js @@ -1,35 +1,42 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { balance, ether, expectEvent, expectRevert, send } = require('@openzeppelin/test-helpers'); -const { expectRevertCustomError } = require('../helpers/customError'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const Create2 = artifacts.require('$Create2'); -const VestingWallet = artifacts.require('VestingWallet'); -// This should be a contract that: -// - has no constructor arguments -// - has no immutable variable populated during construction -const ConstructorLessContract = Create2; +async function fixture() { + const [deployer, other] = await ethers.getSigners(); -contract('Create2', function (accounts) { - const [deployerAccount, other] = accounts; + const factory = await ethers.deployContract('$Create2'); - const salt = 'salt message'; - const saltHex = web3.utils.soliditySha3(salt); + // Bytecode for deploying a contract that includes a constructor. + // We use a vesting wallet, with 3 constructor arguments. + const constructorByteCode = await ethers + .getContractFactory('VestingWallet') + .then(({ bytecode, interface }) => ethers.concat([bytecode, interface.encodeDeploy([other.address, 0n, 0n])])); + + // Bytecode for deploying a contract that has no constructor log. + // Here we use the Create2 helper factory. + const constructorLessBytecode = await ethers + .getContractFactory('$Create2') + .then(({ bytecode, interface }) => ethers.concat([bytecode, interface.encodeDeploy([])])); - const encodedParams = web3.eth.abi.encodeParameters(['address', 'uint64', 'uint64'], [other, 0, 0]).slice(2); + return { deployer, other, factory, constructorByteCode, constructorLessBytecode }; +} - const constructorByteCode = `${VestingWallet.bytecode}${encodedParams}`; +describe('Create2', function () { + const salt = 'salt message'; + const saltHex = ethers.id(salt); beforeEach(async function () { - this.factory = await Create2.new(); + Object.assign(this, await loadFixture(fixture)); }); + describe('computeAddress', function () { it('computes the correct contract address', async function () { - const onChainComputed = await this.factory.$computeAddress(saltHex, web3.utils.keccak256(constructorByteCode)); + const onChainComputed = await this.factory.$computeAddress(saltHex, ethers.keccak256(this.constructorByteCode)); const offChainComputed = ethers.getCreate2Address( - this.factory.address, + this.factory.target, saltHex, - ethers.keccak256(constructorByteCode), + ethers.keccak256(this.constructorByteCode), ); expect(onChainComputed).to.equal(offChainComputed); }); @@ -37,13 +44,13 @@ contract('Create2', function (accounts) { it('computes the correct contract address with deployer', async function () { const onChainComputed = await this.factory.$computeAddress( saltHex, - web3.utils.keccak256(constructorByteCode), - deployerAccount, + ethers.keccak256(this.constructorByteCode), + ethers.Typed.address(this.deployer), ); const offChainComputed = ethers.getCreate2Address( - deployerAccount, + this.deployer.address, saltHex, - ethers.keccak256(constructorByteCode), + ethers.keccak256(this.constructorByteCode), ); expect(onChainComputed).to.equal(offChainComputed); }); @@ -52,71 +59,76 @@ contract('Create2', function (accounts) { describe('deploy', function () { it('deploys a contract without constructor', async function () { const offChainComputed = ethers.getCreate2Address( - this.factory.address, + this.factory.target, saltHex, - ethers.keccak256(ConstructorLessContract.bytecode), + ethers.keccak256(this.constructorLessBytecode), ); - expectEvent(await this.factory.$deploy(0, saltHex, ConstructorLessContract.bytecode), 'return$deploy', { - addr: offChainComputed, - }); + await expect(this.factory.$deploy(0n, saltHex, this.constructorLessBytecode)) + .to.emit(this.factory, 'return$deploy') + .withArgs(offChainComputed); - expect(ConstructorLessContract.bytecode).to.include((await web3.eth.getCode(offChainComputed)).slice(2)); + expect(this.constructorLessBytecode).to.include((await web3.eth.getCode(offChainComputed)).slice(2)); }); it('deploys a contract with constructor arguments', async function () { const offChainComputed = ethers.getCreate2Address( - this.factory.address, + this.factory.target, saltHex, - ethers.keccak256(constructorByteCode), + ethers.keccak256(this.constructorByteCode), ); - expectEvent(await this.factory.$deploy(0, saltHex, constructorByteCode), 'return$deploy', { - addr: offChainComputed, - }); + await expect(this.factory.$deploy(0n, saltHex, this.constructorByteCode)) + .to.emit(this.factory, 'return$deploy') + .withArgs(offChainComputed); - const instance = await VestingWallet.at(offChainComputed); + const instance = await ethers.getContractAt('VestingWallet', offChainComputed); - expect(await instance.owner()).to.be.equal(other); + expect(await instance.owner()).to.equal(this.other.address); }); it('deploys a contract with funds deposited in the factory', async function () { - const deposit = ether('2'); - await send.ether(deployerAccount, this.factory.address, deposit); - expect(await balance.current(this.factory.address)).to.be.bignumber.equal(deposit); + const value = 10n; + + await this.deployer.sendTransaction({ to: this.factory, value }); const offChainComputed = ethers.getCreate2Address( - this.factory.address, + this.factory.target, saltHex, - ethers.keccak256(constructorByteCode), + ethers.keccak256(this.constructorByteCode), ); - expectEvent(await this.factory.$deploy(deposit, saltHex, constructorByteCode), 'return$deploy', { - addr: offChainComputed, - }); + expect(await ethers.provider.getBalance(this.factory)).to.equal(value); + expect(await ethers.provider.getBalance(offChainComputed)).to.equal(0n); - expect(await balance.current(offChainComputed)).to.be.bignumber.equal(deposit); + await expect(this.factory.$deploy(value, saltHex, this.constructorByteCode)) + .to.emit(this.factory, 'return$deploy') + .withArgs(offChainComputed); + + expect(await ethers.provider.getBalance(this.factory)).to.equal(0n); + expect(await ethers.provider.getBalance(offChainComputed)).to.equal(value); }); it('fails deploying a contract in an existent address', async function () { - expectEvent(await this.factory.$deploy(0, saltHex, constructorByteCode), 'return$deploy'); + await expect(this.factory.$deploy(0n, saltHex, this.constructorByteCode)).to.emit(this.factory, 'return$deploy'); - // TODO: Make sure it actually throws "Create2FailedDeployment". - // For some unknown reason, the revert reason sometimes return: - // `revert with unrecognized return data or custom error` - await expectRevert.unspecified(this.factory.$deploy(0, saltHex, constructorByteCode)); + await expect(this.factory.$deploy(0n, saltHex, this.constructorByteCode)).to.be.revertedWithCustomError( + this.factory, + 'Create2FailedDeployment', + ); }); it('fails deploying a contract if the bytecode length is zero', async function () { - await expectRevertCustomError(this.factory.$deploy(0, saltHex, '0x'), 'Create2EmptyBytecode', []); + await expect(this.factory.$deploy(0n, saltHex, '0x')).to.be.revertedWithCustomError( + this.factory, + 'Create2EmptyBytecode', + ); }); it('fails deploying a contract if factory contract does not have sufficient balance', async function () { - await expectRevertCustomError( - this.factory.$deploy(1, saltHex, constructorByteCode), - 'Create2InsufficientBalance', - [0, 1], - ); + await expect(this.factory.$deploy(1n, saltHex, this.constructorByteCode)) + .to.be.revertedWithCustomError(this.factory, 'Create2InsufficientBalance') + .withArgs(0n, 1n); }); }); }); diff --git a/test/utils/Multicall.test.js b/test/utils/Multicall.test.js index 65443cd0a85..7ec7e20ce1b 100644 --- a/test/utils/Multicall.test.js +++ b/test/utils/Multicall.test.js @@ -1,69 +1,72 @@ -const { BN } = require('@openzeppelin/test-helpers'); -const { expectRevertCustomError } = require('../helpers/customError'); +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const ERC20MulticallMock = artifacts.require('$ERC20MulticallMock'); +async function fixture() { + const [holder, alice, bruce] = await ethers.getSigners(); -contract('Multicall', function (accounts) { - const [deployer, alice, bob] = accounts; - const amount = 12000; + const amount = 12_000n; + const helper = await ethers.deployContract('MulticallHelper'); + const mock = await ethers.deployContract('$ERC20MulticallMock', ['name', 'symbol']); + await mock.$_mint(holder, amount); + return { holder, alice, bruce, amount, mock, helper }; +} + +describe('Multicall', function () { beforeEach(async function () { - this.multicallToken = await ERC20MulticallMock.new('name', 'symbol'); - await this.multicallToken.$_mint(deployer, amount); + Object.assign(this, await loadFixture(fixture)); }); it('batches function calls', async function () { - expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0')); - expect(await this.multicallToken.balanceOf(bob)).to.be.bignumber.equal(new BN('0')); + expect(await this.mock.balanceOf(this.alice)).to.equal(0n); + expect(await this.mock.balanceOf(this.bruce)).to.equal(0n); - await this.multicallToken.multicall( - [ - this.multicallToken.contract.methods.transfer(alice, amount / 2).encodeABI(), - this.multicallToken.contract.methods.transfer(bob, amount / 3).encodeABI(), - ], - { from: deployer }, - ); + await expect( + this.mock.multicall([ + this.mock.interface.encodeFunctionData('transfer', [this.alice.address, this.amount / 2n]), + this.mock.interface.encodeFunctionData('transfer', [this.bruce.address, this.amount / 3n]), + ]), + ) + .to.emit(this.mock, 'Transfer') + .withArgs(this.holder.address, this.alice.address, this.amount / 2n) + .to.emit(this.mock, 'Transfer') + .withArgs(this.holder.address, this.bruce.address, this.amount / 3n); - expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN(amount / 2)); - expect(await this.multicallToken.balanceOf(bob)).to.be.bignumber.equal(new BN(amount / 3)); + expect(await this.mock.balanceOf(this.alice)).to.equal(this.amount / 2n); + expect(await this.mock.balanceOf(this.bruce)).to.equal(this.amount / 3n); }); it('returns an array with the result of each call', async function () { - const MulticallTest = artifacts.require('MulticallTest'); - const multicallTest = await MulticallTest.new({ from: deployer }); - await this.multicallToken.transfer(multicallTest.address, amount, { from: deployer }); - expect(await this.multicallToken.balanceOf(multicallTest.address)).to.be.bignumber.equal(new BN(amount)); - - const recipients = [alice, bob]; - const amounts = [amount / 2, amount / 3].map(n => new BN(n)); + await this.mock.transfer(this.helper, this.amount); + expect(await this.mock.balanceOf(this.helper)).to.equal(this.amount); - await multicallTest.checkReturnValues(this.multicallToken.address, recipients, amounts); + await this.helper.checkReturnValues(this.mock, [this.alice, this.bruce], [this.amount / 2n, this.amount / 3n]); }); it('reverts previous calls', async function () { - expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0')); + expect(await this.mock.balanceOf(this.alice)).to.equal(0n); - const call = this.multicallToken.multicall( - [ - this.multicallToken.contract.methods.transfer(alice, amount).encodeABI(), - this.multicallToken.contract.methods.transfer(bob, amount).encodeABI(), - ], - { from: deployer }, - ); + await expect( + this.mock.multicall([ + this.mock.interface.encodeFunctionData('transfer', [this.alice.address, this.amount]), + this.mock.interface.encodeFunctionData('transfer', [this.bruce.address, this.amount]), + ]), + ) + .to.be.revertedWithCustomError(this.mock, 'ERC20InsufficientBalance') + .withArgs(this.holder.address, 0, this.amount); - await expectRevertCustomError(call, 'ERC20InsufficientBalance', [deployer, 0, amount]); - expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0')); + expect(await this.mock.balanceOf(this.alice)).to.equal(0n); }); it('bubbles up revert reasons', async function () { - const call = this.multicallToken.multicall( - [ - this.multicallToken.contract.methods.transfer(alice, amount).encodeABI(), - this.multicallToken.contract.methods.transfer(bob, amount).encodeABI(), - ], - { from: deployer }, - ); - - await expectRevertCustomError(call, 'ERC20InsufficientBalance', [deployer, 0, amount]); + await expect( + this.mock.multicall([ + this.mock.interface.encodeFunctionData('transfer', [this.alice.address, this.amount]), + this.mock.interface.encodeFunctionData('transfer', [this.bruce.address, this.amount]), + ]), + ) + .to.be.revertedWithCustomError(this.mock, 'ERC20InsufficientBalance') + .withArgs(this.holder.address, 0, this.amount); }); }); diff --git a/test/utils/Nonces.test.js b/test/utils/Nonces.test.js index 67a3087e328..18d20defba5 100644 --- a/test/utils/Nonces.test.js +++ b/test/utils/Nonces.test.js @@ -1,71 +1,75 @@ -const expectEvent = require('@openzeppelin/test-helpers/src/expectEvent'); -const { expectRevertCustomError } = require('../helpers/customError'); +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -require('@openzeppelin/test-helpers'); +async function fixture() { + const [sender, other] = await ethers.getSigners(); -const Nonces = artifacts.require('$Nonces'); + const mock = await ethers.deployContract('$Nonces'); -contract('Nonces', function (accounts) { - const [sender, other] = accounts; + return { sender, other, mock }; +} +describe('Nonces', function () { beforeEach(async function () { - this.nonces = await Nonces.new(); + Object.assign(this, await loadFixture(fixture)); }); it('gets a nonce', async function () { - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); + expect(await this.mock.nonces(this.sender)).to.equal(0n); }); describe('_useNonce', function () { it('increments a nonce', async function () { - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); + expect(await this.mock.nonces(this.sender)).to.equal(0n); - const { receipt } = await this.nonces.$_useNonce(sender); - expectEvent(receipt, 'return$_useNonce', ['0']); + await expect(await this.mock.$_useNonce(this.sender)) + .to.emit(this.mock, 'return$_useNonce') + .withArgs(0n); - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); + expect(await this.mock.nonces(this.sender)).to.equal(1n); }); it("increments only sender's nonce", async function () { - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); - expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + expect(await this.mock.nonces(this.sender)).to.equal(0n); + expect(await this.mock.nonces(this.other)).to.equal(0n); - await this.nonces.$_useNonce(sender); + await this.mock.$_useNonce(this.sender); - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); - expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + expect(await this.mock.nonces(this.sender)).to.equal(1n); + expect(await this.mock.nonces(this.other)).to.equal(0n); }); }); describe('_useCheckedNonce', function () { it('increments a nonce', async function () { - const currentNonce = await this.nonces.nonces(sender); - expect(currentNonce).to.be.bignumber.equal('0'); + const currentNonce = await this.mock.nonces(this.sender); - await this.nonces.$_useCheckedNonce(sender, currentNonce); + expect(currentNonce).to.equal(0n); - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); + await this.mock.$_useCheckedNonce(this.sender, currentNonce); + + expect(await this.mock.nonces(this.sender)).to.equal(1n); }); it("increments only sender's nonce", async function () { - const currentNonce = await this.nonces.nonces(sender); + const currentNonce = await this.mock.nonces(this.sender); - expect(currentNonce).to.be.bignumber.equal('0'); - expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + expect(currentNonce).to.equal(0n); + expect(await this.mock.nonces(this.other)).to.equal(0n); - await this.nonces.$_useCheckedNonce(sender, currentNonce); + await this.mock.$_useCheckedNonce(this.sender, currentNonce); - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); - expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + expect(await this.mock.nonces(this.sender)).to.equal(1n); + expect(await this.mock.nonces(this.other)).to.equal(0n); }); it('reverts when nonce is not the expected', async function () { - const currentNonce = await this.nonces.nonces(sender); - await expectRevertCustomError( - this.nonces.$_useCheckedNonce(sender, currentNonce.addn(1)), - 'InvalidAccountNonce', - [sender, currentNonce], - ); + const currentNonce = await this.mock.nonces(this.sender); + + await expect(this.mock.$_useCheckedNonce(this.sender, currentNonce + 1n)) + .to.be.revertedWithCustomError(this.mock, 'InvalidAccountNonce') + .withArgs(this.sender.address, currentNonce); }); }); }); diff --git a/test/utils/Pausable.test.js b/test/utils/Pausable.test.js index e60a62c749e..de46bc46b04 100644 --- a/test/utils/Pausable.test.js +++ b/test/utils/Pausable.test.js @@ -1,83 +1,87 @@ -const { expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { expectRevertCustomError } = require('../helpers/customError'); +async function fixture() { + const [pauser] = await ethers.getSigners(); -const PausableMock = artifacts.require('PausableMock'); + const mock = await ethers.deployContract('PausableMock'); -contract('Pausable', function (accounts) { - const [pauser] = accounts; + return { pauser, mock }; +} +describe('Pausable', function () { beforeEach(async function () { - this.pausable = await PausableMock.new(); + Object.assign(this, await loadFixture(fixture)); }); - context('when unpaused', function () { + describe('when unpaused', function () { beforeEach(async function () { - expect(await this.pausable.paused()).to.equal(false); + expect(await this.mock.paused()).to.be.false; }); it('can perform normal process in non-pause', async function () { - expect(await this.pausable.count()).to.be.bignumber.equal('0'); + expect(await this.mock.count()).to.equal(0n); - await this.pausable.normalProcess(); - expect(await this.pausable.count()).to.be.bignumber.equal('1'); + await this.mock.normalProcess(); + expect(await this.mock.count()).to.equal(1n); }); it('cannot take drastic measure in non-pause', async function () { - await expectRevertCustomError(this.pausable.drasticMeasure(), 'ExpectedPause', []); - expect(await this.pausable.drasticMeasureTaken()).to.equal(false); + await expect(this.mock.drasticMeasure()).to.be.revertedWithCustomError(this.mock, 'ExpectedPause'); + + expect(await this.mock.drasticMeasureTaken()).to.be.false; }); - context('when paused', function () { + describe('when paused', function () { beforeEach(async function () { - this.receipt = await this.pausable.pause({ from: pauser }); + this.tx = await this.mock.pause(); }); - it('emits a Paused event', function () { - expectEvent(this.receipt, 'Paused', { account: pauser }); + it('emits a Paused event', async function () { + await expect(this.tx).to.emit(this.mock, 'Paused').withArgs(this.pauser.address); }); it('cannot perform normal process in pause', async function () { - await expectRevertCustomError(this.pausable.normalProcess(), 'EnforcedPause', []); + await expect(this.mock.normalProcess()).to.be.revertedWithCustomError(this.mock, 'EnforcedPause'); }); it('can take a drastic measure in a pause', async function () { - await this.pausable.drasticMeasure(); - expect(await this.pausable.drasticMeasureTaken()).to.equal(true); + await this.mock.drasticMeasure(); + expect(await this.mock.drasticMeasureTaken()).to.be.true; }); it('reverts when re-pausing', async function () { - await expectRevertCustomError(this.pausable.pause(), 'EnforcedPause', []); + await expect(this.mock.pause()).to.be.revertedWithCustomError(this.mock, 'EnforcedPause'); }); describe('unpausing', function () { it('is unpausable by the pauser', async function () { - await this.pausable.unpause(); - expect(await this.pausable.paused()).to.equal(false); + await this.mock.unpause(); + expect(await this.mock.paused()).to.be.false; }); - context('when unpaused', function () { + describe('when unpaused', function () { beforeEach(async function () { - this.receipt = await this.pausable.unpause({ from: pauser }); + this.tx = await this.mock.unpause(); }); - it('emits an Unpaused event', function () { - expectEvent(this.receipt, 'Unpaused', { account: pauser }); + it('emits an Unpaused event', async function () { + await expect(this.tx).to.emit(this.mock, 'Unpaused').withArgs(this.pauser.address); }); it('should resume allowing normal process', async function () { - expect(await this.pausable.count()).to.be.bignumber.equal('0'); - await this.pausable.normalProcess(); - expect(await this.pausable.count()).to.be.bignumber.equal('1'); + expect(await this.mock.count()).to.equal(0n); + await this.mock.normalProcess(); + expect(await this.mock.count()).to.equal(1n); }); it('should prevent drastic measure', async function () { - await expectRevertCustomError(this.pausable.drasticMeasure(), 'ExpectedPause', []); + await expect(this.mock.drasticMeasure()).to.be.revertedWithCustomError(this.mock, 'ExpectedPause'); }); it('reverts when re-unpausing', async function () { - await expectRevertCustomError(this.pausable.unpause(), 'ExpectedPause', []); + await expect(this.mock.unpause()).to.be.revertedWithCustomError(this.mock, 'ExpectedPause'); }); }); }); diff --git a/test/utils/ReentrancyGuard.test.js b/test/utils/ReentrancyGuard.test.js index 15355c09851..871967e2fae 100644 --- a/test/utils/ReentrancyGuard.test.js +++ b/test/utils/ReentrancyGuard.test.js @@ -1,44 +1,47 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { expectRevertCustomError } = require('../helpers/customError'); +async function fixture() { + const mock = await ethers.deployContract('ReentrancyMock'); + return { mock }; +} -const ReentrancyMock = artifacts.require('ReentrancyMock'); -const ReentrancyAttack = artifacts.require('ReentrancyAttack'); - -contract('ReentrancyGuard', function () { +describe('ReentrancyGuard', function () { beforeEach(async function () { - this.reentrancyMock = await ReentrancyMock.new(); - expect(await this.reentrancyMock.counter()).to.be.bignumber.equal('0'); + Object.assign(this, await loadFixture(fixture)); }); it('nonReentrant function can be called', async function () { - expect(await this.reentrancyMock.counter()).to.be.bignumber.equal('0'); - await this.reentrancyMock.callback(); - expect(await this.reentrancyMock.counter()).to.be.bignumber.equal('1'); + expect(await this.mock.counter()).to.equal(0n); + await this.mock.callback(); + expect(await this.mock.counter()).to.equal(1n); }); it('does not allow remote callback', async function () { - const attacker = await ReentrancyAttack.new(); - await expectRevert(this.reentrancyMock.countAndCall(attacker.address), 'ReentrancyAttack: failed call', []); + const attacker = await ethers.deployContract('ReentrancyAttack'); + await expect(this.mock.countAndCall(attacker)).to.be.revertedWith('ReentrancyAttack: failed call'); }); it('_reentrancyGuardEntered should be true when guarded', async function () { - await this.reentrancyMock.guardedCheckEntered(); + await this.mock.guardedCheckEntered(); }); it('_reentrancyGuardEntered should be false when unguarded', async function () { - await this.reentrancyMock.unguardedCheckNotEntered(); + await this.mock.unguardedCheckNotEntered(); }); // The following are more side-effects than intended behavior: // I put them here as documentation, and to monitor any changes // in the side-effects. it('does not allow local recursion', async function () { - await expectRevertCustomError(this.reentrancyMock.countLocalRecursive(10), 'ReentrancyGuardReentrantCall', []); + await expect(this.mock.countLocalRecursive(10n)).to.be.revertedWithCustomError( + this.mock, + 'ReentrancyGuardReentrantCall', + ); }); it('does not allow indirect local recursion', async function () { - await expectRevert(this.reentrancyMock.countThisRecursive(10), 'ReentrancyMock: failed call', []); + await expect(this.mock.countThisRecursive(10n)).to.be.revertedWith('ReentrancyMock: failed call'); }); }); diff --git a/test/utils/ShortStrings.test.js b/test/utils/ShortStrings.test.js index 189281d38c9..cb1a06aa5c7 100644 --- a/test/utils/ShortStrings.test.js +++ b/test/utils/ShortStrings.test.js @@ -1,19 +1,27 @@ +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { expectRevertCustomError } = require('../helpers/customError'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const ShortStrings = artifacts.require('$ShortStrings'); +const FALLBACK_SENTINEL = ethers.zeroPadValue('0xFF', 32); -function length(sstr) { - return parseInt(sstr.slice(64), 16); -} +const length = sstr => parseInt(sstr.slice(64), 16); +const decode = sstr => ethers.toUtf8String(sstr).slice(0, length(sstr)); +const encode = str => + str.length < 32 + ? ethers.concat([ + ethers.encodeBytes32String(str).slice(0, -2), + ethers.zeroPadValue(ethers.toBeArray(str.length), 1), + ]) + : FALLBACK_SENTINEL; -function decode(sstr) { - return web3.utils.toUtf8(sstr).slice(0, length(sstr)); +async function fixture() { + const mock = await ethers.deployContract('$ShortStrings'); + return { mock }; } -contract('ShortStrings', function () { - before(async function () { - this.mock = await ShortStrings.new(); +describe('ShortStrings', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); }); for (const str of [0, 1, 16, 31, 32, 64, 1024].map(length => 'a'.repeat(length))) { @@ -21,34 +29,35 @@ contract('ShortStrings', function () { it('encode / decode', async function () { if (str.length < 32) { const encoded = await this.mock.$toShortString(str); - expect(decode(encoded)).to.be.equal(str); - - const length = await this.mock.$byteLength(encoded); - expect(length.toNumber()).to.be.equal(str.length); + expect(encoded).to.equal(encode(str)); + expect(decode(encoded)).to.equal(str); - const decoded = await this.mock.$toString(encoded); - expect(decoded).to.be.equal(str); + expect(await this.mock.$byteLength(encoded)).to.equal(str.length); + expect(await this.mock.$toString(encoded)).to.equal(str); } else { - await expectRevertCustomError(this.mock.$toShortString(str), 'StringTooLong', [str]); + await expect(this.mock.$toShortString(str)) + .to.be.revertedWithCustomError(this.mock, 'StringTooLong') + .withArgs(str); } }); it('set / get with fallback', async function () { - const { logs } = await this.mock.$toShortStringWithFallback(str, 0); - const { ret0 } = logs.find(({ event }) => event == 'return$toShortStringWithFallback').args; + const short = await this.mock + .$toShortStringWithFallback(str, 0) + .then(tx => tx.wait()) + .then(receipt => receipt.logs.find(ev => ev.fragment.name == 'return$toShortStringWithFallback').args[0]); - const promise = this.mock.$toString(ret0); + expect(short).to.equal(encode(str)); + + const promise = this.mock.$toString(short); if (str.length < 32) { - expect(await promise).to.be.equal(str); + expect(await promise).to.equal(str); } else { - await expectRevertCustomError(promise, 'InvalidShortString', []); + await expect(promise).to.be.revertedWithCustomError(this.mock, 'InvalidShortString'); } - const length = await this.mock.$byteLengthWithFallback(ret0, 0); - expect(length.toNumber()).to.be.equal(str.length); - - const recovered = await this.mock.$toStringWithFallback(ret0, 0); - expect(recovered).to.be.equal(str); + expect(await this.mock.$byteLengthWithFallback(short, 0)).to.equal(str.length); + expect(await this.mock.$toStringWithFallback(short, 0)).to.equal(str); }); }); } diff --git a/test/utils/StorageSlot.test.js b/test/utils/StorageSlot.test.js index 846512ed2ad..ab237b700a0 100644 --- a/test/utils/StorageSlot.test.js +++ b/test/utils/StorageSlot.test.js @@ -1,210 +1,74 @@ -const { constants, BN } = require('@openzeppelin/test-helpers'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { generators } = require('../helpers/random'); -const StorageSlotMock = artifacts.require('StorageSlotMock'); +const slot = ethers.id('some.storage.slot'); +const otherSlot = ethers.id('some.other.storage.slot'); -const slot = web3.utils.keccak256('some.storage.slot'); -const otherSlot = web3.utils.keccak256('some.other.storage.slot'); +async function fixture() { + const [account] = await ethers.getSigners(); + const mock = await ethers.deployContract('StorageSlotMock'); + return { mock, account }; +} -contract('StorageSlot', function (accounts) { +describe('StorageSlot', function () { beforeEach(async function () { - this.store = await StorageSlotMock.new(); + Object.assign(this, await loadFixture(fixture)); }); - describe('boolean storage slot', function () { - beforeEach(async function () { - this.value = true; - }); - - it('set', async function () { - await this.store.setBoolean(slot, this.value); - }); - - describe('get', function () { - beforeEach(async function () { - await this.store.setBoolean(slot, this.value); + for (const { type, value, zero } of [ + { type: 'Boolean', value: true, zero: false }, + { type: 'Address', value: generators.address(), zero: ethers.ZeroAddress }, + { type: 'Bytes32', value: generators.bytes32(), zero: ethers.ZeroHash }, + { type: 'String', value: 'lorem ipsum', zero: '' }, + { type: 'Bytes', value: generators.hexBytes(128), zero: '0x' }, + ]) { + describe(`${type} storage slot`, function () { + it('set', async function () { + await this.mock.getFunction(`set${type}Slot`)(slot, value); }); - it('from right slot', async function () { - expect(await this.store.getBoolean(slot)).to.be.equal(this.value); - }); + describe('get', function () { + beforeEach(async function () { + await this.mock.getFunction(`set${type}Slot`)(slot, value); + }); - it('from other slot', async function () { - expect(await this.store.getBoolean(otherSlot)).to.be.equal(false); - }); - }); - }); - - describe('address storage slot', function () { - beforeEach(async function () { - this.value = accounts[1]; - }); + it('from right slot', async function () { + expect(await this.mock.getFunction(`get${type}Slot`)(slot)).to.equal(value); + }); - it('set', async function () { - await this.store.setAddress(slot, this.value); - }); - - describe('get', function () { - beforeEach(async function () { - await this.store.setAddress(slot, this.value); - }); - - it('from right slot', async function () { - expect(await this.store.getAddress(slot)).to.be.equal(this.value); - }); - - it('from other slot', async function () { - expect(await this.store.getAddress(otherSlot)).to.be.equal(constants.ZERO_ADDRESS); + it('from other slot', async function () { + expect(await this.mock.getFunction(`get${type}Slot`)(otherSlot)).to.equal(zero); + }); }); }); - }); - - describe('bytes32 storage slot', function () { - beforeEach(async function () { - this.value = web3.utils.keccak256('some byte32 value'); - }); - - it('set', async function () { - await this.store.setBytes32(slot, this.value); - }); - - describe('get', function () { - beforeEach(async function () { - await this.store.setBytes32(slot, this.value); - }); + } - it('from right slot', async function () { - expect(await this.store.getBytes32(slot)).to.be.equal(this.value); + for (const { type, value, zero } of [ + { type: 'String', value: 'lorem ipsum', zero: '' }, + { type: 'Bytes', value: generators.hexBytes(128), zero: '0x' }, + ]) { + describe(`${type} storage pointer`, function () { + it('set', async function () { + await this.mock.getFunction(`set${type}Storage`)(slot, value); }); - it('from other slot', async function () { - expect(await this.store.getBytes32(otherSlot)).to.be.equal(constants.ZERO_BYTES32); - }); - }); - }); + describe('get', function () { + beforeEach(async function () { + await this.mock.getFunction(`set${type}Storage`)(slot, value); + }); - describe('uint256 storage slot', function () { - beforeEach(async function () { - this.value = new BN(1742); - }); - - it('set', async function () { - await this.store.setUint256(slot, this.value); - }); - - describe('get', function () { - beforeEach(async function () { - await this.store.setUint256(slot, this.value); - }); + it('from right slot', async function () { + expect(await this.mock.getFunction(`${type.toLowerCase()}Map`)(slot)).to.equal(value); + expect(await this.mock.getFunction(`get${type}Storage`)(slot)).to.equal(value); + }); - it('from right slot', async function () { - expect(await this.store.getUint256(slot)).to.be.bignumber.equal(this.value); + it('from other slot', async function () { + expect(await this.mock.getFunction(`${type.toLowerCase()}Map`)(otherSlot)).to.equal(zero); + expect(await this.mock.getFunction(`get${type}Storage`)(otherSlot)).to.equal(zero); + }); }); - - it('from other slot', async function () { - expect(await this.store.getUint256(otherSlot)).to.be.bignumber.equal('0'); - }); - }); - }); - - describe('string storage slot', function () { - beforeEach(async function () { - this.value = 'lorem ipsum'; }); - - it('set', async function () { - await this.store.setString(slot, this.value); - }); - - describe('get', function () { - beforeEach(async function () { - await this.store.setString(slot, this.value); - }); - - it('from right slot', async function () { - expect(await this.store.getString(slot)).to.be.equal(this.value); - }); - - it('from other slot', async function () { - expect(await this.store.getString(otherSlot)).to.be.equal(''); - }); - }); - }); - - describe('string storage pointer', function () { - beforeEach(async function () { - this.value = 'lorem ipsum'; - }); - - it('set', async function () { - await this.store.setStringStorage(slot, this.value); - }); - - describe('get', function () { - beforeEach(async function () { - await this.store.setStringStorage(slot, this.value); - }); - - it('from right slot', async function () { - expect(await this.store.stringMap(slot)).to.be.equal(this.value); - expect(await this.store.getStringStorage(slot)).to.be.equal(this.value); - }); - - it('from other slot', async function () { - expect(await this.store.stringMap(otherSlot)).to.be.equal(''); - expect(await this.store.getStringStorage(otherSlot)).to.be.equal(''); - }); - }); - }); - - describe('bytes storage slot', function () { - beforeEach(async function () { - this.value = web3.utils.randomHex(128); - }); - - it('set', async function () { - await this.store.setBytes(slot, this.value); - }); - - describe('get', function () { - beforeEach(async function () { - await this.store.setBytes(slot, this.value); - }); - - it('from right slot', async function () { - expect(await this.store.getBytes(slot)).to.be.equal(this.value); - }); - - it('from other slot', async function () { - expect(await this.store.getBytes(otherSlot)).to.be.equal(null); - }); - }); - }); - - describe('bytes storage pointer', function () { - beforeEach(async function () { - this.value = web3.utils.randomHex(128); - }); - - it('set', async function () { - await this.store.setBytesStorage(slot, this.value); - }); - - describe('get', function () { - beforeEach(async function () { - await this.store.setBytesStorage(slot, this.value); - }); - - it('from right slot', async function () { - expect(await this.store.bytesMap(slot)).to.be.equal(this.value); - expect(await this.store.getBytesStorage(slot)).to.be.equal(this.value); - }); - - it('from other slot', async function () { - expect(await this.store.bytesMap(otherSlot)).to.be.equal(null); - expect(await this.store.getBytesStorage(otherSlot)).to.be.equal(null); - }); - }); - }); + } }); diff --git a/test/utils/Strings.test.js b/test/utils/Strings.test.js index 2435fc71c0d..643172bcbf5 100644 --- a/test/utils/Strings.test.js +++ b/test/utils/Strings.test.js @@ -1,69 +1,71 @@ -const { BN, constants } = require('@openzeppelin/test-helpers'); -const { expectRevertCustomError } = require('../helpers/customError'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const Strings = artifacts.require('$Strings'); +async function fixture() { + const mock = await ethers.deployContract('$Strings'); + return { mock }; +} -contract('Strings', function () { +describe('Strings', function () { before(async function () { - this.strings = await Strings.new(); + Object.assign(this, await loadFixture(fixture)); }); describe('toString', function () { const values = [ - '0', - '7', - '10', - '99', - '100', - '101', - '123', - '4132', - '12345', - '1234567', - '1234567890', - '123456789012345', - '12345678901234567890', - '123456789012345678901234567890', - '1234567890123456789012345678901234567890', - '12345678901234567890123456789012345678901234567890', - '123456789012345678901234567890123456789012345678901234567890', - '1234567890123456789012345678901234567890123456789012345678901234567890', + 0n, + 7n, + 10n, + 99n, + 100n, + 101n, + 123n, + 4132n, + 12345n, + 1234567n, + 1234567890n, + 123456789012345n, + 12345678901234567890n, + 123456789012345678901234567890n, + 1234567890123456789012345678901234567890n, + 12345678901234567890123456789012345678901234567890n, + 123456789012345678901234567890123456789012345678901234567890n, + 1234567890123456789012345678901234567890123456789012345678901234567890n, ]; describe('uint256', function () { it('converts MAX_UINT256', async function () { - const value = constants.MAX_UINT256; - expect(await this.strings.methods['$toString(uint256)'](value)).to.equal(value.toString(10)); + const value = ethers.MaxUint256; + expect(await this.mock.$toString(value)).to.equal(value.toString(10)); }); for (const value of values) { it(`converts ${value}`, async function () { - expect(await this.strings.methods['$toString(uint256)'](value)).to.equal(value); + expect(await this.mock.$toString(value)).to.equal(value); }); } }); describe('int256', function () { it('converts MAX_INT256', async function () { - const value = constants.MAX_INT256; - expect(await this.strings.methods['$toStringSigned(int256)'](value)).to.equal(value.toString(10)); + const value = ethers.MaxInt256; + expect(await this.mock.$toStringSigned(value)).to.equal(value.toString(10)); }); it('converts MIN_INT256', async function () { - const value = constants.MIN_INT256; - expect(await this.strings.methods['$toStringSigned(int256)'](value)).to.equal(value.toString(10)); + const value = ethers.MinInt256; + expect(await this.mock.$toStringSigned(value)).to.equal(value.toString(10)); }); for (const value of values) { it(`convert ${value}`, async function () { - expect(await this.strings.methods['$toStringSigned(int256)'](value)).to.equal(value); + expect(await this.mock.$toStringSigned(value)).to.equal(value); }); it(`convert negative ${value}`, async function () { - const negated = new BN(value).neg(); - expect(await this.strings.methods['$toStringSigned(int256)'](negated)).to.equal(negated.toString(10)); + const negated = -value; + expect(await this.mock.$toStringSigned(negated)).to.equal(negated.toString(10)); }); } }); @@ -71,39 +73,37 @@ contract('Strings', function () { describe('toHexString', function () { it('converts 0', async function () { - expect(await this.strings.methods['$toHexString(uint256)'](0)).to.equal('0x00'); + expect(await this.mock.getFunction('$toHexString(uint256)')(0n)).to.equal('0x00'); }); it('converts a positive number', async function () { - expect(await this.strings.methods['$toHexString(uint256)'](0x4132)).to.equal('0x4132'); + expect(await this.mock.getFunction('$toHexString(uint256)')(0x4132n)).to.equal('0x4132'); }); it('converts MAX_UINT256', async function () { - expect(await this.strings.methods['$toHexString(uint256)'](constants.MAX_UINT256)).to.equal( - web3.utils.toHex(constants.MAX_UINT256), + expect(await this.mock.getFunction('$toHexString(uint256)')(ethers.MaxUint256)).to.equal( + `0x${ethers.MaxUint256.toString(16)}`, ); }); }); describe('toHexString fixed', function () { it('converts a positive number (long)', async function () { - expect(await this.strings.methods['$toHexString(uint256,uint256)'](0x4132, 32)).to.equal( + expect(await this.mock.getFunction('$toHexString(uint256,uint256)')(0x4132n, 32n)).to.equal( '0x0000000000000000000000000000000000000000000000000000000000004132', ); }); it('converts a positive number (short)', async function () { - const length = 1; - await expectRevertCustomError( - this.strings.methods['$toHexString(uint256,uint256)'](0x4132, length), - `StringsInsufficientHexLength`, - [0x4132, length], - ); + const length = 1n; + await expect(this.mock.getFunction('$toHexString(uint256,uint256)')(0x4132n, length)) + .to.be.revertedWithCustomError(this.mock, `StringsInsufficientHexLength`) + .withArgs(0x4132, length); }); it('converts MAX_UINT256', async function () { - expect(await this.strings.methods['$toHexString(uint256,uint256)'](constants.MAX_UINT256, 32)).to.equal( - web3.utils.toHex(constants.MAX_UINT256), + expect(await this.mock.getFunction('$toHexString(uint256,uint256)')(ethers.MaxUint256, 32n)).to.equal( + `0x${ethers.MaxUint256.toString(16)}`, ); }); }); @@ -111,43 +111,43 @@ contract('Strings', function () { describe('toHexString address', function () { it('converts a random address', async function () { const addr = '0xa9036907dccae6a1e0033479b12e837e5cf5a02f'; - expect(await this.strings.methods['$toHexString(address)'](addr)).to.equal(addr); + expect(await this.mock.getFunction('$toHexString(address)')(addr)).to.equal(addr); }); it('converts an address with leading zeros', async function () { const addr = '0x0000e0ca771e21bd00057f54a68c30d400000000'; - expect(await this.strings.methods['$toHexString(address)'](addr)).to.equal(addr); + expect(await this.mock.getFunction('$toHexString(address)')(addr)).to.equal(addr); }); }); describe('equal', function () { it('compares two empty strings', async function () { - expect(await this.strings.methods['$equal(string,string)']('', '')).to.equal(true); + expect(await this.mock.$equal('', '')).to.be.true; }); it('compares two equal strings', async function () { - expect(await this.strings.methods['$equal(string,string)']('a', 'a')).to.equal(true); + expect(await this.mock.$equal('a', 'a')).to.be.true; }); it('compares two different strings', async function () { - expect(await this.strings.methods['$equal(string,string)']('a', 'b')).to.equal(false); + expect(await this.mock.$equal('a', 'b')).to.be.false; }); it('compares two different strings of different lengths', async function () { - expect(await this.strings.methods['$equal(string,string)']('a', 'aa')).to.equal(false); - expect(await this.strings.methods['$equal(string,string)']('aa', 'a')).to.equal(false); + expect(await this.mock.$equal('a', 'aa')).to.be.false; + expect(await this.mock.$equal('aa', 'a')).to.be.false; }); it('compares two different large strings', async function () { const str1 = 'a'.repeat(201); const str2 = 'a'.repeat(200) + 'b'; - expect(await this.strings.methods['$equal(string,string)'](str1, str2)).to.equal(false); + expect(await this.mock.$equal(str1, str2)).to.be.false; }); it('compares two equal large strings', async function () { const str1 = 'a'.repeat(201); const str2 = 'a'.repeat(201); - expect(await this.strings.methods['$equal(string,string)'](str1, str2)).to.equal(true); + expect(await this.mock.$equal(str1, str2)).to.be.true; }); }); }); From 0b1b5f89ef342b73e23e2fb6783d7a377478181f Mon Sep 17 00:00:00 2001 From: luca <80516439+xdaluca@users.noreply.github.com> Date: Fri, 24 Nov 2023 09:21:27 -0800 Subject: [PATCH 19/44] Create FUNDING.json (#4751) --- FUNDING.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 FUNDING.json diff --git a/FUNDING.json b/FUNDING.json new file mode 100644 index 00000000000..c67286216ce --- /dev/null +++ b/FUNDING.json @@ -0,0 +1,7 @@ +{ + "drips": { + "ethereum": { + "ownedBy": "0xAeb37910f93486C85A1F8F994b67E8187554d664" + } + } +} From 769071d47366addc20e160bce53a6dd480d5f89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Mon, 27 Nov 2023 09:39:42 -0600 Subject: [PATCH 20/44] Add note in ERC20Wrapper about rebasing tokens (#4755) Co-authored-by: Hadrien Croubois --- contracts/token/ERC20/extensions/ERC20Wrapper.sol | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/contracts/token/ERC20/extensions/ERC20Wrapper.sol b/contracts/token/ERC20/extensions/ERC20Wrapper.sol index 6645ddc05f8..eecd6ab7a63 100644 --- a/contracts/token/ERC20/extensions/ERC20Wrapper.sol +++ b/contracts/token/ERC20/extensions/ERC20Wrapper.sol @@ -12,6 +12,11 @@ import {SafeERC20} from "../utils/SafeERC20.sol"; * Users can deposit and withdraw "underlying tokens" and receive a matching number of "wrapped tokens". This is useful * in conjunction with other modules. For example, combining this wrapping mechanism with {ERC20Votes} will allow the * wrapping of an existing "basic" ERC-20 into a governance token. + * + * WARNING: Any mechanism in which the underlying token changes the {balanceOf} of an account without an explicit transfer + * may desynchronize this contract's supply and its underlying balance. Please exercise caution when wrapping tokens that + * may undercollateralize the wrapper (i.e. wrapper's total supply is higher than its underlying balance). See {_recover} + * for recovering value accrued to the wrapper. */ abstract contract ERC20Wrapper is ERC20 { IERC20 private immutable _underlying; @@ -75,8 +80,8 @@ abstract contract ERC20Wrapper is ERC20 { } /** - * @dev Mint wrapped token to cover any underlyingTokens that would have been transferred by mistake. Internal - * function that can be exposed with access control if desired. + * @dev Mint wrapped token to cover any underlyingTokens that would have been transferred by mistake or acquired from + * rebasing mechanisms. Internal function that can be exposed with access control if desired. */ function _recover(address account) internal virtual returns (uint256) { uint256 value = _underlying.balanceOf(address(this)) - totalSupply(); From e0ac73cd6e2f3513f3d683e9d272024268c4cfc5 Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Tue, 28 Nov 2023 23:41:10 +0000 Subject: [PATCH 21/44] Refactor enumerableMap generate and tests (#4760) --- scripts/generate/templates/EnumerableMap.js | 8 +- .../generate/templates/EnumerableMap.opts.js | 18 ++ test/utils/structs/EnumerableMap.behavior.js | 14 +- test/utils/structs/EnumerableMap.test.js | 188 +++++++----------- 4 files changed, 93 insertions(+), 135 deletions(-) create mode 100644 scripts/generate/templates/EnumerableMap.opts.js diff --git a/scripts/generate/templates/EnumerableMap.js b/scripts/generate/templates/EnumerableMap.js index 7dbe6ca7f3b..d55305c804d 100644 --- a/scripts/generate/templates/EnumerableMap.js +++ b/scripts/generate/templates/EnumerableMap.js @@ -1,12 +1,6 @@ const format = require('../format-lines'); const { fromBytes32, toBytes32 } = require('./conversion'); - -const TYPES = [ - { name: 'UintToUintMap', keyType: 'uint256', valueType: 'uint256' }, - { name: 'UintToAddressMap', keyType: 'uint256', valueType: 'address' }, - { name: 'AddressToUintMap', keyType: 'address', valueType: 'uint256' }, - { name: 'Bytes32ToUintMap', keyType: 'bytes32', valueType: 'uint256' }, -]; +const { TYPES } = require('./EnumerableMap.opts'); /* eslint-disable max-len */ const header = `\ diff --git a/scripts/generate/templates/EnumerableMap.opts.js b/scripts/generate/templates/EnumerableMap.opts.js new file mode 100644 index 00000000000..699fa7b140e --- /dev/null +++ b/scripts/generate/templates/EnumerableMap.opts.js @@ -0,0 +1,18 @@ +const mapType = str => (str == 'uint256' ? 'Uint' : `${str.charAt(0).toUpperCase()}${str.slice(1)}`); +const formatType = (keyType, valueType) => ({ + name: `${mapType(keyType)}To${mapType(valueType)}Map`, + keyType, + valueType, +}); + +const TYPES = [ + ['uint256', 'uint256'], + ['uint256', 'address'], + ['address', 'uint256'], + ['bytes32', 'uint256'], +].map(args => formatType(...args)); + +module.exports = { + TYPES, + formatType, +}; diff --git a/test/utils/structs/EnumerableMap.behavior.js b/test/utils/structs/EnumerableMap.behavior.js index fb967b34c93..9c675c62d5c 100644 --- a/test/utils/structs/EnumerableMap.behavior.js +++ b/test/utils/structs/EnumerableMap.behavior.js @@ -3,7 +3,7 @@ const { ethers } = require('hardhat'); const zip = (array1, array2) => array1.map((item, index) => [item, array2[index]]); -function shouldBehaveLikeMap(zeroValue, keyType, events) { +function shouldBehaveLikeMap() { async function expectMembersMatch(methods, keys, values) { expect(keys.length).to.equal(values.length); expect(await methods.length()).to.equal(keys.length); @@ -25,7 +25,7 @@ function shouldBehaveLikeMap(zeroValue, keyType, events) { describe('set', function () { it('adds a key', async function () { - await expect(this.methods.set(this.keyA, this.valueA)).to.emit(this.mock, events.setReturn).withArgs(true); + await expect(this.methods.set(this.keyA, this.valueA)).to.emit(this.mock, this.events.setReturn).withArgs(true); await expectMembersMatch(this.methods, [this.keyA], [this.valueA]); }); @@ -41,7 +41,7 @@ function shouldBehaveLikeMap(zeroValue, keyType, events) { it('returns false when adding keys already in the set', async function () { await this.methods.set(this.keyA, this.valueA); - await expect(this.methods.set(this.keyA, this.valueA)).to.emit(this.mock, events.setReturn).withArgs(false); + await expect(this.methods.set(this.keyA, this.valueA)).to.emit(this.mock, this.events.setReturn).withArgs(false); await expectMembersMatch(this.methods, [this.keyA], [this.valueA]); }); @@ -58,7 +58,7 @@ function shouldBehaveLikeMap(zeroValue, keyType, events) { it('removes added keys', async function () { await this.methods.set(this.keyA, this.valueA); - await expect(this.methods.remove(this.keyA)).to.emit(this.mock, events.removeReturn).withArgs(true); + await expect(this.methods.remove(this.keyA)).to.emit(this.mock, this.events.removeReturn).withArgs(true); expect(await this.methods.contains(this.keyA)).to.be.false; await expectMembersMatch(this.methods, [], []); @@ -66,7 +66,7 @@ function shouldBehaveLikeMap(zeroValue, keyType, events) { it('returns false when removing keys not in the set', async function () { await expect(await this.methods.remove(this.keyA)) - .to.emit(this.mock, events.removeReturn) + .to.emit(this.mock, this.events.removeReturn) .withArgs(false); expect(await this.methods.contains(this.keyA)).to.be.false; @@ -130,7 +130,7 @@ function shouldBehaveLikeMap(zeroValue, keyType, events) { it('missing value', async function () { await expect(this.methods.get(this.keyB)) .to.be.revertedWithCustomError(this.mock, 'EnumerableMapNonexistentKey') - .withArgs(ethers.AbiCoder.defaultAbiCoder().encode([keyType], [this.keyB])); + .withArgs(ethers.AbiCoder.defaultAbiCoder().encode([this.keyType], [this.keyB])); }); }); @@ -140,7 +140,7 @@ function shouldBehaveLikeMap(zeroValue, keyType, events) { }); it('missing value', async function () { - expect(await this.methods.tryGet(this.keyB)).to.have.ordered.members([false, zeroValue]); + expect(await this.methods.tryGet(this.keyB)).to.have.ordered.members([false, this.zeroValue]); }); }); }); diff --git a/test/utils/structs/EnumerableMap.test.js b/test/utils/structs/EnumerableMap.test.js index accdb52fabc..183a8c812d5 100644 --- a/test/utils/structs/EnumerableMap.test.js +++ b/test/utils/structs/EnumerableMap.test.js @@ -2,6 +2,7 @@ const { ethers } = require('hardhat'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { mapValues } = require('../../helpers/iterate'); const { randomArray, generators } = require('../../helpers/random'); +const { TYPES, formatType } = require('../../../scripts/generate/templates/EnumerableMap.opts'); const { shouldBehaveLikeMap } = require('./EnumerableMap.behavior'); @@ -14,132 +15,77 @@ const getMethods = (mock, fnSigs) => { ); }; -describe('EnumerableMap', function () { - // UintToAddressMap - describe('UintToAddressMap', function () { - const fixture = async () => { - const mock = await ethers.deployContract('$EnumerableMap'); - - const [keyA, keyB, keyC] = randomArray(generators.uint256); - const [valueA, valueB, valueC] = randomArray(generators.address); - - const methods = getMethods(mock, { - set: '$set(uint256,uint256,address)', - get: '$get_EnumerableMap_UintToAddressMap(uint256,uint256)', - tryGet: '$tryGet_EnumerableMap_UintToAddressMap(uint256,uint256)', - remove: '$remove_EnumerableMap_UintToAddressMap(uint256,uint256)', - length: '$length_EnumerableMap_UintToAddressMap(uint256)', - at: '$at_EnumerableMap_UintToAddressMap(uint256,uint256)', - contains: '$contains_EnumerableMap_UintToAddressMap(uint256,uint256)', - keys: '$keys_EnumerableMap_UintToAddressMap(uint256)', - }); - - return { mock, keyA, keyB, keyC, valueA, valueB, valueC, methods }; - }; - - beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); - }); - - shouldBehaveLikeMap(ethers.ZeroAddress, 'uint256', { - setReturn: 'return$set_EnumerableMap_UintToAddressMap_uint256_address', - removeReturn: 'return$remove_EnumerableMap_UintToAddressMap_uint256', - }); - }); - - // Bytes32ToBytes32Map - describe('Bytes32ToBytes32Map', function () { - const fixture = async () => { - const mock = await ethers.deployContract('$EnumerableMap'); - - const [keyA, keyB, keyC] = randomArray(generators.bytes32); - const [valueA, valueB, valueC] = randomArray(generators.bytes32); - - const methods = getMethods(mock, { - set: '$set(uint256,bytes32,bytes32)', - get: '$get_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32)', - tryGet: '$tryGet_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32)', - remove: '$remove_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32)', - length: '$length_EnumerableMap_Bytes32ToBytes32Map(uint256)', - at: '$at_EnumerableMap_Bytes32ToBytes32Map(uint256,uint256)', - contains: '$contains_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32)', - keys: '$keys_EnumerableMap_Bytes32ToBytes32Map(uint256)', - }); - - return { mock, keyA, keyB, keyC, valueA, valueB, valueC, methods }; - }; - - beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); - }); - - shouldBehaveLikeMap(ethers.ZeroHash, 'bytes32', { - setReturn: 'return$set_EnumerableMap_Bytes32ToBytes32Map_bytes32_bytes32', - removeReturn: 'return$remove_EnumerableMap_Bytes32ToBytes32Map_bytes32', - }); - }); - - // UintToUintMap - describe('UintToUintMap', function () { - const fixture = async () => { - const mock = await ethers.deployContract('$EnumerableMap'); - - const [keyA, keyB, keyC] = randomArray(generators.uint256); - const [valueA, valueB, valueC] = randomArray(generators.uint256); - - const methods = getMethods(mock, { - set: '$set(uint256,uint256,uint256)', - get: '$get_EnumerableMap_UintToUintMap(uint256,uint256)', - tryGet: '$tryGet_EnumerableMap_UintToUintMap(uint256,uint256)', - remove: '$remove_EnumerableMap_UintToUintMap(uint256,uint256)', - length: '$length_EnumerableMap_UintToUintMap(uint256)', - at: '$at_EnumerableMap_UintToUintMap(uint256,uint256)', - contains: '$contains_EnumerableMap_UintToUintMap(uint256,uint256)', - keys: '$keys_EnumerableMap_UintToUintMap(uint256)', - }); - - return { mock, keyA, keyB, keyC, valueA, valueB, valueC, methods }; - }; +const testTypes = [formatType('bytes32', 'bytes32'), ...TYPES]; + +async function fixture() { + const mock = await ethers.deployContract('$EnumerableMap'); + + const zeroValue = { + uint256: 0n, + address: ethers.ZeroAddress, + bytes32: ethers.ZeroHash, + }; + + const env = Object.fromEntries( + testTypes.map(({ name, keyType, valueType }) => [ + name, + { + keyType, + keys: randomArray(generators[keyType]), + values: randomArray(generators[valueType]), + + methods: getMethods( + mock, + testTypes.filter(t => keyType == t.keyType).length == 1 + ? { + set: `$set(uint256,${keyType},${valueType})`, + get: `$get(uint256,${keyType})`, + tryGet: `$tryGet(uint256,${keyType})`, + remove: `$remove(uint256,${keyType})`, + length: `$length_EnumerableMap_${name}(uint256)`, + at: `$at_EnumerableMap_${name}(uint256,uint256)`, + contains: `$contains(uint256,${keyType})`, + keys: `$keys_EnumerableMap_${name}(uint256)`, + } + : { + set: `$set(uint256,${keyType},${valueType})`, + get: `$get_EnumerableMap_${name}(uint256,${keyType})`, + tryGet: `$tryGet_EnumerableMap_${name}(uint256,${keyType})`, + remove: `$remove_EnumerableMap_${name}(uint256,${keyType})`, + length: `$length_EnumerableMap_${name}(uint256)`, + at: `$at_EnumerableMap_${name}(uint256,uint256)`, + contains: `$contains_EnumerableMap_${name}(uint256,${keyType})`, + keys: `$keys_EnumerableMap_${name}(uint256)`, + }, + ), + + zeroValue: zeroValue[valueType], + events: { + setReturn: `return$set_EnumerableMap_${name}_${keyType}_${valueType}`, + removeReturn: `return$remove_EnumerableMap_${name}_${keyType}`, + }, + }, + ]), + ); - beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); - }); + return { mock, env }; +} - shouldBehaveLikeMap(0n, 'uint256', { - setReturn: 'return$set_EnumerableMap_UintToUintMap_uint256_uint256', - removeReturn: 'return$remove_EnumerableMap_UintToUintMap_uint256', - }); +describe('EnumerableMap', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); }); - // Bytes32ToUintMap - describe('Bytes32ToUintMap', function () { - const fixture = async () => { - const mock = await ethers.deployContract('$EnumerableMap'); - - const [keyA, keyB, keyC] = randomArray(generators.bytes32); - const [valueA, valueB, valueC] = randomArray(generators.uint256); - - const methods = getMethods(mock, { - set: '$set(uint256,bytes32,uint256)', - get: '$get_EnumerableMap_Bytes32ToUintMap(uint256,bytes32)', - tryGet: '$tryGet_EnumerableMap_Bytes32ToUintMap(uint256,bytes32)', - remove: '$remove_EnumerableMap_Bytes32ToUintMap(uint256,bytes32)', - length: '$length_EnumerableMap_Bytes32ToUintMap(uint256)', - at: '$at_EnumerableMap_Bytes32ToUintMap(uint256,uint256)', - contains: '$contains_EnumerableMap_Bytes32ToUintMap(uint256,bytes32)', - keys: '$keys_EnumerableMap_Bytes32ToUintMap(uint256)', + // UintToAddressMap + for (const { name } of testTypes) { + describe(name, function () { + beforeEach(async function () { + Object.assign(this, this.env[name]); + [this.keyA, this.keyB, this.keyC] = this.keys; + [this.valueA, this.valueB, this.valueC] = this.values; }); - return { mock, keyA, keyB, keyC, valueA, valueB, valueC, methods }; - }; - - beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); - }); - - shouldBehaveLikeMap(0n, 'bytes32', { - setReturn: 'return$set_EnumerableMap_Bytes32ToUintMap_bytes32_uint256', - removeReturn: 'return$remove_EnumerableMap_Bytes32ToUintMap_bytes32', + shouldBehaveLikeMap(); }); - }); + } }); From e3478edfe789c4ad0292366c96f1c28661d88ab7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 17:47:45 -0600 Subject: [PATCH 22/44] Update dependency @changesets/pre to v2 (#4765) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 53 +++++++++++++++++++++++++++++++++++++++++------ package.json | 2 +- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c3272ea355..3458599f280 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "devDependencies": { "@changesets/changelog-github": "^0.4.8", "@changesets/cli": "^2.26.0", - "@changesets/pre": "^1.0.14", + "@changesets/pre": "^2.0.0", "@changesets/read": "^0.5.9", "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", "@nomicfoundation/hardhat-ethers": "^3.0.4", @@ -259,6 +259,19 @@ "changeset": "bin.js" } }, + "node_modules/@changesets/cli/node_modules/@changesets/pre": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-1.0.14.tgz", + "integrity": "sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.20.1", + "@changesets/errors": "^0.1.4", + "@changesets/types": "^5.2.1", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1" + } + }, "node_modules/@changesets/cli/node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -336,6 +349,19 @@ "@manypkg/get-packages": "^1.1.3" } }, + "node_modules/@changesets/get-release-plan/node_modules/@changesets/pre": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-1.0.14.tgz", + "integrity": "sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.20.1", + "@changesets/errors": "^0.1.4", + "@changesets/types": "^5.2.1", + "@manypkg/get-packages": "^1.1.3", + "fs-extra": "^7.0.1" + } + }, "node_modules/@changesets/get-version-range-type": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.3.2.tgz", @@ -377,18 +403,33 @@ } }, "node_modules/@changesets/pre": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-1.0.14.tgz", - "integrity": "sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-2.0.0.tgz", + "integrity": "sha512-HLTNYX/A4jZxc+Sq8D1AMBsv+1qD6rmmJtjsCJa/9MSRybdxh0mjbTvE6JYZQ/ZiQ0mMlDOlGPXTm9KLTU3jyw==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/errors": "^0.1.4", - "@changesets/types": "^5.2.1", + "@changesets/errors": "^0.2.0", + "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, + "node_modules/@changesets/pre/node_modules/@changesets/errors": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz", + "integrity": "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==", + "dev": true, + "dependencies": { + "extendable-error": "^0.1.5" + } + }, + "node_modules/@changesets/pre/node_modules/@changesets/types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-6.0.0.tgz", + "integrity": "sha512-b1UkfNulgKoWfqyHtzKS5fOZYSJO+77adgL7DLRDr+/7jhChN+QcHnbjiQVOz/U+Ts3PGNySq7diAItzDgugfQ==", + "dev": true + }, "node_modules/@changesets/read": { "version": "0.5.9", "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.5.9.tgz", diff --git a/package.json b/package.json index 50ba8f47838..cf74d6a34b2 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "devDependencies": { "@changesets/changelog-github": "^0.4.8", "@changesets/cli": "^2.26.0", - "@changesets/pre": "^1.0.14", + "@changesets/pre": "^2.0.0", "@changesets/read": "^0.5.9", "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", "@nomicfoundation/hardhat-ethers": "^3.0.4", From 74e396a967bad7dc1032ebab50fabf0d53a67796 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 17:49:38 -0600 Subject: [PATCH 23/44] Update dependency @changesets/changelog-github to ^0.5.0 (#4763) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 24 +++++++++++++++--------- package.json | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3458599f280..3c192be99c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "5.0.0", "license": "MIT", "devDependencies": { - "@changesets/changelog-github": "^0.4.8", + "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.26.0", "@changesets/pre": "^2.0.0", "@changesets/read": "^0.5.9", @@ -205,16 +205,22 @@ } }, "node_modules/@changesets/changelog-github": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-0.4.8.tgz", - "integrity": "sha512-jR1DHibkMAb5v/8ym77E4AMNWZKB5NPzw5a5Wtqm1JepAuIF+hrKp2u04NKM14oBZhHglkCfrla9uq8ORnK/dw==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-0.5.0.tgz", + "integrity": "sha512-zoeq2LJJVcPJcIotHRJEEA2qCqX0AQIeFE+L21L8sRLPVqDhSXY8ZWAt2sohtBpFZkBwu+LUwMSKRr2lMy3LJA==", "dev": true, "dependencies": { - "@changesets/get-github-info": "^0.5.2", - "@changesets/types": "^5.2.1", + "@changesets/get-github-info": "^0.6.0", + "@changesets/types": "^6.0.0", "dotenv": "^8.1.0" } }, + "node_modules/@changesets/changelog-github/node_modules/@changesets/types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-6.0.0.tgz", + "integrity": "sha512-b1UkfNulgKoWfqyHtzKS5fOZYSJO+77adgL7DLRDr+/7jhChN+QcHnbjiQVOz/U+Ts3PGNySq7diAItzDgugfQ==", + "dev": true + }, "node_modules/@changesets/cli": { "version": "2.26.2", "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.26.2.tgz", @@ -325,9 +331,9 @@ } }, "node_modules/@changesets/get-github-info": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.5.2.tgz", - "integrity": "sha512-JppheLu7S114aEs157fOZDjFqUDpm7eHdq5E8SSR0gUBTEK0cNSHsrSR5a66xs0z3RWuo46QvA3vawp8BxDHvg==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.6.0.tgz", + "integrity": "sha512-v/TSnFVXI8vzX9/w3DU2Ol+UlTZcu3m0kXTjTT4KlAdwSvwutcByYwyYn9hwerPWfPkT2JfpoX0KgvCEi8Q/SA==", "dev": true, "dependencies": { "dataloader": "^1.4.0", diff --git a/package.json b/package.json index cf74d6a34b2..37cf126cefd 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ }, "homepage": "https://openzeppelin.com/contracts/", "devDependencies": { - "@changesets/changelog-github": "^0.4.8", + "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.26.0", "@changesets/pre": "^2.0.0", "@changesets/read": "^0.5.9", From c411700572b69c802eeb85fca3519825a5144dd3 Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Tue, 28 Nov 2023 23:51:58 +0000 Subject: [PATCH 24/44] Refactor EnumerableSet generation and tests (#4762) --- scripts/generate/templates/EnumerableSet.js | 7 +- .../generate/templates/EnumerableSet.opts.js | 10 ++ test/utils/structs/EnumerableSet.behavior.js | 18 ++- test/utils/structs/EnumerableSet.test.js | 116 ++++++------------ 4 files changed, 59 insertions(+), 92 deletions(-) create mode 100644 scripts/generate/templates/EnumerableSet.opts.js diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index cb9bffb2c87..e25242cbb9b 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -1,11 +1,6 @@ const format = require('../format-lines'); const { fromBytes32, toBytes32 } = require('./conversion'); - -const TYPES = [ - { name: 'Bytes32Set', type: 'bytes32' }, - { name: 'AddressSet', type: 'address' }, - { name: 'UintSet', type: 'uint256' }, -]; +const { TYPES } = require('./EnumerableSet.opts'); /* eslint-disable max-len */ const header = `\ diff --git a/scripts/generate/templates/EnumerableSet.opts.js b/scripts/generate/templates/EnumerableSet.opts.js new file mode 100644 index 00000000000..fb53724fe8f --- /dev/null +++ b/scripts/generate/templates/EnumerableSet.opts.js @@ -0,0 +1,10 @@ +const mapType = str => (str == 'uint256' ? 'Uint' : `${str.charAt(0).toUpperCase()}${str.slice(1)}`); + +const formatType = type => ({ + name: `${mapType(type)}Set`, + type, +}); + +const TYPES = ['bytes32', 'address', 'uint256'].map(formatType); + +module.exports = { TYPES, formatType }; diff --git a/test/utils/structs/EnumerableSet.behavior.js b/test/utils/structs/EnumerableSet.behavior.js index 5b9e067e8de..d3d4f26d58b 100644 --- a/test/utils/structs/EnumerableSet.behavior.js +++ b/test/utils/structs/EnumerableSet.behavior.js @@ -1,6 +1,7 @@ const { expect } = require('chai'); +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); -function shouldBehaveLikeSet(events) { +function shouldBehaveLikeSet() { async function expectMembersMatch(methods, values) { expect(await methods.length()).to.equal(values.length); for (const value of values) expect(await methods.contains(value)).to.be.true; @@ -17,7 +18,7 @@ function shouldBehaveLikeSet(events) { describe('add', function () { it('adds a value', async function () { - await expect(this.methods.add(this.valueA)).to.emit(this.mock, events.addReturn).withArgs(true); + await expect(this.methods.add(this.valueA)).to.emit(this.mock, this.events.addReturn).withArgs(true); await expectMembersMatch(this.methods, [this.valueA]); }); @@ -33,7 +34,7 @@ function shouldBehaveLikeSet(events) { it('returns false when adding values already in the set', async function () { await this.methods.add(this.valueA); - await expect(this.methods.add(this.valueA)).to.emit(this.mock, events.addReturn).withArgs(false); + await expect(this.methods.add(this.valueA)).to.emit(this.mock, this.events.addReturn).withArgs(false); await expectMembersMatch(this.methods, [this.valueA]); }); @@ -41,7 +42,12 @@ function shouldBehaveLikeSet(events) { describe('at', function () { it('reverts when retrieving non-existent elements', async function () { - await expect(this.methods.at(0)).to.be.reverted; + await expect(this.methods.at(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + + it('retrieves existing element', async function () { + await this.methods.add(this.valueA); + expect(await this.methods.at(0)).to.equal(this.valueA); }); }); @@ -49,14 +55,14 @@ function shouldBehaveLikeSet(events) { it('removes added values', async function () { await this.methods.add(this.valueA); - await expect(this.methods.remove(this.valueA)).to.emit(this.mock, events.removeReturn).withArgs(true); + await expect(this.methods.remove(this.valueA)).to.emit(this.mock, this.events.removeReturn).withArgs(true); expect(await this.methods.contains(this.valueA)).to.be.false; await expectMembersMatch(this.methods, []); }); it('returns false when removing values not in the set', async function () { - await expect(this.methods.remove(this.valueA)).to.emit(this.mock, events.removeReturn).withArgs(false); + await expect(this.methods.remove(this.valueA)).to.emit(this.mock, this.events.removeReturn).withArgs(false); expect(await this.methods.contains(this.valueA)).to.be.false; }); diff --git a/test/utils/structs/EnumerableSet.test.js b/test/utils/structs/EnumerableSet.test.js index 726ea0ee379..4345dfe7d14 100644 --- a/test/utils/structs/EnumerableSet.test.js +++ b/test/utils/structs/EnumerableSet.test.js @@ -2,6 +2,7 @@ const { ethers } = require('hardhat'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { mapValues } = require('../../helpers/iterate'); const { randomArray, generators } = require('../../helpers/random'); +const { TYPES } = require('../../../scripts/generate/templates/EnumerableSet.opts'); const { shouldBehaveLikeSet } = require('./EnumerableSet.behavior'); @@ -14,91 +15,46 @@ const getMethods = (mock, fnSigs) => { ); }; -describe('EnumerableSet', function () { - // Bytes32Set - describe('EnumerableBytes32Set', function () { - const fixture = async () => { - const mock = await ethers.deployContract('$EnumerableSet'); - - const [valueA, valueB, valueC] = randomArray(generators.bytes32); - - const methods = getMethods(mock, { - add: '$add(uint256,bytes32)', - remove: '$remove(uint256,bytes32)', - contains: '$contains(uint256,bytes32)', - length: `$length_EnumerableSet_Bytes32Set(uint256)`, - at: `$at_EnumerableSet_Bytes32Set(uint256,uint256)`, - values: `$values_EnumerableSet_Bytes32Set(uint256)`, - }); - - return { mock, valueA, valueB, valueC, methods }; - }; - - beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); - }); - - shouldBehaveLikeSet({ - addReturn: `return$add_EnumerableSet_Bytes32Set_bytes32`, - removeReturn: `return$remove_EnumerableSet_Bytes32Set_bytes32`, - }); - }); - - // AddressSet - describe('EnumerableAddressSet', function () { - const fixture = async () => { - const mock = await ethers.deployContract('$EnumerableSet'); - - const [valueA, valueB, valueC] = randomArray(generators.address); - - const methods = getMethods(mock, { - add: '$add(uint256,address)', - remove: '$remove(uint256,address)', - contains: '$contains(uint256,address)', - length: `$length_EnumerableSet_AddressSet(uint256)`, - at: `$at_EnumerableSet_AddressSet(uint256,uint256)`, - values: `$values_EnumerableSet_AddressSet(uint256)`, - }); - - return { mock, valueA, valueB, valueC, methods }; - }; +async function fixture() { + const mock = await ethers.deployContract('$EnumerableSet'); + + const env = Object.fromEntries( + TYPES.map(({ name, type }) => [ + type, + { + values: randomArray(generators[type]), + methods: getMethods(mock, { + add: `$add(uint256,${type})`, + remove: `$remove(uint256,${type})`, + contains: `$contains(uint256,${type})`, + length: `$length_EnumerableSet_${name}(uint256)`, + at: `$at_EnumerableSet_${name}(uint256,uint256)`, + values: `$values_EnumerableSet_${name}(uint256)`, + }), + events: { + addReturn: `return$add_EnumerableSet_${name}_${type}`, + removeReturn: `return$remove_EnumerableSet_${name}_${type}`, + }, + }, + ]), + ); - beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); - }); + return { mock, env }; +} - shouldBehaveLikeSet({ - addReturn: `return$add_EnumerableSet_AddressSet_address`, - removeReturn: `return$remove_EnumerableSet_AddressSet_address`, - }); +describe('EnumerableSet', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); }); - // UintSet - describe('EnumerableUintSet', function () { - const fixture = async () => { - const mock = await ethers.deployContract('$EnumerableSet'); - - const [valueA, valueB, valueC] = randomArray(generators.uint256); - - const methods = getMethods(mock, { - add: '$add(uint256,uint256)', - remove: '$remove(uint256,uint256)', - contains: '$contains(uint256,uint256)', - length: `$length_EnumerableSet_UintSet(uint256)`, - at: `$at_EnumerableSet_UintSet(uint256,uint256)`, - values: `$values_EnumerableSet_UintSet(uint256)`, + for (const { type } of TYPES) { + describe(type, function () { + beforeEach(function () { + Object.assign(this, this.env[type]); + [this.valueA, this.valueB, this.valueC] = this.values; }); - return { mock, valueA, valueB, valueC, methods }; - }; - - beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); + shouldBehaveLikeSet(); }); - - shouldBehaveLikeSet({ - addReturn: `return$add_EnumerableSet_UintSet_uint256`, - removeReturn: `return$remove_EnumerableSet_UintSet_uint256`, - }); - }); + } }); From a32077bbac15af9fc9d7d74ffca33c7e331abf60 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 00:03:42 +0000 Subject: [PATCH 25/44] Update dependency @changesets/read to ^0.6.0 (#4764) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 97 +++++++++++++++++++++++++++++++++++++++++++---- package.json | 2 +- 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c192be99c7..922723c432f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.26.0", "@changesets/pre": "^2.0.0", - "@changesets/read": "^0.5.9", + "@changesets/read": "^0.6.0", "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", "@nomicfoundation/hardhat-ethers": "^3.0.4", "@nomicfoundation/hardhat-foundry": "^1.1.1", @@ -278,6 +278,22 @@ "fs-extra": "^7.0.1" } }, + "node_modules/@changesets/cli/node_modules/@changesets/read": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.5.9.tgz", + "integrity": "sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.20.1", + "@changesets/git": "^2.0.0", + "@changesets/logger": "^0.0.5", + "@changesets/parse": "^0.3.16", + "@changesets/types": "^5.2.1", + "chalk": "^2.1.0", + "fs-extra": "^7.0.1", + "p-filter": "^2.1.0" + } + }, "node_modules/@changesets/cli/node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -368,6 +384,22 @@ "fs-extra": "^7.0.1" } }, + "node_modules/@changesets/get-release-plan/node_modules/@changesets/read": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.5.9.tgz", + "integrity": "sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.20.1", + "@changesets/git": "^2.0.0", + "@changesets/logger": "^0.0.5", + "@changesets/parse": "^0.3.16", + "@changesets/types": "^5.2.1", + "chalk": "^2.1.0", + "fs-extra": "^7.0.1", + "p-filter": "^2.1.0" + } + }, "node_modules/@changesets/get-version-range-type": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.3.2.tgz", @@ -437,21 +469,70 @@ "dev": true }, "node_modules/@changesets/read": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.5.9.tgz", - "integrity": "sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.0.tgz", + "integrity": "sha512-ZypqX8+/im1Fm98K4YcZtmLKgjs1kDQ5zHpc2U1qdtNBmZZfo/IBiG162RoP0CUF05tvp2y4IspH11PLnPxuuw==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/git": "^2.0.0", - "@changesets/logger": "^0.0.5", - "@changesets/parse": "^0.3.16", - "@changesets/types": "^5.2.1", + "@changesets/git": "^3.0.0", + "@changesets/logger": "^0.1.0", + "@changesets/parse": "^0.4.0", + "@changesets/types": "^6.0.0", "chalk": "^2.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0" } }, + "node_modules/@changesets/read/node_modules/@changesets/errors": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz", + "integrity": "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==", + "dev": true, + "dependencies": { + "extendable-error": "^0.1.5" + } + }, + "node_modules/@changesets/read/node_modules/@changesets/git": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-3.0.0.tgz", + "integrity": "sha512-vvhnZDHe2eiBNRFHEgMiGd2CT+164dfYyrJDhwwxTVD/OW0FUD6G7+4DIx1dNwkwjHyzisxGAU96q0sVNBns0w==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.20.1", + "@changesets/errors": "^0.2.0", + "@changesets/types": "^6.0.0", + "@manypkg/get-packages": "^1.1.3", + "is-subdir": "^1.1.1", + "micromatch": "^4.0.2", + "spawndamnit": "^2.0.0" + } + }, + "node_modules/@changesets/read/node_modules/@changesets/logger": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.0.tgz", + "integrity": "sha512-pBrJm4CQm9VqFVwWnSqKEfsS2ESnwqwH+xR7jETxIErZcfd1u2zBSqrHbRHR7xjhSgep9x2PSKFKY//FAshA3g==", + "dev": true, + "dependencies": { + "chalk": "^2.1.0" + } + }, + "node_modules/@changesets/read/node_modules/@changesets/parse": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.0.tgz", + "integrity": "sha512-TS/9KG2CdGXS27S+QxbZXgr8uPsP4yNJYb4BC2/NeFUj80Rni3TeD2qwWmabymxmrLo7JEsytXH1FbpKTbvivw==", + "dev": true, + "dependencies": { + "@changesets/types": "^6.0.0", + "js-yaml": "^3.13.1" + } + }, + "node_modules/@changesets/read/node_modules/@changesets/types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-6.0.0.tgz", + "integrity": "sha512-b1UkfNulgKoWfqyHtzKS5fOZYSJO+77adgL7DLRDr+/7jhChN+QcHnbjiQVOz/U+Ts3PGNySq7diAItzDgugfQ==", + "dev": true + }, "node_modules/@changesets/types": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/@changesets/types/-/types-5.2.1.tgz", diff --git a/package.json b/package.json index 37cf126cefd..7eb4603888e 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.26.0", "@changesets/pre": "^2.0.0", - "@changesets/read": "^0.5.9", + "@changesets/read": "^0.6.0", "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", "@nomicfoundation/hardhat-ethers": "^3.0.4", "@nomicfoundation/hardhat-foundry": "^1.1.1", From c35057978f9d13fee96c0ee34d251c00286d3e47 Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Wed, 29 Nov 2023 19:57:16 +0000 Subject: [PATCH 26/44] Migrate ERC20 and ERC20Wrapper tests to ethersjs (#4743) Co-authored-by: Hadrien Croubois --- test/token/ERC20/ERC20.behavior.js | 326 +++++++----------- test/token/ERC20/ERC20.test.js | 195 ++++++----- .../ERC20/extensions/ERC20Wrapper.test.js | 267 +++++++------- 3 files changed, 363 insertions(+), 425 deletions(-) diff --git a/test/token/ERC20/ERC20.behavior.js b/test/token/ERC20/ERC20.behavior.js index b6f8617b247..522df3fcf0e 100644 --- a/test/token/ERC20/ERC20.behavior.js +++ b/test/token/ERC20/ERC20.behavior.js @@ -1,335 +1,271 @@ -const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { ZERO_ADDRESS, MAX_UINT256 } = constants; -const { expectRevertCustomError } = require('../../helpers/customError'); - -function shouldBehaveLikeERC20(initialSupply, accounts, opts = {}) { - const [initialHolder, recipient, anotherAccount] = accounts; +function shouldBehaveLikeERC20(initialSupply, opts = {}) { const { forcedApproval } = opts; - describe('total supply', function () { - it('returns the total token value', async function () { - expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply); - }); + it('total supply: returns the total token value', async function () { + expect(await this.token.totalSupply()).to.equal(initialSupply); }); describe('balanceOf', function () { - describe('when the requested account has no tokens', function () { - it('returns zero', async function () { - expect(await this.token.balanceOf(anotherAccount)).to.be.bignumber.equal('0'); - }); + it('returns zero when the requested account has no tokens', async function () { + expect(await this.token.balanceOf(this.anotherAccount)).to.equal(0n); }); - describe('when the requested account has some tokens', function () { - it('returns the total token value', async function () { - expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(initialSupply); - }); + it('returns the total token value when the requested account has some tokens', async function () { + expect(await this.token.balanceOf(this.initialHolder)).to.equal(initialSupply); }); }); describe('transfer', function () { - shouldBehaveLikeERC20Transfer(initialHolder, recipient, initialSupply, function (from, to, value) { - return this.token.transfer(to, value, { from }); + beforeEach(function () { + this.transfer = (from, to, value) => this.token.connect(from).transfer(to, value); }); + + shouldBehaveLikeERC20Transfer(initialSupply); }); describe('transfer from', function () { - const spender = recipient; - describe('when the token owner is not the zero address', function () { - const tokenOwner = initialHolder; - describe('when the recipient is not the zero address', function () { - const to = anotherAccount; - describe('when the spender has enough allowance', function () { beforeEach(async function () { - await this.token.approve(spender, initialSupply, { from: initialHolder }); + await this.token.connect(this.initialHolder).approve(this.recipient, initialSupply); }); describe('when the token owner has enough balance', function () { const value = initialSupply; - it('transfers the requested value', async function () { - await this.token.transferFrom(tokenOwner, to, value, { from: spender }); - - expect(await this.token.balanceOf(tokenOwner)).to.be.bignumber.equal('0'); + beforeEach(async function () { + this.tx = await this.token + .connect(this.recipient) + .transferFrom(this.initialHolder, this.anotherAccount, value); + }); - expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value); + it('transfers the requested value', async function () { + await expect(this.tx).to.changeTokenBalances( + this.token, + [this.initialHolder, this.anotherAccount], + [-value, value], + ); }); it('decreases the spender allowance', async function () { - await this.token.transferFrom(tokenOwner, to, value, { from: spender }); - - expect(await this.token.allowance(tokenOwner, spender)).to.be.bignumber.equal('0'); + expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(0n); }); it('emits a transfer event', async function () { - expectEvent(await this.token.transferFrom(tokenOwner, to, value, { from: spender }), 'Transfer', { - from: tokenOwner, - to: to, - value: value, - }); + await expect(this.tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.initialHolder.address, this.anotherAccount.address, value); }); if (forcedApproval) { it('emits an approval event', async function () { - expectEvent(await this.token.transferFrom(tokenOwner, to, value, { from: spender }), 'Approval', { - owner: tokenOwner, - spender: spender, - value: await this.token.allowance(tokenOwner, spender), - }); + await expect(this.tx) + .to.emit(this.token, 'Approval') + .withArgs( + this.initialHolder.address, + this.recipient.address, + await this.token.allowance(this.initialHolder, this.recipient), + ); }); } else { it('does not emit an approval event', async function () { - expectEvent.notEmitted( - await this.token.transferFrom(tokenOwner, to, value, { from: spender }), - 'Approval', - ); + await expect(this.tx).to.not.emit(this.token, 'Approval'); }); } }); - describe('when the token owner does not have enough balance', function () { + it('reverts when the token owner does not have enough balance', async function () { const value = initialSupply; - - beforeEach('reducing balance', async function () { - await this.token.transfer(to, 1, { from: tokenOwner }); - }); - - it('reverts', async function () { - await expectRevertCustomError( - this.token.transferFrom(tokenOwner, to, value, { from: spender }), - 'ERC20InsufficientBalance', - [tokenOwner, value - 1, value], - ); - }); + await this.token.connect(this.initialHolder).transfer(this.anotherAccount, 1n); + await expect( + this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value), + ) + .to.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') + .withArgs(this.initialHolder.address, value - 1n, value); }); }); describe('when the spender does not have enough allowance', function () { - const allowance = initialSupply.subn(1); + const allowance = initialSupply - 1n; beforeEach(async function () { - await this.token.approve(spender, allowance, { from: tokenOwner }); + await this.token.connect(this.initialHolder).approve(this.recipient, allowance); }); - describe('when the token owner has enough balance', function () { + it('reverts when the token owner has enough balance', async function () { const value = initialSupply; - - it('reverts', async function () { - await expectRevertCustomError( - this.token.transferFrom(tokenOwner, to, value, { from: spender }), - 'ERC20InsufficientAllowance', - [spender, allowance, value], - ); - }); + await expect( + this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value), + ) + .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientAllowance') + .withArgs(this.recipient.address, allowance, value); }); - describe('when the token owner does not have enough balance', function () { + it('reverts when the token owner does not have enough balance', async function () { const value = allowance; - - beforeEach('reducing balance', async function () { - await this.token.transfer(to, 2, { from: tokenOwner }); - }); - - it('reverts', async function () { - await expectRevertCustomError( - this.token.transferFrom(tokenOwner, to, value, { from: spender }), - 'ERC20InsufficientBalance', - [tokenOwner, value - 1, value], - ); - }); + await this.token.connect(this.initialHolder).transfer(this.anotherAccount, 2); + await expect( + this.token.connect(this.recipient).transferFrom(this.initialHolder, this.anotherAccount, value), + ) + .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') + .withArgs(this.initialHolder.address, value - 1n, value); }); }); describe('when the spender has unlimited allowance', function () { beforeEach(async function () { - await this.token.approve(spender, MAX_UINT256, { from: initialHolder }); + await this.token.connect(this.initialHolder).approve(this.recipient, ethers.MaxUint256); + this.tx = await this.token + .connect(this.recipient) + .transferFrom(this.initialHolder, this.anotherAccount, 1n); }); it('does not decrease the spender allowance', async function () { - await this.token.transferFrom(tokenOwner, to, 1, { from: spender }); - - expect(await this.token.allowance(tokenOwner, spender)).to.be.bignumber.equal(MAX_UINT256); + expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(ethers.MaxUint256); }); it('does not emit an approval event', async function () { - expectEvent.notEmitted(await this.token.transferFrom(tokenOwner, to, 1, { from: spender }), 'Approval'); + await expect(this.tx).to.not.emit(this.token, 'Approval'); }); }); }); - describe('when the recipient is the zero address', function () { + it('reverts when the recipient is the zero address', async function () { const value = initialSupply; - const to = ZERO_ADDRESS; - - beforeEach(async function () { - await this.token.approve(spender, value, { from: tokenOwner }); - }); - - it('reverts', async function () { - await expectRevertCustomError( - this.token.transferFrom(tokenOwner, to, value, { from: spender }), - 'ERC20InvalidReceiver', - [ZERO_ADDRESS], - ); - }); + await this.token.connect(this.initialHolder).approve(this.recipient, value); + await expect(this.token.connect(this.recipient).transferFrom(this.initialHolder, ethers.ZeroAddress, value)) + .to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver') + .withArgs(ethers.ZeroAddress); }); }); - describe('when the token owner is the zero address', function () { - const value = 0; - const tokenOwner = ZERO_ADDRESS; - const to = recipient; - - it('reverts', async function () { - await expectRevertCustomError( - this.token.transferFrom(tokenOwner, to, value, { from: spender }), - 'ERC20InvalidApprover', - [ZERO_ADDRESS], - ); - }); + it('reverts when the token owner is the zero address', async function () { + const value = 0n; + await expect(this.token.connect(this.recipient).transferFrom(ethers.ZeroAddress, this.recipient, value)) + .to.be.revertedWithCustomError(this.token, 'ERC20InvalidApprover') + .withArgs(ethers.ZeroAddress); }); }); describe('approve', function () { - shouldBehaveLikeERC20Approve(initialHolder, recipient, initialSupply, function (owner, spender, value) { - return this.token.approve(spender, value, { from: owner }); + beforeEach(function () { + this.approve = (owner, spender, value) => this.token.connect(owner).approve(spender, value); }); + + shouldBehaveLikeERC20Approve(initialSupply); }); } -function shouldBehaveLikeERC20Transfer(from, to, balance, transfer) { +function shouldBehaveLikeERC20Transfer(balance) { describe('when the recipient is not the zero address', function () { - describe('when the sender does not have enough balance', function () { - const value = balance.addn(1); - - it('reverts', async function () { - await expectRevertCustomError(transfer.call(this, from, to, value), 'ERC20InsufficientBalance', [ - from, - balance, - value, - ]); - }); + it('reverts when the sender does not have enough balance', async function () { + const value = balance + 1n; + await expect(this.transfer(this.initialHolder, this.recipient, value)) + .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') + .withArgs(this.initialHolder.address, balance, value); }); describe('when the sender transfers all balance', function () { const value = balance; - it('transfers the requested value', async function () { - await transfer.call(this, from, to, value); - - expect(await this.token.balanceOf(from)).to.be.bignumber.equal('0'); + beforeEach(async function () { + this.tx = await this.transfer(this.initialHolder, this.recipient, value); + }); - expect(await this.token.balanceOf(to)).to.be.bignumber.equal(value); + it('transfers the requested value', async function () { + await expect(this.tx).to.changeTokenBalances(this.token, [this.initialHolder, this.recipient], [-value, value]); }); it('emits a transfer event', async function () { - expectEvent(await transfer.call(this, from, to, value), 'Transfer', { from, to, value: value }); + await expect(this.tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.initialHolder.address, this.recipient.address, value); }); }); describe('when the sender transfers zero tokens', function () { - const value = new BN('0'); - - it('transfers the requested value', async function () { - await transfer.call(this, from, to, value); + const value = 0n; - expect(await this.token.balanceOf(from)).to.be.bignumber.equal(balance); + beforeEach(async function () { + this.tx = await this.transfer(this.initialHolder, this.recipient, value); + }); - expect(await this.token.balanceOf(to)).to.be.bignumber.equal('0'); + it('transfers the requested value', async function () { + await expect(this.tx).to.changeTokenBalances(this.token, [this.initialHolder, this.recipient], [0n, 0n]); }); it('emits a transfer event', async function () { - expectEvent(await transfer.call(this, from, to, value), 'Transfer', { from, to, value: value }); + await expect(this.tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.initialHolder.address, this.recipient.address, value); }); }); }); - describe('when the recipient is the zero address', function () { - it('reverts', async function () { - await expectRevertCustomError(transfer.call(this, from, ZERO_ADDRESS, balance), 'ERC20InvalidReceiver', [ - ZERO_ADDRESS, - ]); - }); + it('reverts when the recipient is the zero address', async function () { + await expect(this.transfer(this.initialHolder, ethers.ZeroAddress, balance)) + .to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver') + .withArgs(ethers.ZeroAddress); }); } -function shouldBehaveLikeERC20Approve(owner, spender, supply, approve) { +function shouldBehaveLikeERC20Approve(supply) { describe('when the spender is not the zero address', function () { describe('when the sender has enough balance', function () { const value = supply; it('emits an approval event', async function () { - expectEvent(await approve.call(this, owner, spender, value), 'Approval', { - owner: owner, - spender: spender, - value: value, - }); + await expect(this.approve(this.initialHolder, this.recipient, value)) + .to.emit(this.token, 'Approval') + .withArgs(this.initialHolder.address, this.recipient.address, value); }); - describe('when there was no approved value before', function () { - it('approves the requested value', async function () { - await approve.call(this, owner, spender, value); + it('approves the requested value when there was no approved value before', async function () { + await this.approve(this.initialHolder, this.recipient, value); - expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value); - }); + expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(value); }); - describe('when the spender had an approved value', function () { - beforeEach(async function () { - await approve.call(this, owner, spender, new BN(1)); - }); - - it('approves the requested value and replaces the previous one', async function () { - await approve.call(this, owner, spender, value); + it('approves the requested value and replaces the previous one when the spender had an approved value', async function () { + await this.approve(this.initialHolder, this.recipient, 1n); + await this.approve(this.initialHolder, this.recipient, value); - expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value); - }); + expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(value); }); }); describe('when the sender does not have enough balance', function () { - const value = supply.addn(1); + const value = supply + 1n; it('emits an approval event', async function () { - expectEvent(await approve.call(this, owner, spender, value), 'Approval', { - owner: owner, - spender: spender, - value: value, - }); + await expect(this.approve(this.initialHolder, this.recipient, value)) + .to.emit(this.token, 'Approval') + .withArgs(this.initialHolder.address, this.recipient.address, value); }); - describe('when there was no approved value before', function () { - it('approves the requested value', async function () { - await approve.call(this, owner, spender, value); + it('approves the requested value when there was no approved value before', async function () { + await this.approve(this.initialHolder, this.recipient, value); - expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value); - }); + expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(value); }); - describe('when the spender had an approved value', function () { - beforeEach(async function () { - await approve.call(this, owner, spender, new BN(1)); - }); + it('approves the requested value and replaces the previous one when the spender had an approved value', async function () { + await this.approve(this.initialHolder, this.recipient, 1n); + await this.approve(this.initialHolder, this.recipient, value); - it('approves the requested value and replaces the previous one', async function () { - await approve.call(this, owner, spender, value); - - expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value); - }); + expect(await this.token.allowance(this.initialHolder, this.recipient)).to.equal(value); }); }); }); - describe('when the spender is the zero address', function () { - it('reverts', async function () { - await expectRevertCustomError(approve.call(this, owner, ZERO_ADDRESS, supply), `ERC20InvalidSpender`, [ - ZERO_ADDRESS, - ]); - }); + it('reverts when the spender is the zero address', async function () { + await expect(this.approve(this.initialHolder, ethers.ZeroAddress, supply)) + .to.be.revertedWithCustomError(this.token, `ERC20InvalidSpender`) + .withArgs(ethers.ZeroAddress); }); } diff --git a/test/token/ERC20/ERC20.test.js b/test/token/ERC20/ERC20.test.js index 2191fd8cb57..97037a697a7 100644 --- a/test/token/ERC20/ERC20.test.js +++ b/test/token/ERC20/ERC20.test.js @@ -1,34 +1,37 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { ZERO_ADDRESS } = constants; +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); const { shouldBehaveLikeERC20, shouldBehaveLikeERC20Transfer, shouldBehaveLikeERC20Approve, } = require('./ERC20.behavior'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const TOKENS = [ - { Token: artifacts.require('$ERC20') }, - { Token: artifacts.require('$ERC20ApprovalMock'), forcedApproval: true }, -]; +const TOKENS = [{ Token: '$ERC20' }, { Token: '$ERC20ApprovalMock', forcedApproval: true }]; -contract('ERC20', function (accounts) { - const [initialHolder, recipient] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const initialSupply = new BN(100); +const name = 'My Token'; +const symbol = 'MTKN'; +const initialSupply = 100n; +describe('ERC20', function () { for (const { Token, forcedApproval } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { + describe(Token, function () { + const fixture = async () => { + const [initialHolder, recipient, anotherAccount] = await ethers.getSigners(); + + const token = await ethers.deployContract(Token, [name, symbol]); + await token.$_mint(initialHolder, initialSupply); + + return { initialHolder, recipient, anotherAccount, token }; + }; + beforeEach(async function () { - this.token = await Token.new(name, symbol); - await this.token.$_mint(initialHolder, initialSupply); + Object.assign(this, await loadFixture(fixture)); }); - shouldBehaveLikeERC20(initialSupply, accounts, { forcedApproval }); + shouldBehaveLikeERC20(initialSupply, { forcedApproval }); it('has a name', async function () { expect(await this.token.name()).to.equal(name); @@ -39,162 +42,164 @@ contract('ERC20', function (accounts) { }); it('has 18 decimals', async function () { - expect(await this.token.decimals()).to.be.bignumber.equal('18'); + expect(await this.token.decimals()).to.equal(18n); }); describe('_mint', function () { - const value = new BN(50); + const value = 50n; it('rejects a null account', async function () { - await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, value), 'ERC20InvalidReceiver', [ZERO_ADDRESS]); + await expect(this.token.$_mint(ethers.ZeroAddress, value)) + .to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver') + .withArgs(ethers.ZeroAddress); }); it('rejects overflow', async function () { - const maxUint256 = new BN('2').pow(new BN(256)).subn(1); - await expectRevert( - this.token.$_mint(recipient, maxUint256), - 'reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)', + await expect(this.token.$_mint(this.recipient, ethers.MaxUint256)).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW, ); }); describe('for a non zero account', function () { beforeEach('minting', async function () { - this.receipt = await this.token.$_mint(recipient, value); + this.tx = await this.token.$_mint(this.recipient, value); }); it('increments totalSupply', async function () { - const expectedSupply = initialSupply.add(value); - expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply); + await expect(await this.token.totalSupply()).to.equal(initialSupply + value); }); it('increments recipient balance', async function () { - expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(value); + await expect(this.tx).to.changeTokenBalance(this.token, this.recipient, value); }); it('emits Transfer event', async function () { - const event = expectEvent(this.receipt, 'Transfer', { from: ZERO_ADDRESS, to: recipient }); - - expect(event.args.value).to.be.bignumber.equal(value); + await expect(this.tx) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.recipient.address, value); }); }); }); describe('_burn', function () { it('rejects a null account', async function () { - await expectRevertCustomError(this.token.$_burn(ZERO_ADDRESS, new BN(1)), 'ERC20InvalidSender', [ - ZERO_ADDRESS, - ]); + await expect(this.token.$_burn(ethers.ZeroAddress, 1n)) + .to.be.revertedWithCustomError(this.token, 'ERC20InvalidSender') + .withArgs(ethers.ZeroAddress); }); describe('for a non zero account', function () { it('rejects burning more than balance', async function () { - await expectRevertCustomError( - this.token.$_burn(initialHolder, initialSupply.addn(1)), - 'ERC20InsufficientBalance', - [initialHolder, initialSupply, initialSupply.addn(1)], - ); + await expect(this.token.$_burn(this.initialHolder, initialSupply + 1n)) + .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') + .withArgs(this.initialHolder.address, initialSupply, initialSupply + 1n); }); const describeBurn = function (description, value) { describe(description, function () { beforeEach('burning', async function () { - this.receipt = await this.token.$_burn(initialHolder, value); + this.tx = await this.token.$_burn(this.initialHolder, value); }); it('decrements totalSupply', async function () { - const expectedSupply = initialSupply.sub(value); - expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply); + expect(await this.token.totalSupply()).to.equal(initialSupply - value); }); it('decrements initialHolder balance', async function () { - const expectedBalance = initialSupply.sub(value); - expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(expectedBalance); + await expect(this.tx).to.changeTokenBalance(this.token, this.initialHolder, -value); }); it('emits Transfer event', async function () { - const event = expectEvent(this.receipt, 'Transfer', { from: initialHolder, to: ZERO_ADDRESS }); - - expect(event.args.value).to.be.bignumber.equal(value); + await expect(this.tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.initialHolder.address, ethers.ZeroAddress, value); }); }); }; describeBurn('for entire balance', initialSupply); - describeBurn('for less value than balance', initialSupply.subn(1)); + describeBurn('for less value than balance', initialSupply - 1n); }); }); describe('_update', function () { - const value = new BN(1); + const value = 1n; + + beforeEach(async function () { + this.totalSupply = await this.token.totalSupply(); + }); it('from is the zero address', async function () { - const balanceBefore = await this.token.balanceOf(initialHolder); - const totalSupply = await this.token.totalSupply(); + const tx = await this.token.$_update(ethers.ZeroAddress, this.initialHolder, value); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.initialHolder.address, value); - expectEvent(await this.token.$_update(ZERO_ADDRESS, initialHolder, value), 'Transfer', { - from: ZERO_ADDRESS, - to: initialHolder, - value: value, - }); - expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.add(value)); - expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.add(value)); + expect(await this.token.totalSupply()).to.equal(this.totalSupply + value); + await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, value); }); it('to is the zero address', async function () { - const balanceBefore = await this.token.balanceOf(initialHolder); - const totalSupply = await this.token.totalSupply(); + const tx = await this.token.$_update(this.initialHolder, ethers.ZeroAddress, value); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.initialHolder.address, ethers.ZeroAddress, value); - expectEvent(await this.token.$_update(initialHolder, ZERO_ADDRESS, value), 'Transfer', { - from: initialHolder, - to: ZERO_ADDRESS, - value: value, - }); - expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.sub(value)); - expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.sub(value)); + expect(await this.token.totalSupply()).to.equal(this.totalSupply - value); + await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -value); }); - it('from and to are the zero address', async function () { - const totalSupply = await this.token.totalSupply(); + describe('from and to are the same address', function () { + it('zero address', async function () { + const tx = await this.token.$_update(ethers.ZeroAddress, ethers.ZeroAddress, value); + await expect(tx).to.emit(this.token, 'Transfer').withArgs(ethers.ZeroAddress, ethers.ZeroAddress, value); - await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, value); + expect(await this.token.totalSupply()).to.equal(this.totalSupply); + await expect(tx).to.changeTokenBalance(this.token, ethers.ZeroAddress, 0n); + }); - expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply); - expectEvent(await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, value), 'Transfer', { - from: ZERO_ADDRESS, - to: ZERO_ADDRESS, - value: value, + describe('non zero address', function () { + it('reverts without balance', async function () { + await expect(this.token.$_update(this.recipient, this.recipient, value)) + .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') + .withArgs(this.recipient.address, 0n, value); + }); + + it('executes with balance', async function () { + const tx = await this.token.$_update(this.initialHolder, this.initialHolder, value); + await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, 0n); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.initialHolder.address, this.initialHolder.address, value); + }); }); }); }); describe('_transfer', function () { - shouldBehaveLikeERC20Transfer(initialHolder, recipient, initialSupply, function (from, to, value) { - return this.token.$_transfer(from, to, value); + beforeEach(function () { + this.transfer = this.token.$_transfer; }); - describe('when the sender is the zero address', function () { - it('reverts', async function () { - await expectRevertCustomError( - this.token.$_transfer(ZERO_ADDRESS, recipient, initialSupply), - 'ERC20InvalidSender', - [ZERO_ADDRESS], - ); - }); + shouldBehaveLikeERC20Transfer(initialSupply); + + it('reverts when the sender is the zero address', async function () { + await expect(this.token.$_transfer(ethers.ZeroAddress, this.recipient, initialSupply)) + .to.be.revertedWithCustomError(this.token, 'ERC20InvalidSender') + .withArgs(ethers.ZeroAddress); }); }); describe('_approve', function () { - shouldBehaveLikeERC20Approve(initialHolder, recipient, initialSupply, function (owner, spender, value) { - return this.token.$_approve(owner, spender, value); + beforeEach(function () { + this.approve = this.token.$_approve; }); - describe('when the owner is the zero address', function () { - it('reverts', async function () { - await expectRevertCustomError( - this.token.$_approve(ZERO_ADDRESS, recipient, initialSupply), - 'ERC20InvalidApprover', - [ZERO_ADDRESS], - ); - }); + shouldBehaveLikeERC20Approve(initialSupply); + + it('reverts when the owner is the zero address', async function () { + await expect(this.token.$_approve(ethers.ZeroAddress, this.recipient, initialSupply)) + .to.be.revertedWithCustomError(this.token, 'ERC20InvalidApprover') + .withArgs(ethers.ZeroAddress); }); }); }); diff --git a/test/token/ERC20/extensions/ERC20Wrapper.test.js b/test/token/ERC20/extensions/ERC20Wrapper.test.js index c54a9e00763..b61573edd88 100644 --- a/test/token/ERC20/extensions/ERC20Wrapper.test.js +++ b/test/token/ERC20/extensions/ERC20Wrapper.test.js @@ -1,31 +1,34 @@ -const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { ZERO_ADDRESS, MAX_UINT256 } = constants; +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldBehaveLikeERC20 } = require('../ERC20.behavior'); -const { expectRevertCustomError } = require('../../../helpers/customError'); -const NotAnERC20 = artifacts.require('CallReceiverMock'); -const ERC20Decimals = artifacts.require('$ERC20DecimalsMock'); -const ERC20Wrapper = artifacts.require('$ERC20Wrapper'); +const name = 'My Token'; +const symbol = 'MTKN'; +const initialSupply = 100n; -contract('ERC20Wrapper', function (accounts) { - const [initialHolder, receiver] = accounts; +async function fixture() { + const [initialHolder, recipient, anotherAccount] = await ethers.getSigners(); + const underlying = await ethers.deployContract('$ERC20DecimalsMock', [name, symbol, 9]); + await underlying.$_mint(initialHolder, initialSupply); + + const token = await ethers.deployContract('$ERC20Wrapper', [`Wrapped ${name}`, `W${symbol}`, underlying]); + + return { initialHolder, recipient, anotherAccount, underlying, token }; +} + +describe('ERC20Wrapper', function () { const name = 'My Token'; const symbol = 'MTKN'; - const initialSupply = new BN(100); - beforeEach(async function () { - this.underlying = await ERC20Decimals.new(name, symbol, 9); - await this.underlying.$_mint(initialHolder, initialSupply); - - this.token = await ERC20Wrapper.new(`Wrapped ${name}`, `W${symbol}`, this.underlying.address); + Object.assign(this, await loadFixture(fixture)); }); - afterEach(async function () { - expect(await this.underlying.balanceOf(this.token.address)).to.be.bignumber.equal(await this.token.totalSupply()); + afterEach('Underlying balance', async function () { + expect(await this.underlying.balanceOf(this.token)).to.be.equal(await this.token.totalSupply()); }); it('has a name', async function () { @@ -37,175 +40,169 @@ contract('ERC20Wrapper', function (accounts) { }); it('has the same decimals as the underlying token', async function () { - expect(await this.token.decimals()).to.be.bignumber.equal('9'); + expect(await this.token.decimals()).to.be.equal(9n); }); it('decimals default back to 18 if token has no metadata', async function () { - const noDecimals = await NotAnERC20.new(); - const otherToken = await ERC20Wrapper.new(`Wrapped ${name}`, `W${symbol}`, noDecimals.address); - expect(await otherToken.decimals()).to.be.bignumber.equal('18'); + const noDecimals = await ethers.deployContract('CallReceiverMock'); + const token = await ethers.deployContract('$ERC20Wrapper', [`Wrapped ${name}`, `W${symbol}`, noDecimals]); + expect(await token.decimals()).to.be.equal(18n); }); it('has underlying', async function () { - expect(await this.token.underlying()).to.be.bignumber.equal(this.underlying.address); + expect(await this.token.underlying()).to.be.equal(this.underlying.target); }); describe('deposit', function () { - it('valid', async function () { - await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder }); - const { tx } = await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }); - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: initialHolder, - to: this.token.address, - value: initialSupply, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: ZERO_ADDRESS, - to: initialHolder, - value: initialSupply, - }); + it('executes with approval', async function () { + await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply); + const tx = await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply); + await expect(tx) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.initialHolder.address, this.token.target, initialSupply) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.initialHolder.address, initialSupply); + + await expect(tx).to.changeTokenBalances( + this.underlying, + [this.initialHolder, this.token], + [-initialSupply, initialSupply], + ); + await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, initialSupply); }); - it('missing approval', async function () { - await expectRevertCustomError( - this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }), - 'ERC20InsufficientAllowance', - [this.token.address, 0, initialSupply], - ); + it('reverts when missing approval', async function () { + await expect(this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply)) + .to.be.revertedWithCustomError(this.underlying, 'ERC20InsufficientAllowance') + .withArgs(this.token.target, 0, initialSupply); }); - it('missing balance', async function () { - await this.underlying.approve(this.token.address, MAX_UINT256, { from: initialHolder }); - await expectRevertCustomError( - this.token.depositFor(initialHolder, MAX_UINT256, { from: initialHolder }), - 'ERC20InsufficientBalance', - [initialHolder, initialSupply, MAX_UINT256], - ); + it('reverts when inssuficient balance', async function () { + await this.underlying.connect(this.initialHolder).approve(this.token, ethers.MaxUint256); + await expect(this.token.connect(this.initialHolder).depositFor(this.initialHolder, ethers.MaxUint256)) + .to.be.revertedWithCustomError(this.underlying, 'ERC20InsufficientBalance') + .withArgs(this.initialHolder.address, initialSupply, ethers.MaxUint256); }); - it('to other account', async function () { - await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder }); - const { tx } = await this.token.depositFor(receiver, initialSupply, { from: initialHolder }); - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: initialHolder, - to: this.token.address, - value: initialSupply, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: ZERO_ADDRESS, - to: receiver, - value: initialSupply, - }); + it('deposits to other account', async function () { + await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply); + const tx = await this.token.connect(this.initialHolder).depositFor(this.recipient, initialSupply); + await expect(tx) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.initialHolder.address, this.token.target, initialSupply) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.recipient.address, initialSupply); + + await expect(tx).to.changeTokenBalances( + this.underlying, + [this.initialHolder, this.token], + [-initialSupply, initialSupply], + ); + await expect(tx).to.changeTokenBalances(this.token, [this.initialHolder, this.recipient], [0, initialSupply]); }); it('reverts minting to the wrapper contract', async function () { - await this.underlying.approve(this.token.address, MAX_UINT256, { from: initialHolder }); - await expectRevertCustomError( - this.token.depositFor(this.token.address, MAX_UINT256, { from: initialHolder }), - 'ERC20InvalidReceiver', - [this.token.address], - ); + await this.underlying.connect(this.initialHolder).approve(this.token, ethers.MaxUint256); + await expect(this.token.connect(this.initialHolder).depositFor(this.token, ethers.MaxUint256)) + .to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver') + .withArgs(this.token.target); }); }); describe('withdraw', function () { beforeEach(async function () { - await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder }); - await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }); + await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply); + await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply); }); - it('missing balance', async function () { - await expectRevertCustomError( - this.token.withdrawTo(initialHolder, MAX_UINT256, { from: initialHolder }), - 'ERC20InsufficientBalance', - [initialHolder, initialSupply, MAX_UINT256], - ); + it('reverts when inssuficient balance', async function () { + await expect(this.token.connect(this.initialHolder).withdrawTo(this.initialHolder, ethers.MaxInt256)) + .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') + .withArgs(this.initialHolder.address, initialSupply, ethers.MaxInt256); }); - it('valid', async function () { - const value = new BN(42); - - const { tx } = await this.token.withdrawTo(initialHolder, value, { from: initialHolder }); - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: this.token.address, - to: initialHolder, - value: value, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: initialHolder, - to: ZERO_ADDRESS, - value: value, - }); + it('executes when operation is valid', async function () { + const value = 42n; + + const tx = await this.token.connect(this.initialHolder).withdrawTo(this.initialHolder, value); + await expect(tx) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.token.target, this.initialHolder.address, value) + .to.emit(this.token, 'Transfer') + .withArgs(this.initialHolder.address, ethers.ZeroAddress, value); + + await expect(tx).to.changeTokenBalances(this.underlying, [this.token, this.initialHolder], [-value, value]); + await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -value); }); it('entire balance', async function () { - const { tx } = await this.token.withdrawTo(initialHolder, initialSupply, { from: initialHolder }); - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: this.token.address, - to: initialHolder, - value: initialSupply, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: initialHolder, - to: ZERO_ADDRESS, - value: initialSupply, - }); + const tx = await this.token.connect(this.initialHolder).withdrawTo(this.initialHolder, initialSupply); + await expect(tx) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.token.target, this.initialHolder.address, initialSupply) + .to.emit(this.token, 'Transfer') + .withArgs(this.initialHolder.address, ethers.ZeroAddress, initialSupply); + + await expect(tx).to.changeTokenBalances( + this.underlying, + [this.token, this.initialHolder], + [-initialSupply, initialSupply], + ); + await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -initialSupply); }); it('to other account', async function () { - const { tx } = await this.token.withdrawTo(receiver, initialSupply, { from: initialHolder }); - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: this.token.address, - to: receiver, - value: initialSupply, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: initialHolder, - to: ZERO_ADDRESS, - value: initialSupply, - }); + const tx = await this.token.connect(this.initialHolder).withdrawTo(this.recipient, initialSupply); + await expect(tx) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.token.target, this.recipient.address, initialSupply) + .to.emit(this.token, 'Transfer') + .withArgs(this.initialHolder.address, ethers.ZeroAddress, initialSupply); + + await expect(tx).to.changeTokenBalances( + this.underlying, + [this.token, this.initialHolder, this.recipient], + [-initialSupply, 0, initialSupply], + ); + await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -initialSupply); }); it('reverts withdrawing to the wrapper contract', async function () { - expectRevertCustomError( - this.token.withdrawTo(this.token.address, initialSupply, { from: initialHolder }), - 'ERC20InvalidReceiver', - [this.token.address], - ); + await expect(this.token.connect(this.initialHolder).withdrawTo(this.token, initialSupply)) + .to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver') + .withArgs(this.token.target); }); }); describe('recover', function () { it('nothing to recover', async function () { - await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder }); - await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }); - - const { tx } = await this.token.$_recover(receiver); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: ZERO_ADDRESS, - to: receiver, - value: '0', - }); + await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply); + await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply); + + const tx = await this.token.$_recover(this.recipient); + await expect(tx).to.emit(this.token, 'Transfer').withArgs(ethers.ZeroAddress, this.recipient.address, 0n); + + await expect(tx).to.changeTokenBalance(this.token, this.recipient, 0); }); it('something to recover', async function () { - await this.underlying.transfer(this.token.address, initialSupply, { from: initialHolder }); - - const { tx } = await this.token.$_recover(receiver); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: ZERO_ADDRESS, - to: receiver, - value: initialSupply, - }); + await this.underlying.connect(this.initialHolder).transfer(this.token, initialSupply); + + const tx = await this.token.$_recover(this.recipient); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.recipient.address, initialSupply); + + await expect(tx).to.changeTokenBalance(this.token, this.recipient, initialSupply); }); }); describe('erc20 behaviour', function () { beforeEach(async function () { - await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder }); - await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }); + await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply); + await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply); }); - shouldBehaveLikeERC20(initialSupply, accounts); + shouldBehaveLikeERC20(initialSupply); }); }); From ae69142379cd5f20b79c1f864d89f8fe955898da Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Wed, 29 Nov 2023 21:51:08 +0000 Subject: [PATCH 27/44] Migrate proxy folder to ethersjs (#4746) Co-authored-by: Hadrien Croubois Co-authored-by: ernestognw --- test/helpers/erc1967.js | 39 +- test/proxy/Clones.behaviour.js | 77 ++-- test/proxy/Clones.test.js | 99 ++-- test/proxy/ERC1967/ERC1967Proxy.test.js | 25 +- test/proxy/ERC1967/ERC1967Utils.test.js | 138 +++--- test/proxy/Proxy.behaviour.js | 112 ++--- test/proxy/beacon/BeaconProxy.test.js | 190 ++++---- test/proxy/beacon/UpgradeableBeacon.test.js | 63 +-- test/proxy/transparent/ProxyAdmin.test.js | 88 ++-- .../TransparentUpgradeableProxy.behaviour.js | 421 ++++++++---------- .../TransparentUpgradeableProxy.test.js | 40 +- test/proxy/utils/Initializable.test.js | 196 ++++---- test/proxy/utils/UUPSUpgradeable.test.js | 157 +++---- 13 files changed, 787 insertions(+), 858 deletions(-) diff --git a/test/helpers/erc1967.js b/test/helpers/erc1967.js index 50542c89a9f..88a87d66121 100644 --- a/test/helpers/erc1967.js +++ b/test/helpers/erc1967.js @@ -5,37 +5,32 @@ const ImplementationLabel = 'eip1967.proxy.implementation'; const AdminLabel = 'eip1967.proxy.admin'; const BeaconLabel = 'eip1967.proxy.beacon'; -function labelToSlot(label) { - return ethers.toBeHex(BigInt(ethers.keccak256(ethers.toUtf8Bytes(label))) - 1n); -} +const erc1967slot = label => ethers.toBeHex(ethers.toBigInt(ethers.id(label)) - 1n); +const erc7201slot = label => ethers.toBeHex(ethers.toBigInt(ethers.keccak256(erc1967slot(label))) & ~0xffn); -function getSlot(address, slot) { - return getStorageAt( - ethers.isAddress(address) ? address : address.address, - ethers.isBytesLike(slot) ? slot : labelToSlot(slot), +const getSlot = (address, slot) => + (ethers.isAddressable(address) ? address.getAddress() : Promise.resolve(address)).then(address => + getStorageAt(address, ethers.isBytesLike(slot) ? slot : erc1967slot(slot)), ); -} -function setSlot(address, slot, value) { - return setStorageAt( - ethers.isAddress(address) ? address : address.address, - ethers.isBytesLike(slot) ? slot : labelToSlot(slot), - value, - ); -} +const setSlot = (address, slot, value) => + Promise.all([ + ethers.isAddressable(address) ? address.getAddress() : Promise.resolve(address), + ethers.isAddressable(value) ? value.getAddress() : Promise.resolve(value), + ]).then(([address, value]) => setStorageAt(address, ethers.isBytesLike(slot) ? slot : erc1967slot(slot), value)); -async function getAddressInSlot(address, slot) { - const slotValue = await getSlot(address, slot); - return ethers.getAddress(slotValue.substring(slotValue.length - 40)); -} +const getAddressInSlot = (address, slot) => + getSlot(address, slot).then(slotValue => ethers.AbiCoder.defaultAbiCoder().decode(['address'], slotValue)[0]); module.exports = { ImplementationLabel, AdminLabel, BeaconLabel, - ImplementationSlot: labelToSlot(ImplementationLabel), - AdminSlot: labelToSlot(AdminLabel), - BeaconSlot: labelToSlot(BeaconLabel), + ImplementationSlot: erc1967slot(ImplementationLabel), + AdminSlot: erc1967slot(AdminLabel), + BeaconSlot: erc1967slot(BeaconLabel), + erc1967slot, + erc7201slot, setSlot, getSlot, getAddressInSlot, diff --git a/test/proxy/Clones.behaviour.js b/test/proxy/Clones.behaviour.js index b5fd3c51b21..861fae8a2af 100644 --- a/test/proxy/Clones.behaviour.js +++ b/test/proxy/Clones.behaviour.js @@ -1,33 +1,29 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const DummyImplementation = artifacts.require('DummyImplementation'); - -module.exports = function shouldBehaveLikeClone(createClone) { - before('deploy implementation', async function () { - this.implementation = web3.utils.toChecksumAddress((await DummyImplementation.new()).address); - }); - +module.exports = function shouldBehaveLikeClone() { const assertProxyInitialization = function ({ value, balance }) { it('initializes the proxy', async function () { - const dummy = new DummyImplementation(this.proxy); - expect(await dummy.value()).to.be.bignumber.equal(value.toString()); + const dummy = await ethers.getContractAt('DummyImplementation', this.proxy); + expect(await dummy.value()).to.equal(value); }); it('has expected balance', async function () { - expect(await web3.eth.getBalance(this.proxy)).to.be.bignumber.equal(balance.toString()); + expect(await ethers.provider.getBalance(this.proxy)).to.equal(balance); }); }; describe('initialization without parameters', function () { describe('non payable', function () { - const expectedInitializedValue = 10; - const initializeData = new DummyImplementation('').contract.methods['initializeNonPayable()']().encodeABI(); + const expectedInitializedValue = 10n; + + beforeEach(async function () { + this.initializeData = await this.implementation.interface.encodeFunctionData('initializeNonPayable'); + }); describe('when not sending balance', function () { beforeEach('creating proxy', async function () { - this.proxy = (await createClone(this.implementation, initializeData)).address; + this.proxy = await this.createClone(this.initializeData); }); assertProxyInitialization({ @@ -37,21 +33,24 @@ module.exports = function shouldBehaveLikeClone(createClone) { }); describe('when sending some balance', function () { - const value = 10e5; + const value = 10n ** 6n; it('reverts', async function () { - await expectRevert.unspecified(createClone(this.implementation, initializeData, { value })); + await expect(this.createClone(this.initializeData, { value })).to.be.reverted; }); }); }); describe('payable', function () { - const expectedInitializedValue = 100; - const initializeData = new DummyImplementation('').contract.methods['initializePayable()']().encodeABI(); + const expectedInitializedValue = 100n; + + beforeEach(async function () { + this.initializeData = await this.implementation.interface.encodeFunctionData('initializePayable'); + }); describe('when not sending balance', function () { beforeEach('creating proxy', async function () { - this.proxy = (await createClone(this.implementation, initializeData)).address; + this.proxy = await this.createClone(this.initializeData); }); assertProxyInitialization({ @@ -61,10 +60,10 @@ module.exports = function shouldBehaveLikeClone(createClone) { }); describe('when sending some balance', function () { - const value = 10e5; + const value = 10n ** 6n; beforeEach('creating proxy', async function () { - this.proxy = (await createClone(this.implementation, initializeData, { value })).address; + this.proxy = await this.createClone(this.initializeData, { value }); }); assertProxyInitialization({ @@ -77,14 +76,17 @@ module.exports = function shouldBehaveLikeClone(createClone) { describe('initialization with parameters', function () { describe('non payable', function () { - const expectedInitializedValue = 10; - const initializeData = new DummyImplementation('').contract.methods - .initializeNonPayableWithValue(expectedInitializedValue) - .encodeABI(); + const expectedInitializedValue = 10n; + + beforeEach(async function () { + this.initializeData = await this.implementation.interface.encodeFunctionData('initializeNonPayableWithValue', [ + expectedInitializedValue, + ]); + }); describe('when not sending balance', function () { beforeEach('creating proxy', async function () { - this.proxy = (await createClone(this.implementation, initializeData)).address; + this.proxy = await this.createClone(this.initializeData); }); assertProxyInitialization({ @@ -94,23 +96,26 @@ module.exports = function shouldBehaveLikeClone(createClone) { }); describe('when sending some balance', function () { - const value = 10e5; + const value = 10n ** 6n; it('reverts', async function () { - await expectRevert.unspecified(createClone(this.implementation, initializeData, { value })); + await expect(this.createClone(this.initializeData, { value })).to.be.reverted; }); }); }); describe('payable', function () { - const expectedInitializedValue = 42; - const initializeData = new DummyImplementation('').contract.methods - .initializePayableWithValue(expectedInitializedValue) - .encodeABI(); + const expectedInitializedValue = 42n; + + beforeEach(function () { + this.initializeData = this.implementation.interface.encodeFunctionData('initializePayableWithValue', [ + expectedInitializedValue, + ]); + }); describe('when not sending balance', function () { beforeEach('creating proxy', async function () { - this.proxy = (await createClone(this.implementation, initializeData)).address; + this.proxy = await this.createClone(this.initializeData); }); assertProxyInitialization({ @@ -120,10 +125,10 @@ module.exports = function shouldBehaveLikeClone(createClone) { }); describe('when sending some balance', function () { - const value = 10e5; + const value = 10n ** 6n; beforeEach('creating proxy', async function () { - this.proxy = (await createClone(this.implementation, initializeData, { value })).address; + this.proxy = await this.createClone(this.initializeData, { value }); }); assertProxyInitialization({ diff --git a/test/proxy/Clones.test.js b/test/proxy/Clones.test.js index ad3dd537ccc..626b1e56467 100644 --- a/test/proxy/Clones.test.js +++ b/test/proxy/Clones.test.js @@ -1,62 +1,87 @@ -const { ethers } = require('ethers'); -const { expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { expectRevertCustomError } = require('../helpers/customError'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const shouldBehaveLikeClone = require('./Clones.behaviour'); -const Clones = artifacts.require('$Clones'); +async function fixture() { + const [deployer] = await ethers.getSigners(); -contract('Clones', function (accounts) { - const [deployer] = accounts; + const factory = await ethers.deployContract('$Clones'); + const implementation = await ethers.deployContract('DummyImplementation'); + + const newClone = async (initData, opts = {}) => { + const clone = await factory.$clone.staticCall(implementation).then(address => implementation.attach(address)); + await factory.$clone(implementation); + await deployer.sendTransaction({ to: clone, value: opts.value ?? 0n, data: initData ?? '0x' }); + return clone; + }; + + const newCloneDeterministic = async (initData, opts = {}) => { + const salt = opts.salt ?? ethers.randomBytes(32); + const clone = await factory.$cloneDeterministic + .staticCall(implementation, salt) + .then(address => implementation.attach(address)); + await factory.$cloneDeterministic(implementation, salt); + await deployer.sendTransaction({ to: clone, value: opts.value ?? 0n, data: initData ?? '0x' }); + return clone; + }; + + return { deployer, factory, implementation, newClone, newCloneDeterministic }; +} + +describe('Clones', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); describe('clone', function () { - shouldBehaveLikeClone(async (implementation, initData, opts = {}) => { - const factory = await Clones.new(); - const receipt = await factory.$clone(implementation); - const address = receipt.logs.find(({ event }) => event === 'return$clone').args.instance; - await web3.eth.sendTransaction({ from: deployer, to: address, value: opts.value, data: initData }); - return { address }; + beforeEach(async function () { + this.createClone = this.newClone; }); + + shouldBehaveLikeClone(); }); describe('cloneDeterministic', function () { - shouldBehaveLikeClone(async (implementation, initData, opts = {}) => { - const salt = web3.utils.randomHex(32); - const factory = await Clones.new(); - const receipt = await factory.$cloneDeterministic(implementation, salt); - const address = receipt.logs.find(({ event }) => event === 'return$cloneDeterministic').args.instance; - await web3.eth.sendTransaction({ from: deployer, to: address, value: opts.value, data: initData }); - return { address }; + beforeEach(async function () { + this.createClone = this.newCloneDeterministic; }); - it('address already used', async function () { - const implementation = web3.utils.randomHex(20); - const salt = web3.utils.randomHex(32); - const factory = await Clones.new(); + shouldBehaveLikeClone(); + + it('revert if address already used', async function () { + const salt = ethers.randomBytes(32); + // deploy once - expectEvent(await factory.$cloneDeterministic(implementation, salt), 'return$cloneDeterministic'); + await expect(this.factory.$cloneDeterministic(this.implementation, salt)).to.emit( + this.factory, + 'return$cloneDeterministic', + ); + // deploy twice - await expectRevertCustomError(factory.$cloneDeterministic(implementation, salt), 'ERC1167FailedCreateClone', []); + await expect(this.factory.$cloneDeterministic(this.implementation, salt)).to.be.revertedWithCustomError( + this.factory, + 'ERC1167FailedCreateClone', + ); }); it('address prediction', async function () { - const implementation = web3.utils.randomHex(20); - const salt = web3.utils.randomHex(32); - const factory = await Clones.new(); - const predicted = await factory.$predictDeterministicAddress(implementation, salt); + const salt = ethers.randomBytes(32); - const creationCode = [ + const creationCode = ethers.concat([ '0x3d602d80600a3d3981f3363d3d373d3d3d363d73', - implementation.replace(/0x/, '').toLowerCase(), - '5af43d82803e903d91602b57fd5bf3', - ].join(''); + this.implementation.target, + '0x5af43d82803e903d91602b57fd5bf3', + ]); - expect(ethers.getCreate2Address(factory.address, salt, ethers.keccak256(creationCode))).to.be.equal(predicted); + 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); - expectEvent(await factory.$cloneDeterministic(implementation, salt), 'return$cloneDeterministic', { - instance: predicted, - }); + await expect(this.factory.$cloneDeterministic(this.implementation, salt)) + .to.emit(this.factory, 'return$cloneDeterministic') + .withArgs(predicted); }); }); }); diff --git a/test/proxy/ERC1967/ERC1967Proxy.test.js b/test/proxy/ERC1967/ERC1967Proxy.test.js index 81cc4350749..b22280046e6 100644 --- a/test/proxy/ERC1967/ERC1967Proxy.test.js +++ b/test/proxy/ERC1967/ERC1967Proxy.test.js @@ -1,12 +1,23 @@ +const { ethers } = require('hardhat'); + const shouldBehaveLikeProxy = require('../Proxy.behaviour'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const fixture = async () => { + const [nonContractAddress] = await ethers.getSigners(); + + const implementation = await ethers.deployContract('DummyImplementation'); + + const createProxy = (implementation, initData, opts) => + ethers.deployContract('ERC1967Proxy', [implementation, initData], opts); -const ERC1967Proxy = artifacts.require('ERC1967Proxy'); + return { nonContractAddress, implementation, createProxy }; +}; -contract('ERC1967Proxy', function (accounts) { - // `undefined`, `null` and other false-ish opts will not be forwarded. - const createProxy = async function (implementation, initData, opts) { - return ERC1967Proxy.new(implementation, initData, ...[opts].filter(Boolean)); - }; +describe('ERC1967Proxy', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); - shouldBehaveLikeProxy(createProxy, accounts); + shouldBehaveLikeProxy(); }); diff --git a/test/proxy/ERC1967/ERC1967Utils.test.js b/test/proxy/ERC1967/ERC1967Utils.test.js index 975b08d81a8..f733e297f58 100644 --- a/test/proxy/ERC1967/ERC1967Utils.test.js +++ b/test/proxy/ERC1967/ERC1967Utils.test.js @@ -1,70 +1,65 @@ -const { expectEvent, constants } = require('@openzeppelin/test-helpers'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const { getAddressInSlot, setSlot, ImplementationSlot, AdminSlot, BeaconSlot } = require('../../helpers/erc1967'); +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { ZERO_ADDRESS } = constants; +const { getAddressInSlot, setSlot, ImplementationSlot, AdminSlot, BeaconSlot } = require('../../helpers/erc1967'); -const ERC1967Utils = artifacts.require('$ERC1967Utils'); +async function fixture() { + const [, admin, anotherAccount] = await ethers.getSigners(); -const V1 = artifacts.require('DummyImplementation'); -const V2 = artifacts.require('CallReceiverMock'); -const UpgradeableBeaconMock = artifacts.require('UpgradeableBeaconMock'); -const UpgradeableBeaconReentrantMock = artifacts.require('UpgradeableBeaconReentrantMock'); + const utils = await ethers.deployContract('$ERC1967Utils'); + const v1 = await ethers.deployContract('DummyImplementation'); + const v2 = await ethers.deployContract('CallReceiverMock'); -contract('ERC1967Utils', function (accounts) { - const [, admin, anotherAccount] = accounts; - const EMPTY_DATA = '0x'; + return { admin, anotherAccount, utils, v1, v2 }; +} +describe('ERC1967Utils', function () { beforeEach('setup', async function () { - this.utils = await ERC1967Utils.new(); - this.v1 = await V1.new(); - this.v2 = await V2.new(); + Object.assign(this, await loadFixture(fixture)); }); describe('IMPLEMENTATION_SLOT', function () { beforeEach('set v1 implementation', async function () { - await setSlot(this.utils, ImplementationSlot, this.v1.address); + await setSlot(this.utils, ImplementationSlot, this.v1); }); describe('getImplementation', function () { it('returns current implementation and matches implementation slot value', async function () { - expect(await this.utils.$getImplementation()).to.equal(this.v1.address); - expect(await getAddressInSlot(this.utils.address, ImplementationSlot)).to.equal(this.v1.address); + expect(await this.utils.$getImplementation()).to.equal(this.v1.target); + expect(await getAddressInSlot(this.utils, ImplementationSlot)).to.equal(this.v1.target); }); }); describe('upgradeToAndCall', function () { it('sets implementation in storage and emits event', async function () { - const newImplementation = this.v2.address; - const receipt = await this.utils.$upgradeToAndCall(newImplementation, EMPTY_DATA); + const newImplementation = this.v2; + const tx = await this.utils.$upgradeToAndCall(newImplementation, '0x'); - expect(await getAddressInSlot(this.utils.address, ImplementationSlot)).to.equal(newImplementation); - expectEvent(receipt, 'Upgraded', { implementation: newImplementation }); + expect(await getAddressInSlot(this.utils, ImplementationSlot)).to.equal(newImplementation.target); + await expect(tx).to.emit(this.utils, 'Upgraded').withArgs(newImplementation.target); }); it('reverts when implementation does not contain code', async function () { - await expectRevertCustomError( - this.utils.$upgradeToAndCall(anotherAccount, EMPTY_DATA), - 'ERC1967InvalidImplementation', - [anotherAccount], - ); + await expect(this.utils.$upgradeToAndCall(this.anotherAccount, '0x')) + .to.be.revertedWithCustomError(this.utils, 'ERC1967InvalidImplementation') + .withArgs(this.anotherAccount.address); }); describe('when data is empty', function () { it('reverts when value is sent', async function () { - await expectRevertCustomError( - this.utils.$upgradeToAndCall(this.v2.address, EMPTY_DATA, { value: 1 }), + await expect(this.utils.$upgradeToAndCall(this.v2, '0x', { value: 1 })).to.be.revertedWithCustomError( + this.utils, 'ERC1967NonPayable', - [], ); }); }); describe('when data is not empty', function () { it('delegates a call to the new implementation', async function () { - const initializeData = this.v2.contract.methods.mockFunction().encodeABI(); - const receipt = await this.utils.$upgradeToAndCall(this.v2.address, initializeData); - await expectEvent.inTransaction(receipt.tx, await V2.at(this.utils.address), 'MockFunctionCalled'); + const initializeData = this.v2.interface.encodeFunctionData('mockFunction'); + const tx = await this.utils.$upgradeToAndCall(this.v2, initializeData); + await expect(tx).to.emit(await ethers.getContractAt('CallReceiverMock', this.utils), 'MockFunctionCalled'); }); }); }); @@ -72,99 +67,94 @@ contract('ERC1967Utils', function (accounts) { describe('ADMIN_SLOT', function () { beforeEach('set admin', async function () { - await setSlot(this.utils, AdminSlot, admin); + await setSlot(this.utils, AdminSlot, this.admin); }); describe('getAdmin', function () { it('returns current admin and matches admin slot value', async function () { - expect(await this.utils.$getAdmin()).to.equal(admin); - expect(await getAddressInSlot(this.utils.address, AdminSlot)).to.equal(admin); + expect(await this.utils.$getAdmin()).to.equal(this.admin.address); + expect(await getAddressInSlot(this.utils, AdminSlot)).to.equal(this.admin.address); }); }); describe('changeAdmin', function () { it('sets admin in storage and emits event', async function () { - const newAdmin = anotherAccount; - const receipt = await this.utils.$changeAdmin(newAdmin); + const newAdmin = this.anotherAccount; + const tx = await this.utils.$changeAdmin(newAdmin); - expect(await getAddressInSlot(this.utils.address, AdminSlot)).to.equal(newAdmin); - expectEvent(receipt, 'AdminChanged', { previousAdmin: admin, newAdmin: newAdmin }); + expect(await getAddressInSlot(this.utils, AdminSlot)).to.equal(newAdmin.address); + await expect(tx).to.emit(this.utils, 'AdminChanged').withArgs(this.admin.address, newAdmin.address); }); it('reverts when setting the address zero as admin', async function () { - await expectRevertCustomError(this.utils.$changeAdmin(ZERO_ADDRESS), 'ERC1967InvalidAdmin', [ZERO_ADDRESS]); + await expect(this.utils.$changeAdmin(ethers.ZeroAddress)) + .to.be.revertedWithCustomError(this.utils, 'ERC1967InvalidAdmin') + .withArgs(ethers.ZeroAddress); }); }); }); describe('BEACON_SLOT', function () { beforeEach('set beacon', async function () { - this.beacon = await UpgradeableBeaconMock.new(this.v1.address); - await setSlot(this.utils, BeaconSlot, this.beacon.address); + this.beacon = await ethers.deployContract('UpgradeableBeaconMock', [this.v1]); + await setSlot(this.utils, BeaconSlot, this.beacon); }); describe('getBeacon', function () { it('returns current beacon and matches beacon slot value', async function () { - expect(await this.utils.$getBeacon()).to.equal(this.beacon.address); - expect(await getAddressInSlot(this.utils.address, BeaconSlot)).to.equal(this.beacon.address); + expect(await this.utils.$getBeacon()).to.equal(this.beacon.target); + expect(await getAddressInSlot(this.utils, BeaconSlot)).to.equal(this.beacon.target); }); }); describe('upgradeBeaconToAndCall', function () { it('sets beacon in storage and emits event', async function () { - const newBeacon = await UpgradeableBeaconMock.new(this.v2.address); - const receipt = await this.utils.$upgradeBeaconToAndCall(newBeacon.address, EMPTY_DATA); + const newBeacon = await ethers.deployContract('UpgradeableBeaconMock', [this.v2]); + const tx = await this.utils.$upgradeBeaconToAndCall(newBeacon, '0x'); - expect(await getAddressInSlot(this.utils.address, BeaconSlot)).to.equal(newBeacon.address); - expectEvent(receipt, 'BeaconUpgraded', { beacon: newBeacon.address }); + expect(await getAddressInSlot(this.utils, BeaconSlot)).to.equal(newBeacon.target); + await expect(tx).to.emit(this.utils, 'BeaconUpgraded').withArgs(newBeacon.target); }); it('reverts when beacon does not contain code', async function () { - await expectRevertCustomError( - this.utils.$upgradeBeaconToAndCall(anotherAccount, EMPTY_DATA), - 'ERC1967InvalidBeacon', - [anotherAccount], - ); + await expect(this.utils.$upgradeBeaconToAndCall(this.anotherAccount, '0x')) + .to.be.revertedWithCustomError(this.utils, 'ERC1967InvalidBeacon') + .withArgs(this.anotherAccount.address); }); it("reverts when beacon's implementation does not contain code", async function () { - const newBeacon = await UpgradeableBeaconMock.new(anotherAccount); + const newBeacon = await ethers.deployContract('UpgradeableBeaconMock', [this.anotherAccount]); - await expectRevertCustomError( - this.utils.$upgradeBeaconToAndCall(newBeacon.address, EMPTY_DATA), - 'ERC1967InvalidImplementation', - [anotherAccount], - ); + await expect(this.utils.$upgradeBeaconToAndCall(newBeacon, '0x')) + .to.be.revertedWithCustomError(this.utils, 'ERC1967InvalidImplementation') + .withArgs(this.anotherAccount.address); }); describe('when data is empty', function () { it('reverts when value is sent', async function () { - const newBeacon = await UpgradeableBeaconMock.new(this.v2.address); - await expectRevertCustomError( - this.utils.$upgradeBeaconToAndCall(newBeacon.address, EMPTY_DATA, { value: 1 }), + const newBeacon = await ethers.deployContract('UpgradeableBeaconMock', [this.v2]); + await expect(this.utils.$upgradeBeaconToAndCall(newBeacon, '0x', { value: 1 })).to.be.revertedWithCustomError( + this.utils, 'ERC1967NonPayable', - [], ); }); }); describe('when data is not empty', function () { it('delegates a call to the new implementation', async function () { - const initializeData = this.v2.contract.methods.mockFunction().encodeABI(); - const newBeacon = await UpgradeableBeaconMock.new(this.v2.address); - const receipt = await this.utils.$upgradeBeaconToAndCall(newBeacon.address, initializeData); - await expectEvent.inTransaction(receipt.tx, await V2.at(this.utils.address), 'MockFunctionCalled'); + const initializeData = this.v2.interface.encodeFunctionData('mockFunction'); + const newBeacon = await ethers.deployContract('UpgradeableBeaconMock', [this.v2]); + const tx = await this.utils.$upgradeBeaconToAndCall(newBeacon, initializeData); + await expect(tx).to.emit(await ethers.getContractAt('CallReceiverMock', this.utils), 'MockFunctionCalled'); }); }); describe('reentrant beacon implementation() call', function () { it('sees the new beacon implementation', async function () { - const newBeacon = await UpgradeableBeaconReentrantMock.new(); - await expectRevertCustomError( - this.utils.$upgradeBeaconToAndCall(newBeacon.address, '0x'), - 'BeaconProxyBeaconSlotAddress', - [newBeacon.address], - ); + const newBeacon = await ethers.deployContract('UpgradeableBeaconReentrantMock'); + await expect(this.utils.$upgradeBeaconToAndCall(newBeacon, '0x')) + .to.be.revertedWithCustomError(newBeacon, 'BeaconProxyBeaconSlotAddress') + .withArgs(newBeacon.target); }); }); }); diff --git a/test/proxy/Proxy.behaviour.js b/test/proxy/Proxy.behaviour.js index acce6d188d9..84cd93b5114 100644 --- a/test/proxy/Proxy.behaviour.js +++ b/test/proxy/Proxy.behaviour.js @@ -1,100 +1,94 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); -const { getSlot, ImplementationSlot } = require('../helpers/erc1967'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { expectRevertCustomError } = require('../helpers/customError'); -const DummyImplementation = artifacts.require('DummyImplementation'); +const { getAddressInSlot, ImplementationSlot } = require('../helpers/erc1967'); -module.exports = function shouldBehaveLikeProxy(createProxy, accounts) { +module.exports = function shouldBehaveLikeProxy() { it('cannot be initialized with a non-contract address', async function () { - const nonContractAddress = accounts[0]; - const initializeData = Buffer.from(''); - await expectRevert.unspecified(createProxy(nonContractAddress, initializeData)); - }); - - before('deploy implementation', async function () { - this.implementation = web3.utils.toChecksumAddress((await DummyImplementation.new()).address); + const initializeData = '0x'; + await expect(this.createProxy(this.nonContractAddress, initializeData)) + .to.be.revertedWithCustomError(await ethers.getContractFactory('ERC1967Proxy'), 'ERC1967InvalidImplementation') + .withArgs(this.nonContractAddress.address); }); const assertProxyInitialization = function ({ value, balance }) { it('sets the implementation address', async function () { - const implementationSlot = await getSlot(this.proxy, ImplementationSlot); - const implementationAddress = web3.utils.toChecksumAddress(implementationSlot.substr(-40)); - expect(implementationAddress).to.be.equal(this.implementation); + expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.implementation.target); }); it('initializes the proxy', async function () { - const dummy = new DummyImplementation(this.proxy); - expect(await dummy.value()).to.be.bignumber.equal(value.toString()); + const dummy = this.implementation.attach(this.proxy); + expect(await dummy.value()).to.equal(value); }); it('has expected balance', async function () { - expect(await web3.eth.getBalance(this.proxy)).to.be.bignumber.equal(balance.toString()); + expect(await ethers.provider.getBalance(this.proxy)).to.equal(balance); }); }; describe('without initialization', function () { - const initializeData = Buffer.from(''); + const initializeData = '0x'; describe('when not sending balance', function () { beforeEach('creating proxy', async function () { - this.proxy = (await createProxy(this.implementation, initializeData)).address; + this.proxy = await this.createProxy(this.implementation, initializeData); }); - assertProxyInitialization({ value: 0, balance: 0 }); + assertProxyInitialization({ value: 0n, balance: 0n }); }); describe('when sending some balance', function () { - const value = 10e5; + const value = 10n ** 5n; it('reverts', async function () { - await expectRevertCustomError( - createProxy(this.implementation, initializeData, { value }), - 'ERC1967NonPayable', - [], - ); + await expect(this.createProxy(this.implementation, initializeData, { value })).to.be.reverted; }); }); }); describe('initialization without parameters', function () { describe('non payable', function () { - const expectedInitializedValue = 10; - const initializeData = new DummyImplementation('').contract.methods['initializeNonPayable()']().encodeABI(); + const expectedInitializedValue = 10n; + + beforeEach(function () { + this.initializeData = this.implementation.interface.encodeFunctionData('initializeNonPayable'); + }); describe('when not sending balance', function () { beforeEach('creating proxy', async function () { - this.proxy = (await createProxy(this.implementation, initializeData)).address; + this.proxy = await this.createProxy(this.implementation, this.initializeData); }); assertProxyInitialization({ value: expectedInitializedValue, - balance: 0, + balance: 0n, }); }); describe('when sending some balance', function () { - const value = 10e5; + const value = 10n ** 5n; it('reverts', async function () { - await expectRevert.unspecified(createProxy(this.implementation, initializeData, { value })); + await expect(this.createProxy(this.implementation, this.initializeData, { value })).to.be.reverted; }); }); }); describe('payable', function () { - const expectedInitializedValue = 100; - const initializeData = new DummyImplementation('').contract.methods['initializePayable()']().encodeABI(); + const expectedInitializedValue = 100n; + + beforeEach(function () { + this.initializeData = this.implementation.interface.encodeFunctionData('initializePayable'); + }); describe('when not sending balance', function () { beforeEach('creating proxy', async function () { - this.proxy = (await createProxy(this.implementation, initializeData)).address; + this.proxy = await this.createProxy(this.implementation, this.initializeData); }); assertProxyInitialization({ value: expectedInitializedValue, - balance: 0, + balance: 0n, }); }); @@ -102,7 +96,7 @@ module.exports = function shouldBehaveLikeProxy(createProxy, accounts) { const value = 10e5; beforeEach('creating proxy', async function () { - this.proxy = (await createProxy(this.implementation, initializeData, { value })).address; + this.proxy = await this.createProxy(this.implementation, this.initializeData, { value }); }); assertProxyInitialization({ @@ -115,14 +109,17 @@ module.exports = function shouldBehaveLikeProxy(createProxy, accounts) { describe('initialization with parameters', function () { describe('non payable', function () { - const expectedInitializedValue = 10; - const initializeData = new DummyImplementation('').contract.methods - .initializeNonPayableWithValue(expectedInitializedValue) - .encodeABI(); + const expectedInitializedValue = 10n; + + beforeEach(function () { + this.initializeData = this.implementation.interface.encodeFunctionData('initializeNonPayableWithValue', [ + expectedInitializedValue, + ]); + }); describe('when not sending balance', function () { beforeEach('creating proxy', async function () { - this.proxy = (await createProxy(this.implementation, initializeData)).address; + this.proxy = await this.createProxy(this.implementation, this.initializeData); }); assertProxyInitialization({ @@ -135,33 +132,36 @@ module.exports = function shouldBehaveLikeProxy(createProxy, accounts) { const value = 10e5; it('reverts', async function () { - await expectRevert.unspecified(createProxy(this.implementation, initializeData, { value })); + await expect(this.createProxy(this.implementation, this.initializeData, { value })).to.be.reverted; }); }); }); describe('payable', function () { - const expectedInitializedValue = 42; - const initializeData = new DummyImplementation('').contract.methods - .initializePayableWithValue(expectedInitializedValue) - .encodeABI(); + const expectedInitializedValue = 42n; + + beforeEach(function () { + this.initializeData = this.implementation.interface.encodeFunctionData('initializePayableWithValue', [ + expectedInitializedValue, + ]); + }); describe('when not sending balance', function () { beforeEach('creating proxy', async function () { - this.proxy = (await createProxy(this.implementation, initializeData)).address; + this.proxy = await this.createProxy(this.implementation, this.initializeData); }); assertProxyInitialization({ value: expectedInitializedValue, - balance: 0, + balance: 0n, }); }); describe('when sending some balance', function () { - const value = 10e5; + const value = 10n ** 5n; beforeEach('creating proxy', async function () { - this.proxy = (await createProxy(this.implementation, initializeData, { value })).address; + this.proxy = await this.createProxy(this.implementation, this.initializeData, { value }); }); assertProxyInitialization({ @@ -172,10 +172,12 @@ module.exports = function shouldBehaveLikeProxy(createProxy, accounts) { }); describe('reverting initialization', function () { - const initializeData = new DummyImplementation('').contract.methods.reverts().encodeABI(); + beforeEach(function () { + this.initializeData = this.implementation.interface.encodeFunctionData('reverts'); + }); it('reverts', async function () { - await expectRevert(createProxy(this.implementation, initializeData), 'DummyImplementation reverted'); + await expect(this.createProxy(this.implementation, this.initializeData)).to.be.reverted; }); }); }); diff --git a/test/proxy/beacon/BeaconProxy.test.js b/test/proxy/beacon/BeaconProxy.test.js index d583d0ffbf1..66856ac0896 100644 --- a/test/proxy/beacon/BeaconProxy.test.js +++ b/test/proxy/beacon/BeaconProxy.test.js @@ -1,152 +1,138 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); -const { getSlot, BeaconSlot } = require('../../helpers/erc1967'); +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { getAddressInSlot, BeaconSlot } = require('../../helpers/erc1967'); -const { expectRevertCustomError } = require('../../helpers/customError'); +async function fixture() { + const [admin, other] = await ethers.getSigners(); -const { expect } = require('chai'); + const v1 = await ethers.deployContract('DummyImplementation'); + const v2 = await ethers.deployContract('DummyImplementationV2'); + const factory = await ethers.getContractFactory('BeaconProxy'); + const beacon = await ethers.deployContract('UpgradeableBeacon', [v1, admin]); + + const newBeaconProxy = (beacon, data, opts = {}) => factory.deploy(beacon, data, opts); -const UpgradeableBeacon = artifacts.require('UpgradeableBeacon'); -const BeaconProxy = artifacts.require('BeaconProxy'); -const DummyImplementation = artifacts.require('DummyImplementation'); -const DummyImplementationV2 = artifacts.require('DummyImplementationV2'); -const BadBeaconNoImpl = artifacts.require('BadBeaconNoImpl'); -const BadBeaconNotContract = artifacts.require('BadBeaconNotContract'); + return { admin, other, factory, beacon, v1, v2, newBeaconProxy }; +} -contract('BeaconProxy', function (accounts) { - const [upgradeableBeaconAdmin, anotherAccount] = accounts; +describe('BeaconProxy', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); describe('bad beacon is not accepted', async function () { it('non-contract beacon', async function () { - await expectRevertCustomError(BeaconProxy.new(anotherAccount, '0x'), 'ERC1967InvalidBeacon', [anotherAccount]); + const notBeacon = this.other; + + await expect(this.newBeaconProxy(notBeacon, '0x')) + .to.be.revertedWithCustomError(this.factory, 'ERC1967InvalidBeacon') + .withArgs(notBeacon.address); }); it('non-compliant beacon', async function () { - const beacon = await BadBeaconNoImpl.new(); - await expectRevert.unspecified(BeaconProxy.new(beacon.address, '0x')); + const badBeacon = await ethers.deployContract('BadBeaconNoImpl'); + + await expect(this.newBeaconProxy(badBeacon, '0x')).to.be.revertedWithoutReason; }); it('non-contract implementation', async function () { - const beacon = await BadBeaconNotContract.new(); - const implementation = await beacon.implementation(); - await expectRevertCustomError(BeaconProxy.new(beacon.address, '0x'), 'ERC1967InvalidImplementation', [ - implementation, - ]); - }); - }); + const badBeacon = await ethers.deployContract('BadBeaconNotContract'); - before('deploy implementation', async function () { - this.implementationV0 = await DummyImplementation.new(); - this.implementationV1 = await DummyImplementationV2.new(); + await expect(this.newBeaconProxy(badBeacon, '0x')) + .to.be.revertedWithCustomError(this.factory, 'ERC1967InvalidImplementation') + .withArgs(await badBeacon.implementation()); + }); }); describe('initialization', function () { - before(function () { - this.assertInitialized = async ({ value, balance }) => { - const beaconSlot = await getSlot(this.proxy, BeaconSlot); - const beaconAddress = web3.utils.toChecksumAddress(beaconSlot.substr(-40)); - expect(beaconAddress).to.equal(this.beacon.address); + async function assertInitialized({ value, balance }) { + const beaconAddress = await getAddressInSlot(this.proxy, BeaconSlot); + expect(beaconAddress).to.equal(this.beacon.target); - const dummy = new DummyImplementation(this.proxy.address); - expect(await dummy.value()).to.bignumber.eq(value); - - expect(await web3.eth.getBalance(this.proxy.address)).to.bignumber.eq(balance); - }; - }); + const dummy = this.v1.attach(this.proxy); + expect(await dummy.value()).to.equal(value); - beforeEach('deploy beacon', async function () { - this.beacon = await UpgradeableBeacon.new(this.implementationV0.address, upgradeableBeaconAdmin); - }); + expect(await ethers.provider.getBalance(this.proxy)).to.equal(balance); + } it('no initialization', async function () { - const data = Buffer.from(''); - this.proxy = await BeaconProxy.new(this.beacon.address, data); - await this.assertInitialized({ value: '0', balance: '0' }); + this.proxy = await this.newBeaconProxy(this.beacon, '0x'); + await assertInitialized.bind(this)({ value: 0n, balance: 0n }); }); it('non-payable initialization', async function () { - const value = '55'; - const data = this.implementationV0.contract.methods.initializeNonPayableWithValue(value).encodeABI(); - this.proxy = await BeaconProxy.new(this.beacon.address, data); - await this.assertInitialized({ value, balance: '0' }); + const value = 55n; + const data = this.v1.interface.encodeFunctionData('initializeNonPayableWithValue', [value]); + + this.proxy = await this.newBeaconProxy(this.beacon, data); + await assertInitialized.bind(this)({ value, balance: 0n }); }); it('payable initialization', async function () { - const value = '55'; - const data = this.implementationV0.contract.methods.initializePayableWithValue(value).encodeABI(); - const balance = '100'; - this.proxy = await BeaconProxy.new(this.beacon.address, data, { value: balance }); - await this.assertInitialized({ value, balance }); + const value = 55n; + const data = this.v1.interface.encodeFunctionData('initializePayableWithValue', [value]); + const balance = 100n; + + this.proxy = await this.newBeaconProxy(this.beacon, data, { value: balance }); + await assertInitialized.bind(this)({ value, balance }); }); it('reverting initialization due to value', async function () { - const data = Buffer.from(''); - await expectRevertCustomError( - BeaconProxy.new(this.beacon.address, data, { value: '1' }), + await expect(this.newBeaconProxy(this.beacon, '0x', { value: 1n })).to.be.revertedWithCustomError( + this.factory, 'ERC1967NonPayable', - [], ); }); it('reverting initialization function', async function () { - const data = this.implementationV0.contract.methods.reverts().encodeABI(); - await expectRevert(BeaconProxy.new(this.beacon.address, data), 'DummyImplementation reverted'); + const data = this.v1.interface.encodeFunctionData('reverts'); + await expect(this.newBeaconProxy(this.beacon, data)).to.be.revertedWith('DummyImplementation reverted'); }); }); - it('upgrade a proxy by upgrading its beacon', async function () { - const beacon = await UpgradeableBeacon.new(this.implementationV0.address, upgradeableBeaconAdmin); + describe('upgrade', async function () { + it('upgrade a proxy by upgrading its beacon', async function () { + const value = 10n; + const data = this.v1.interface.encodeFunctionData('initializeNonPayableWithValue', [value]); + const proxy = await this.newBeaconProxy(this.beacon, data).then(instance => this.v1.attach(instance)); - const value = '10'; - const data = this.implementationV0.contract.methods.initializeNonPayableWithValue(value).encodeABI(); - const proxy = await BeaconProxy.new(beacon.address, data); + // test initial values + expect(await proxy.value()).to.equal(value); - const dummy = new DummyImplementation(proxy.address); + // test initial version + expect(await proxy.version()).to.equal('V1'); - // test initial values - expect(await dummy.value()).to.bignumber.eq(value); + // upgrade beacon + await this.beacon.connect(this.admin).upgradeTo(this.v2); - // test initial version - expect(await dummy.version()).to.eq('V1'); - - // upgrade beacon - await beacon.upgradeTo(this.implementationV1.address, { from: upgradeableBeaconAdmin }); - - // test upgraded version - expect(await dummy.version()).to.eq('V2'); - }); - - it('upgrade 2 proxies by upgrading shared beacon', async function () { - const value1 = '10'; - const value2 = '42'; - - const beacon = await UpgradeableBeacon.new(this.implementationV0.address, upgradeableBeaconAdmin); - - const proxy1InitializeData = this.implementationV0.contract.methods - .initializeNonPayableWithValue(value1) - .encodeABI(); - const proxy1 = await BeaconProxy.new(beacon.address, proxy1InitializeData); + // test upgraded version + expect(await proxy.version()).to.equal('V2'); + }); - const proxy2InitializeData = this.implementationV0.contract.methods - .initializeNonPayableWithValue(value2) - .encodeABI(); - const proxy2 = await BeaconProxy.new(beacon.address, proxy2InitializeData); + it('upgrade 2 proxies by upgrading shared beacon', async function () { + const value1 = 10n; + const data1 = this.v1.interface.encodeFunctionData('initializeNonPayableWithValue', [value1]); + const proxy1 = await this.newBeaconProxy(this.beacon, data1).then(instance => this.v1.attach(instance)); - const dummy1 = new DummyImplementation(proxy1.address); - const dummy2 = new DummyImplementation(proxy2.address); + const value2 = 42n; + const data2 = this.v1.interface.encodeFunctionData('initializeNonPayableWithValue', [value2]); + const proxy2 = await this.newBeaconProxy(this.beacon, data2).then(instance => this.v1.attach(instance)); - // test initial values - expect(await dummy1.value()).to.bignumber.eq(value1); - expect(await dummy2.value()).to.bignumber.eq(value2); + // test initial values + expect(await proxy1.value()).to.equal(value1); + expect(await proxy2.value()).to.equal(value2); - // test initial version - expect(await dummy1.version()).to.eq('V1'); - expect(await dummy2.version()).to.eq('V1'); + // test initial version + expect(await proxy1.version()).to.equal('V1'); + expect(await proxy2.version()).to.equal('V1'); - // upgrade beacon - await beacon.upgradeTo(this.implementationV1.address, { from: upgradeableBeaconAdmin }); + // upgrade beacon + await this.beacon.connect(this.admin).upgradeTo(this.v2); - // test upgraded version - expect(await dummy1.version()).to.eq('V2'); - expect(await dummy2.version()).to.eq('V2'); + // test upgraded version + expect(await proxy1.version()).to.equal('V2'); + expect(await proxy2.version()).to.equal('V2'); + }); }); }); diff --git a/test/proxy/beacon/UpgradeableBeacon.test.js b/test/proxy/beacon/UpgradeableBeacon.test.js index 0737f6fdfe6..be6aca754f0 100644 --- a/test/proxy/beacon/UpgradeableBeacon.test.js +++ b/test/proxy/beacon/UpgradeableBeacon.test.js @@ -1,54 +1,55 @@ -const { expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { expectRevertCustomError } = require('../../helpers/customError'); +async function fixture() { + const [admin, other] = await ethers.getSigners(); -const UpgradeableBeacon = artifacts.require('UpgradeableBeacon'); -const Implementation1 = artifacts.require('Implementation1'); -const Implementation2 = artifacts.require('Implementation2'); + const v1 = await ethers.deployContract('Implementation1'); + const v2 = await ethers.deployContract('Implementation2'); + const beacon = await ethers.deployContract('UpgradeableBeacon', [v1, admin]); -contract('UpgradeableBeacon', function (accounts) { - const [owner, other] = accounts; + return { admin, other, beacon, v1, v2 }; +} - it('cannot be created with non-contract implementation', async function () { - await expectRevertCustomError(UpgradeableBeacon.new(other, owner), 'BeaconInvalidImplementation', [other]); +describe('UpgradeableBeacon', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); }); - context('once deployed', async function () { - beforeEach('deploying beacon', async function () { - this.v1 = await Implementation1.new(); - this.beacon = await UpgradeableBeacon.new(this.v1.address, owner); - }); + it('cannot be created with non-contract implementation', async function () { + await expect(ethers.deployContract('UpgradeableBeacon', [this.other, this.admin])) + .to.be.revertedWithCustomError(this.beacon, 'BeaconInvalidImplementation') + .withArgs(this.other.address); + }); + describe('once deployed', async function () { it('emits Upgraded event to the first implementation', async function () { - const beacon = await UpgradeableBeacon.new(this.v1.address, owner); - await expectEvent.inTransaction(beacon.contract.transactionHash, beacon, 'Upgraded', { - implementation: this.v1.address, - }); + await expect(this.beacon.deploymentTransaction()).to.emit(this.beacon, 'Upgraded').withArgs(this.v1.target); }); it('returns implementation', async function () { - expect(await this.beacon.implementation()).to.equal(this.v1.address); + expect(await this.beacon.implementation()).to.equal(this.v1.target); }); - it('can be upgraded by the owner', async function () { - const v2 = await Implementation2.new(); - const receipt = await this.beacon.upgradeTo(v2.address, { from: owner }); - expectEvent(receipt, 'Upgraded', { implementation: v2.address }); - expect(await this.beacon.implementation()).to.equal(v2.address); + it('can be upgraded by the admin', async function () { + await expect(this.beacon.connect(this.admin).upgradeTo(this.v2)) + .to.emit(this.beacon, 'Upgraded') + .withArgs(this.v2.target); + + expect(await this.beacon.implementation()).to.equal(this.v2.target); }); it('cannot be upgraded to a non-contract', async function () { - await expectRevertCustomError(this.beacon.upgradeTo(other, { from: owner }), 'BeaconInvalidImplementation', [ - other, - ]); + await expect(this.beacon.connect(this.admin).upgradeTo(this.other)) + .to.be.revertedWithCustomError(this.beacon, 'BeaconInvalidImplementation') + .withArgs(this.other.address); }); it('cannot be upgraded by other account', async function () { - const v2 = await Implementation2.new(); - await expectRevertCustomError(this.beacon.upgradeTo(v2.address, { from: other }), 'OwnableUnauthorizedAccount', [ - other, - ]); + await expect(this.beacon.connect(this.other).upgradeTo(this.v2)) + .to.be.revertedWithCustomError(this.beacon, 'OwnableUnauthorizedAccount') + .withArgs(this.other.address); }); }); }); diff --git a/test/proxy/transparent/ProxyAdmin.test.js b/test/proxy/transparent/ProxyAdmin.test.js index a3122eae334..9f137536a33 100644 --- a/test/proxy/transparent/ProxyAdmin.test.js +++ b/test/proxy/transparent/ProxyAdmin.test.js @@ -1,36 +1,33 @@ const { ethers } = require('hardhat'); -const { expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); -const ImplV1 = artifacts.require('DummyImplementation'); -const ImplV2 = artifacts.require('DummyImplementationV2'); -const ProxyAdmin = artifacts.require('ProxyAdmin'); -const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy'); -const ITransparentUpgradeableProxy = artifacts.require('ITransparentUpgradeableProxy'); - +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { getAddressInSlot, ImplementationSlot } = require('../../helpers/erc1967'); -const { expectRevertCustomError } = require('../../helpers/customError'); -contract('ProxyAdmin', function (accounts) { - const [proxyAdminOwner, anotherAccount] = accounts; +async function fixture() { + const [admin, other] = await ethers.getSigners(); - before('set implementations', async function () { - this.implementationV1 = await ImplV1.new(); - this.implementationV2 = await ImplV2.new(); - }); + const v1 = await ethers.deployContract('DummyImplementation'); + const v2 = await ethers.deployContract('DummyImplementationV2'); - beforeEach(async function () { - const initializeData = Buffer.from(''); - const proxy = await TransparentUpgradeableProxy.new(this.implementationV1.address, proxyAdminOwner, initializeData); + const proxy = await ethers + .deployContract('TransparentUpgradeableProxy', [v1, admin, '0x']) + .then(instance => ethers.getContractAt('ITransparentUpgradeableProxy', instance)); + + const proxyAdmin = await ethers.getContractAt( + 'ProxyAdmin', + ethers.getCreateAddress({ from: proxy.target, nonce: 1n }), + ); - const proxyNonce = await web3.eth.getTransactionCount(proxy.address); - const proxyAdminAddress = ethers.getCreateAddress({ from: proxy.address, nonce: proxyNonce - 1 }); // Nonce already used - this.proxyAdmin = await ProxyAdmin.at(proxyAdminAddress); + return { admin, other, v1, v2, proxy, proxyAdmin }; +} - this.proxy = await ITransparentUpgradeableProxy.at(proxy.address); +describe('ProxyAdmin', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); }); it('has an owner', async function () { - expect(await this.proxyAdmin.owner()).to.equal(proxyAdminOwner); + expect(await this.proxyAdmin.owner()).to.equal(this.admin.address); }); it('has an interface version', async function () { @@ -40,24 +37,16 @@ contract('ProxyAdmin', function (accounts) { describe('without data', function () { context('with unauthorized account', function () { it('fails to upgrade', async function () { - await expectRevertCustomError( - this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, '0x', { - from: anotherAccount, - }), - 'OwnableUnauthorizedAccount', - [anotherAccount], - ); + await expect(this.proxyAdmin.connect(this.other).upgradeAndCall(this.proxy, this.v2, '0x')) + .to.be.revertedWithCustomError(this.proxyAdmin, 'OwnableUnauthorizedAccount') + .withArgs(this.other.address); }); }); context('with authorized account', function () { it('upgrades implementation', async function () { - await this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, '0x', { - from: proxyAdminOwner, - }); - - const implementationAddress = await getAddressInSlot(this.proxy, ImplementationSlot); - expect(implementationAddress).to.be.equal(this.implementationV2.address); + await this.proxyAdmin.connect(this.admin).upgradeAndCall(this.proxy, this.v2, '0x'); + expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.be.equal(this.v2.target); }); }); }); @@ -65,37 +54,26 @@ contract('ProxyAdmin', function (accounts) { describe('with data', function () { context('with unauthorized account', function () { it('fails to upgrade', async function () { - const callData = new ImplV1('').contract.methods.initializeNonPayableWithValue(1337).encodeABI(); - await expectRevertCustomError( - this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, { - from: anotherAccount, - }), - 'OwnableUnauthorizedAccount', - [anotherAccount], - ); + const data = this.v1.interface.encodeFunctionData('initializeNonPayableWithValue', [1337n]); + await expect(this.proxyAdmin.connect(this.other).upgradeAndCall(this.proxy, this.v2, data)) + .to.be.revertedWithCustomError(this.proxyAdmin, 'OwnableUnauthorizedAccount') + .withArgs(this.other.address); }); }); context('with authorized account', function () { context('with invalid callData', function () { it('fails to upgrade', async function () { - const callData = '0x12345678'; - await expectRevert.unspecified( - this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, { - from: proxyAdminOwner, - }), - ); + const data = '0x12345678'; + await expect(this.proxyAdmin.connect(this.admin).upgradeAndCall(this.proxy, this.v2, data)).to.be.reverted; }); }); context('with valid callData', function () { it('upgrades implementation', async function () { - const callData = new ImplV1('').contract.methods.initializeNonPayableWithValue(1337).encodeABI(); - await this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, { - from: proxyAdminOwner, - }); - const implementationAddress = await getAddressInSlot(this.proxy, ImplementationSlot); - expect(implementationAddress).to.be.equal(this.implementationV2.address); + const data = this.v2.interface.encodeFunctionData('initializeNonPayableWithValue', [1337n]); + await this.proxyAdmin.connect(this.admin).upgradeAndCall(this.proxy, this.v2, data); + expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.be.equal(this.v2.target); }); }); }); diff --git a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js index da4d992872b..02122819987 100644 --- a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js +++ b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js @@ -1,261 +1,223 @@ -const { BN, expectRevert, expectEvent, constants } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS } = constants; -const { getAddressInSlot, ImplementationSlot, AdminSlot } = require('../../helpers/erc1967'); -const { expectRevertCustomError } = require('../../helpers/customError'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { ethers, web3 } = require('hardhat'); + +const { getAddressInSlot, ImplementationSlot, AdminSlot } = require('../../helpers/erc1967'); const { impersonate } = require('../../helpers/account'); -const Implementation1 = artifacts.require('Implementation1'); -const Implementation2 = artifacts.require('Implementation2'); -const Implementation3 = artifacts.require('Implementation3'); -const Implementation4 = artifacts.require('Implementation4'); -const MigratableMockV1 = artifacts.require('MigratableMockV1'); -const MigratableMockV2 = artifacts.require('MigratableMockV2'); -const MigratableMockV3 = artifacts.require('MigratableMockV3'); -const InitializableMock = artifacts.require('InitializableMock'); -const DummyImplementation = artifacts.require('DummyImplementation'); -const ClashingImplementation = artifacts.require('ClashingImplementation'); -const Ownable = artifacts.require('Ownable'); - -module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProxy, initialOwner, accounts) { - const [anotherAccount] = accounts; - - async function createProxyWithImpersonatedProxyAdmin(logic, initData, opts = undefined) { - const proxy = await createProxy(logic, initData, opts); - - // Expect proxy admin to be the first and only contract created by the proxy - const proxyAdminAddress = ethers.getCreateAddress({ from: proxy.address, nonce: 1 }); - await impersonate(proxyAdminAddress); - - return { - proxy, - proxyAdminAddress, +// createProxy, initialOwner, accounts +module.exports = function shouldBehaveLikeTransparentUpgradeableProxy() { + before(async function () { + const implementationV0 = await ethers.deployContract('DummyImplementation'); + const implementationV1 = await ethers.deployContract('DummyImplementation'); + + const createProxyWithImpersonatedProxyAdmin = async (logic, initData, opts = undefined) => { + const [proxy, tx] = await this.createProxy(logic, initData, opts).then(instance => + Promise.all([ethers.getContractAt('ITransparentUpgradeableProxy', instance), instance.deploymentTransaction()]), + ); + + const proxyAdmin = await ethers.getContractAt( + 'ProxyAdmin', + ethers.getCreateAddress({ from: proxy.target, nonce: 1n }), + ); + const proxyAdminAsSigner = await proxyAdmin.getAddress().then(impersonate); + + return { + instance: logic.attach(proxy.target), // attaching proxy directly works well for everything except for event resolution + proxy, + proxyAdmin, + proxyAdminAsSigner, + tx, + }; }; - } - before(async function () { - this.implementationV0 = (await DummyImplementation.new()).address; - this.implementationV1 = (await DummyImplementation.new()).address; + Object.assign(this, { + implementationV0, + implementationV1, + createProxyWithImpersonatedProxyAdmin, + }); }); beforeEach(async function () { - const initializeData = Buffer.from(''); - const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin( - this.implementationV0, - initializeData, - ); - this.proxy = proxy; - this.proxyAdminAddress = proxyAdminAddress; + Object.assign(this, await this.createProxyWithImpersonatedProxyAdmin(this.implementationV0, '0x')); }); describe('implementation', function () { it('returns the current implementation address', async function () { - const implementationAddress = await getAddressInSlot(this.proxy, ImplementationSlot); - expect(implementationAddress).to.be.equal(this.implementationV0); + expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.implementationV0.target); }); it('delegates to the implementation', async function () { - const dummy = new DummyImplementation(this.proxy.address); - const value = await dummy.get(); - - expect(value).to.equal(true); + expect(await this.instance.get()).to.be.true; }); }); describe('proxy admin', function () { it('emits AdminChanged event during construction', async function () { - await expectEvent.inConstruction(this.proxy, 'AdminChanged', { - previousAdmin: ZERO_ADDRESS, - newAdmin: this.proxyAdminAddress, - }); + await expect(this.tx).to.emit(this.proxy, 'AdminChanged').withArgs(ethers.ZeroAddress, this.proxyAdmin.target); }); it('sets the proxy admin in storage with the correct initial owner', async function () { - expect(await getAddressInSlot(this.proxy, AdminSlot)).to.be.equal(this.proxyAdminAddress); - const proxyAdmin = await Ownable.at(this.proxyAdminAddress); - expect(await proxyAdmin.owner()).to.be.equal(initialOwner); + expect(await getAddressInSlot(this.proxy, AdminSlot)).to.equal(this.proxyAdmin.target); + + expect(await this.proxyAdmin.owner()).to.equal(this.owner.address); }); it('can overwrite the admin by the implementation', async function () { - const dummy = new DummyImplementation(this.proxy.address); - await dummy.unsafeOverrideAdmin(anotherAccount); + await this.instance.unsafeOverrideAdmin(this.other); + const ERC1967AdminSlotValue = await getAddressInSlot(this.proxy, AdminSlot); - expect(ERC1967AdminSlotValue).to.be.equal(anotherAccount); + expect(ERC1967AdminSlotValue).to.equal(this.other.address); + expect(ERC1967AdminSlotValue).to.not.equal(this.proxyAdmin.address); // Still allows previous admin to execute admin operations - expect(ERC1967AdminSlotValue).to.not.equal(this.proxyAdminAddress); - expectEvent( - await this.proxy.upgradeToAndCall(this.implementationV1, '0x', { from: this.proxyAdminAddress }), - 'Upgraded', - { - implementation: this.implementationV1, - }, - ); + await expect(this.proxy.connect(this.proxyAdminAsSigner).upgradeToAndCall(this.implementationV1, '0x')) + .to.emit(this.proxy, 'Upgraded') + .withArgs(this.implementationV1.target); }); }); describe('upgradeToAndCall', function () { describe('without migrations', function () { beforeEach(async function () { - this.behavior = await InitializableMock.new(); + this.behavior = await ethers.deployContract('InitializableMock'); }); describe('when the call does not fail', function () { - const initializeData = new InitializableMock('').contract.methods['initializeWithX(uint256)'](42).encodeABI(); + beforeEach(function () { + this.initializeData = this.behavior.interface.encodeFunctionData('initializeWithX', [42n]); + }); describe('when the sender is the admin', function () { - const value = 1e5; + const value = 10n ** 5n; beforeEach(async function () { - this.receipt = await this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { - from: this.proxyAdminAddress, - value, - }); + this.tx = await this.proxy + .connect(this.proxyAdminAsSigner) + .upgradeToAndCall(this.behavior, this.initializeData, { + value, + }); }); it('upgrades to the requested implementation', async function () { - const implementationAddress = await getAddressInSlot(this.proxy, ImplementationSlot); - expect(implementationAddress).to.be.equal(this.behavior.address); + expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behavior.target); }); - it('emits an event', function () { - expectEvent(this.receipt, 'Upgraded', { implementation: this.behavior.address }); + it('emits an event', async function () { + await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behavior.target); }); it('calls the initializer function', async function () { - const migratable = new InitializableMock(this.proxy.address); - const x = await migratable.x(); - expect(x).to.be.bignumber.equal('42'); + expect(await this.behavior.attach(this.proxy).x()).to.equal(42n); }); it('sends given value to the proxy', async function () { - const balance = await web3.eth.getBalance(this.proxy.address); - expect(balance.toString()).to.be.bignumber.equal(value.toString()); + expect(await ethers.provider.getBalance(this.proxy)).to.equal(value); }); it('uses the storage of the proxy', async function () { // storage layout should look as follows: // - 0: Initializable storage ++ initializerRan ++ onlyInitializingRan // - 1: x - const storedValue = await web3.eth.getStorageAt(this.proxy.address, 1); - expect(parseInt(storedValue)).to.eq(42); + expect(await ethers.provider.getStorage(this.proxy, 1n)).to.equal(42n); }); }); describe('when the sender is not the admin', function () { it('reverts', async function () { - await expectRevert.unspecified( - this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { from: anotherAccount }), - ); + await expect(this.proxy.connect(this.other).upgradeToAndCall(this.behavior, this.initializeData)).to.be + .reverted; }); }); }); describe('when the call does fail', function () { - const initializeData = new InitializableMock('').contract.methods.fail().encodeABI(); + beforeEach(function () { + this.initializeData = this.behavior.interface.encodeFunctionData('fail'); + }); it('reverts', async function () { - await expectRevert.unspecified( - this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { from: this.proxyAdminAddress }), - ); + await expect(this.proxy.connect(this.proxyAdminAsSigner).upgradeToAndCall(this.behavior, this.initializeData)) + .to.be.reverted; }); }); }); describe('with migrations', function () { describe('when the sender is the admin', function () { - const value = 1e5; + const value = 10n ** 5n; describe('when upgrading to V1', function () { - const v1MigrationData = new MigratableMockV1('').contract.methods.initialize(42).encodeABI(); - beforeEach(async function () { - this.behaviorV1 = await MigratableMockV1.new(); - this.balancePreviousV1 = new BN(await web3.eth.getBalance(this.proxy.address)); - this.receipt = await this.proxy.upgradeToAndCall(this.behaviorV1.address, v1MigrationData, { - from: this.proxyAdminAddress, - value, - }); + this.behaviorV1 = await ethers.deployContract('MigratableMockV1'); + const v1MigrationData = this.behaviorV1.interface.encodeFunctionData('initialize', [42n]); + + this.balancePreviousV1 = await ethers.provider.getBalance(this.proxy); + this.tx = await this.proxy + .connect(this.proxyAdminAsSigner) + .upgradeToAndCall(this.behaviorV1, v1MigrationData, { + value, + }); }); it('upgrades to the requested version and emits an event', async function () { - const implementation = await getAddressInSlot(this.proxy, ImplementationSlot); - expect(implementation).to.be.equal(this.behaviorV1.address); - expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV1.address }); + expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behaviorV1.target); + + await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV1.target); }); it("calls the 'initialize' function and sends given value to the proxy", async function () { - const migratable = new MigratableMockV1(this.proxy.address); - - const x = await migratable.x(); - expect(x).to.be.bignumber.equal('42'); - - const balance = await web3.eth.getBalance(this.proxy.address); - expect(new BN(balance)).to.be.bignumber.equal(this.balancePreviousV1.addn(value)); + expect(await this.behaviorV1.attach(this.proxy).x()).to.equal(42n); + expect(await ethers.provider.getBalance(this.proxy)).to.equal(this.balancePreviousV1 + value); }); describe('when upgrading to V2', function () { - const v2MigrationData = new MigratableMockV2('').contract.methods.migrate(10, 42).encodeABI(); - beforeEach(async function () { - this.behaviorV2 = await MigratableMockV2.new(); - this.balancePreviousV2 = new BN(await web3.eth.getBalance(this.proxy.address)); - this.receipt = await this.proxy.upgradeToAndCall(this.behaviorV2.address, v2MigrationData, { - from: this.proxyAdminAddress, - value, - }); + this.behaviorV2 = await ethers.deployContract('MigratableMockV2'); + const v2MigrationData = this.behaviorV2.interface.encodeFunctionData('migrate', [10n, 42n]); + + this.balancePreviousV2 = await ethers.provider.getBalance(this.proxy); + this.tx = await this.proxy + .connect(this.proxyAdminAsSigner) + .upgradeToAndCall(this.behaviorV2, v2MigrationData, { + value, + }); }); it('upgrades to the requested version and emits an event', async function () { - const implementation = await getAddressInSlot(this.proxy, ImplementationSlot); - expect(implementation).to.be.equal(this.behaviorV2.address); - expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV2.address }); + expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behaviorV2.target); + + await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV2.target); }); it("calls the 'migrate' function and sends given value to the proxy", async function () { - const migratable = new MigratableMockV2(this.proxy.address); - - const x = await migratable.x(); - expect(x).to.be.bignumber.equal('10'); - - const y = await migratable.y(); - expect(y).to.be.bignumber.equal('42'); - - const balance = new BN(await web3.eth.getBalance(this.proxy.address)); - expect(balance).to.be.bignumber.equal(this.balancePreviousV2.addn(value)); + expect(await this.behaviorV2.attach(this.proxy).x()).to.equal(10n); + expect(await this.behaviorV2.attach(this.proxy).y()).to.equal(42n); + expect(await ethers.provider.getBalance(this.proxy)).to.equal(this.balancePreviousV2 + value); }); describe('when upgrading to V3', function () { - const v3MigrationData = new MigratableMockV3('').contract.methods['migrate()']().encodeABI(); - beforeEach(async function () { - this.behaviorV3 = await MigratableMockV3.new(); - this.balancePreviousV3 = new BN(await web3.eth.getBalance(this.proxy.address)); - this.receipt = await this.proxy.upgradeToAndCall(this.behaviorV3.address, v3MigrationData, { - from: this.proxyAdminAddress, - value, - }); + this.behaviorV3 = await ethers.deployContract('MigratableMockV3'); + const v3MigrationData = this.behaviorV3.interface.encodeFunctionData('migrate()'); + + this.balancePreviousV3 = await ethers.provider.getBalance(this.proxy); + this.tx = await this.proxy + .connect(this.proxyAdminAsSigner) + .upgradeToAndCall(this.behaviorV3, v3MigrationData, { + value, + }); }); it('upgrades to the requested version and emits an event', async function () { - const implementation = await getAddressInSlot(this.proxy, ImplementationSlot); - expect(implementation).to.be.equal(this.behaviorV3.address); - expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV3.address }); + expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behaviorV3.target); + + await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV3.target); }); it("calls the 'migrate' function and sends given value to the proxy", async function () { - const migratable = new MigratableMockV3(this.proxy.address); - - const x = await migratable.x(); - expect(x).to.be.bignumber.equal('42'); - - const y = await migratable.y(); - expect(y).to.be.bignumber.equal('10'); - - const balance = new BN(await web3.eth.getBalance(this.proxy.address)); - expect(balance).to.be.bignumber.equal(this.balancePreviousV3.addn(value)); + expect(await this.behaviorV3.attach(this.proxy).x()).to.equal(42n); + expect(await this.behaviorV3.attach(this.proxy).y()).to.equal(10n); + expect(await ethers.provider.getBalance(this.proxy)).to.equal(this.balancePreviousV3 + value); }); }); }); @@ -263,12 +225,10 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx }); describe('when the sender is not the admin', function () { - const from = anotherAccount; - it('reverts', async function () { - const behaviorV1 = await MigratableMockV1.new(); - const v1MigrationData = new MigratableMockV1('').contract.methods.initialize(42).encodeABI(); - await expectRevert.unspecified(this.proxy.upgradeToAndCall(behaviorV1.address, v1MigrationData, { from })); + const behaviorV1 = await ethers.deployContract('MigratableMockV1'); + const v1MigrationData = behaviorV1.interface.encodeFunctionData('initialize', [42n]); + await expect(this.proxy.connect(this.other).upgradeToAndCall(behaviorV1, v1MigrationData)).to.be.reverted; }); }); }); @@ -276,137 +236,122 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx describe('transparent proxy', function () { beforeEach('creating proxy', async function () { - const initializeData = Buffer.from(''); - this.clashingImplV0 = (await ClashingImplementation.new()).address; - this.clashingImplV1 = (await ClashingImplementation.new()).address; - const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin( - this.clashingImplV0, - initializeData, - ); - this.proxy = proxy; - this.proxyAdminAddress = proxyAdminAddress; - this.clashing = new ClashingImplementation(this.proxy.address); + this.clashingImplV0 = await ethers.deployContract('ClashingImplementation'); + this.clashingImplV1 = await ethers.deployContract('ClashingImplementation'); + + Object.assign(this, await this.createProxyWithImpersonatedProxyAdmin(this.clashingImplV0, '0x')); }); it('proxy admin cannot call delegated functions', async function () { - await expectRevertCustomError( - this.clashing.delegatedFunction({ from: this.proxyAdminAddress }), + const interface = await ethers.getContractFactory('TransparentUpgradeableProxy'); + + await expect(this.instance.connect(this.proxyAdminAsSigner).delegatedFunction()).to.be.revertedWithCustomError( + interface, 'ProxyDeniedAdminAccess', - [], ); }); describe('when function names clash', function () { it('executes the proxy function if the sender is the admin', async function () { - const receipt = await this.proxy.upgradeToAndCall(this.clashingImplV1, '0x', { - from: this.proxyAdminAddress, - }); - expectEvent(receipt, 'Upgraded', { implementation: this.clashingImplV1 }); + await expect(this.proxy.connect(this.proxyAdminAsSigner).upgradeToAndCall(this.clashingImplV1, '0x')) + .to.emit(this.proxy, 'Upgraded') + .withArgs(this.clashingImplV1.target); }); it('delegates the call to implementation when sender is not the admin', async function () { - const receipt = await this.proxy.upgradeToAndCall(this.clashingImplV1, '0x', { - from: anotherAccount, - }); - expectEvent.notEmitted(receipt, 'Upgraded'); - expectEvent.inTransaction(receipt.tx, this.clashing, 'ClashingImplementationCall'); + await expect(this.proxy.connect(this.other).upgradeToAndCall(this.clashingImplV1, '0x')) + .to.emit(this.instance, 'ClashingImplementationCall') + .to.not.emit(this.proxy, 'Upgraded'); }); }); }); - describe('regression', () => { - const initializeData = Buffer.from(''); + describe('regression', function () { + const initializeData = '0x'; - it('should add new function', async () => { - const instance1 = await Implementation1.new(); - const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin( - instance1.address, + it('should add new function', async function () { + const impl1 = await ethers.deployContract('Implementation1'); + const impl2 = await ethers.deployContract('Implementation2'); + const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin( + impl1, initializeData, ); - const proxyInstance1 = new Implementation1(proxy.address); - await proxyInstance1.setValue(42); + await instance.setValue(42n); + + // `getValue` is not available in impl1 + await expect(impl2.attach(instance).getValue()).to.be.reverted; - const instance2 = await Implementation2.new(); - await proxy.upgradeToAndCall(instance2.address, '0x', { from: proxyAdminAddress }); + // do upgrade + await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl2, '0x'); - const proxyInstance2 = new Implementation2(proxy.address); - const res = await proxyInstance2.getValue(); - expect(res.toString()).to.eq('42'); + // `getValue` is available in impl2 + expect(await impl2.attach(instance).getValue()).to.equal(42n); }); - it('should remove function', async () => { - const instance2 = await Implementation2.new(); - const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin( - instance2.address, + it('should remove function', async function () { + const impl1 = await ethers.deployContract('Implementation1'); + const impl2 = await ethers.deployContract('Implementation2'); + const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin( + impl2, initializeData, ); - const proxyInstance2 = new Implementation2(proxy.address); - await proxyInstance2.setValue(42); - const res = await proxyInstance2.getValue(); - expect(res.toString()).to.eq('42'); + await instance.setValue(42n); + + // `getValue` is available in impl2 + expect(await impl2.attach(instance).getValue()).to.equal(42n); - const instance1 = await Implementation1.new(); - await proxy.upgradeToAndCall(instance1.address, '0x', { from: proxyAdminAddress }); + // do downgrade + await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl1, '0x'); - const proxyInstance1 = new Implementation2(proxy.address); - await expectRevert.unspecified(proxyInstance1.getValue()); + // `getValue` is not available in impl1 + await expect(impl2.attach(instance).getValue()).to.be.reverted; }); - it('should change function signature', async () => { - const instance1 = await Implementation1.new(); - const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin( - instance1.address, + it('should change function signature', async function () { + const impl1 = await ethers.deployContract('Implementation1'); + const impl3 = await ethers.deployContract('Implementation3'); + const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin( + impl1, initializeData, ); - const proxyInstance1 = new Implementation1(proxy.address); - await proxyInstance1.setValue(42); + await instance.setValue(42n); - const instance3 = await Implementation3.new(); - await proxy.upgradeToAndCall(instance3.address, '0x', { from: proxyAdminAddress }); - const proxyInstance3 = new Implementation3(proxy.address); + await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl3, '0x'); - const res = await proxyInstance3.getValue(8); - expect(res.toString()).to.eq('50'); + expect(await impl3.attach(instance).getValue(8n)).to.equal(50n); }); - it('should add fallback function', async () => { - const initializeData = Buffer.from(''); - const instance1 = await Implementation1.new(); - const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin( - instance1.address, + it('should add fallback function', async function () { + const impl1 = await ethers.deployContract('Implementation1'); + const impl4 = await ethers.deployContract('Implementation4'); + const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin( + impl1, initializeData, ); - const instance4 = await Implementation4.new(); - await proxy.upgradeToAndCall(instance4.address, '0x', { from: proxyAdminAddress }); - const proxyInstance4 = new Implementation4(proxy.address); + await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl4, '0x'); - const data = '0x'; - await web3.eth.sendTransaction({ to: proxy.address, from: anotherAccount, data }); + await this.other.sendTransaction({ to: proxy }); - const res = await proxyInstance4.getValue(); - expect(res.toString()).to.eq('1'); + expect(await impl4.attach(instance).getValue()).to.equal(1n); }); - it('should remove fallback function', async () => { - const instance4 = await Implementation4.new(); - const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin( - instance4.address, + it('should remove fallback function', async function () { + const impl2 = await ethers.deployContract('Implementation2'); + const impl4 = await ethers.deployContract('Implementation4'); + const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin( + impl4, initializeData, ); - const instance2 = await Implementation2.new(); - await proxy.upgradeToAndCall(instance2.address, '0x', { from: proxyAdminAddress }); + await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl2, '0x'); - const data = '0x'; - await expectRevert.unspecified(web3.eth.sendTransaction({ to: proxy.address, from: anotherAccount, data })); + await expect(this.other.sendTransaction({ to: proxy })).to.be.reverted; - const proxyInstance2 = new Implementation2(proxy.address); - const res = await proxyInstance2.getValue(); - expect(res.toString()).to.eq('0'); + expect(await impl2.attach(instance).getValue()).to.equal(0n); }); }); }; diff --git a/test/proxy/transparent/TransparentUpgradeableProxy.test.js b/test/proxy/transparent/TransparentUpgradeableProxy.test.js index f45e392f69d..61e18014e5b 100644 --- a/test/proxy/transparent/TransparentUpgradeableProxy.test.js +++ b/test/proxy/transparent/TransparentUpgradeableProxy.test.js @@ -1,24 +1,28 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + const shouldBehaveLikeProxy = require('../Proxy.behaviour'); const shouldBehaveLikeTransparentUpgradeableProxy = require('./TransparentUpgradeableProxy.behaviour'); -const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy'); -const ITransparentUpgradeableProxy = artifacts.require('ITransparentUpgradeableProxy'); - -contract('TransparentUpgradeableProxy', function (accounts) { - const [owner, ...otherAccounts] = accounts; - - // `undefined`, `null` and other false-ish opts will not be forwarded. - const createProxy = async function (logic, initData, opts = undefined) { - const { address, transactionHash } = await TransparentUpgradeableProxy.new( - logic, - owner, - initData, - ...[opts].filter(Boolean), - ); - const instance = await ITransparentUpgradeableProxy.at(address); - return { ...instance, transactionHash }; +async function fixture() { + const [owner, other, ...accounts] = await ethers.getSigners(); + + const implementation = await ethers.deployContract('DummyImplementation'); + + const createProxy = function (logic, initData, opts = undefined) { + return ethers.deployContract('TransparentUpgradeableProxy', [logic, owner, initData], opts); }; - shouldBehaveLikeProxy(createProxy, otherAccounts); - shouldBehaveLikeTransparentUpgradeableProxy(createProxy, owner, otherAccounts); + return { nonContractAddress: owner, owner, other, accounts, implementation, createProxy }; +} + +describe('TransparentUpgradeableProxy', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeProxy(); + + // createProxy, owner, otherAccounts + shouldBehaveLikeTransparentUpgradeableProxy(); }); diff --git a/test/proxy/utils/Initializable.test.js b/test/proxy/utils/Initializable.test.js index b9ff3b05223..bc26e6b60a5 100644 --- a/test/proxy/utils/Initializable.test.js +++ b/test/proxy/utils/Initializable.test.js @@ -1,220 +1,218 @@ -const { expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const { MAX_UINT64 } = require('../../helpers/constants'); - -const InitializableMock = artifacts.require('InitializableMock'); -const ConstructorInitializableMock = artifacts.require('ConstructorInitializableMock'); -const ChildConstructorInitializableMock = artifacts.require('ChildConstructorInitializableMock'); -const ReinitializerMock = artifacts.require('ReinitializerMock'); -const SampleChild = artifacts.require('SampleChild'); -const DisableBad1 = artifacts.require('DisableBad1'); -const DisableBad2 = artifacts.require('DisableBad2'); -const DisableOk = artifacts.require('DisableOk'); - -contract('Initializable', function () { +const { + bigint: { MAX_UINT64 }, +} = require('../../helpers/constants'); + +describe('Initializable', function () { describe('basic testing without inheritance', function () { beforeEach('deploying', async function () { - this.contract = await InitializableMock.new(); + this.mock = await ethers.deployContract('InitializableMock'); }); describe('before initialize', function () { it('initializer has not run', async function () { - expect(await this.contract.initializerRan()).to.equal(false); + expect(await this.mock.initializerRan()).to.be.false; }); it('_initializing returns false before initialization', async function () { - expect(await this.contract.isInitializing()).to.equal(false); + expect(await this.mock.isInitializing()).to.be.false; }); }); describe('after initialize', function () { beforeEach('initializing', async function () { - await this.contract.initialize(); + await this.mock.initialize(); }); it('initializer has run', async function () { - expect(await this.contract.initializerRan()).to.equal(true); + expect(await this.mock.initializerRan()).to.be.true; }); it('_initializing returns false after initialization', async function () { - expect(await this.contract.isInitializing()).to.equal(false); + expect(await this.mock.isInitializing()).to.be.false; }); it('initializer does not run again', async function () { - await expectRevertCustomError(this.contract.initialize(), 'InvalidInitialization', []); + await expect(this.mock.initialize()).to.be.revertedWithCustomError(this.mock, 'InvalidInitialization'); }); }); describe('nested under an initializer', function () { it('initializer modifier reverts', async function () { - await expectRevertCustomError(this.contract.initializerNested(), 'InvalidInitialization', []); + await expect(this.mock.initializerNested()).to.be.revertedWithCustomError(this.mock, 'InvalidInitialization'); }); it('onlyInitializing modifier succeeds', async function () { - await this.contract.onlyInitializingNested(); - expect(await this.contract.onlyInitializingRan()).to.equal(true); + await this.mock.onlyInitializingNested(); + expect(await this.mock.onlyInitializingRan()).to.be.true; }); }); it('cannot call onlyInitializable function outside the scope of an initializable function', async function () { - await expectRevertCustomError(this.contract.initializeOnlyInitializing(), 'NotInitializing', []); + await expect(this.mock.initializeOnlyInitializing()).to.be.revertedWithCustomError(this.mock, 'NotInitializing'); }); }); it('nested initializer can run during construction', async function () { - const contract2 = await ConstructorInitializableMock.new(); - expect(await contract2.initializerRan()).to.equal(true); - expect(await contract2.onlyInitializingRan()).to.equal(true); + const mock = await ethers.deployContract('ConstructorInitializableMock'); + expect(await mock.initializerRan()).to.be.true; + expect(await mock.onlyInitializingRan()).to.be.true; }); it('multiple constructor levels can be initializers', async function () { - const contract2 = await ChildConstructorInitializableMock.new(); - expect(await contract2.initializerRan()).to.equal(true); - expect(await contract2.childInitializerRan()).to.equal(true); - expect(await contract2.onlyInitializingRan()).to.equal(true); + const mock = await ethers.deployContract('ChildConstructorInitializableMock'); + expect(await mock.initializerRan()).to.be.true; + expect(await mock.childInitializerRan()).to.be.true; + expect(await mock.onlyInitializingRan()).to.be.true; }); describe('reinitialization', function () { beforeEach('deploying', async function () { - this.contract = await ReinitializerMock.new(); + this.mock = await ethers.deployContract('ReinitializerMock'); }); it('can reinitialize', async function () { - expect(await this.contract.counter()).to.be.bignumber.equal('0'); - await this.contract.initialize(); - expect(await this.contract.counter()).to.be.bignumber.equal('1'); - await this.contract.reinitialize(2); - expect(await this.contract.counter()).to.be.bignumber.equal('2'); - await this.contract.reinitialize(3); - expect(await this.contract.counter()).to.be.bignumber.equal('3'); + expect(await this.mock.counter()).to.equal(0n); + await this.mock.initialize(); + expect(await this.mock.counter()).to.equal(1n); + await this.mock.reinitialize(2); + expect(await this.mock.counter()).to.equal(2n); + await this.mock.reinitialize(3); + expect(await this.mock.counter()).to.equal(3n); }); it('can jump multiple steps', async function () { - expect(await this.contract.counter()).to.be.bignumber.equal('0'); - await this.contract.initialize(); - expect(await this.contract.counter()).to.be.bignumber.equal('1'); - await this.contract.reinitialize(128); - expect(await this.contract.counter()).to.be.bignumber.equal('2'); + expect(await this.mock.counter()).to.equal(0n); + await this.mock.initialize(); + expect(await this.mock.counter()).to.equal(1n); + await this.mock.reinitialize(128); + expect(await this.mock.counter()).to.equal(2n); }); it('cannot nest reinitializers', async function () { - expect(await this.contract.counter()).to.be.bignumber.equal('0'); - await expectRevertCustomError(this.contract.nestedReinitialize(2, 2), 'InvalidInitialization', []); - await expectRevertCustomError(this.contract.nestedReinitialize(2, 3), 'InvalidInitialization', []); - await expectRevertCustomError(this.contract.nestedReinitialize(3, 2), 'InvalidInitialization', []); + expect(await this.mock.counter()).to.equal(0n); + await expect(this.mock.nestedReinitialize(2, 2)).to.be.revertedWithCustomError( + this.mock, + 'InvalidInitialization', + ); + await expect(this.mock.nestedReinitialize(2, 3)).to.be.revertedWithCustomError( + this.mock, + 'InvalidInitialization', + ); + await expect(this.mock.nestedReinitialize(3, 2)).to.be.revertedWithCustomError( + this.mock, + 'InvalidInitialization', + ); }); it('can chain reinitializers', async function () { - expect(await this.contract.counter()).to.be.bignumber.equal('0'); - await this.contract.chainReinitialize(2, 3); - expect(await this.contract.counter()).to.be.bignumber.equal('2'); + expect(await this.mock.counter()).to.equal(0n); + await this.mock.chainReinitialize(2, 3); + expect(await this.mock.counter()).to.equal(2n); }); it('_getInitializedVersion returns right version', async function () { - await this.contract.initialize(); - expect(await this.contract.getInitializedVersion()).to.be.bignumber.equal('1'); - await this.contract.reinitialize(12); - expect(await this.contract.getInitializedVersion()).to.be.bignumber.equal('12'); + await this.mock.initialize(); + expect(await this.mock.getInitializedVersion()).to.equal(1n); + await this.mock.reinitialize(12); + expect(await this.mock.getInitializedVersion()).to.equal(12n); }); describe('contract locking', function () { it('prevents initialization', async function () { - await this.contract.disableInitializers(); - await expectRevertCustomError(this.contract.initialize(), 'InvalidInitialization', []); + await this.mock.disableInitializers(); + await expect(this.mock.initialize()).to.be.revertedWithCustomError(this.mock, 'InvalidInitialization'); }); it('prevents re-initialization', async function () { - await this.contract.disableInitializers(); - await expectRevertCustomError(this.contract.reinitialize(255), 'InvalidInitialization', []); + await this.mock.disableInitializers(); + await expect(this.mock.reinitialize(255n)).to.be.revertedWithCustomError(this.mock, 'InvalidInitialization'); }); it('can lock contract after initialization', async function () { - await this.contract.initialize(); - await this.contract.disableInitializers(); - await expectRevertCustomError(this.contract.reinitialize(255), 'InvalidInitialization', []); + await this.mock.initialize(); + await this.mock.disableInitializers(); + await expect(this.mock.reinitialize(255n)).to.be.revertedWithCustomError(this.mock, 'InvalidInitialization'); }); }); }); describe('events', function () { it('constructor initialization emits event', async function () { - const contract = await ConstructorInitializableMock.new(); - - await expectEvent.inTransaction(contract.transactionHash, contract, 'Initialized', { version: '1' }); + const mock = await ethers.deployContract('ConstructorInitializableMock'); + await expect(mock.deploymentTransaction()).to.emit(mock, 'Initialized').withArgs(1n); }); it('initialization emits event', async function () { - const contract = await ReinitializerMock.new(); - - const { receipt } = await contract.initialize(); - expect(receipt.logs.filter(({ event }) => event === 'Initialized').length).to.be.equal(1); - expectEvent(receipt, 'Initialized', { version: '1' }); + const mock = await ethers.deployContract('ReinitializerMock'); + await expect(mock.initialize()).to.emit(mock, 'Initialized').withArgs(1n); }); it('reinitialization emits event', async function () { - const contract = await ReinitializerMock.new(); - - const { receipt } = await contract.reinitialize(128); - expect(receipt.logs.filter(({ event }) => event === 'Initialized').length).to.be.equal(1); - expectEvent(receipt, 'Initialized', { version: '128' }); + const mock = await ethers.deployContract('ReinitializerMock'); + await expect(mock.reinitialize(128)).to.emit(mock, 'Initialized').withArgs(128n); }); it('chained reinitialization emits multiple events', async function () { - const contract = await ReinitializerMock.new(); + const mock = await ethers.deployContract('ReinitializerMock'); - const { receipt } = await contract.chainReinitialize(2, 3); - expect(receipt.logs.filter(({ event }) => event === 'Initialized').length).to.be.equal(2); - expectEvent(receipt, 'Initialized', { version: '2' }); - expectEvent(receipt, 'Initialized', { version: '3' }); + await expect(mock.chainReinitialize(2, 3)) + .to.emit(mock, 'Initialized') + .withArgs(2n) + .to.emit(mock, 'Initialized') + .withArgs(3n); }); }); describe('complex testing with inheritance', function () { - const mother = '12'; + const mother = 12n; const gramps = '56'; - const father = '34'; - const child = '78'; + const father = 34n; + const child = 78n; beforeEach('deploying', async function () { - this.contract = await SampleChild.new(); - }); - - beforeEach('initializing', async function () { - await this.contract.initialize(mother, gramps, father, child); + this.mock = await ethers.deployContract('SampleChild'); + await this.mock.initialize(mother, gramps, father, child); }); it('initializes human', async function () { - expect(await this.contract.isHuman()).to.be.equal(true); + expect(await this.mock.isHuman()).to.be.true; }); it('initializes mother', async function () { - expect(await this.contract.mother()).to.be.bignumber.equal(mother); + expect(await this.mock.mother()).to.equal(mother); }); it('initializes gramps', async function () { - expect(await this.contract.gramps()).to.be.bignumber.equal(gramps); + expect(await this.mock.gramps()).to.equal(gramps); }); it('initializes father', async function () { - expect(await this.contract.father()).to.be.bignumber.equal(father); + expect(await this.mock.father()).to.equal(father); }); it('initializes child', async function () { - expect(await this.contract.child()).to.be.bignumber.equal(child); + expect(await this.mock.child()).to.equal(child); }); }); describe('disabling initialization', function () { it('old and new patterns in bad sequence', async function () { - await expectRevertCustomError(DisableBad1.new(), 'InvalidInitialization', []); - await expectRevertCustomError(DisableBad2.new(), 'InvalidInitialization', []); + const DisableBad1 = await ethers.getContractFactory('DisableBad1'); + await expect(DisableBad1.deploy()).to.be.revertedWithCustomError(DisableBad1, 'InvalidInitialization'); + + const DisableBad2 = await ethers.getContractFactory('DisableBad2'); + await expect(DisableBad2.deploy()).to.be.revertedWithCustomError(DisableBad2, 'InvalidInitialization'); }); it('old and new patterns in good sequence', async function () { - const ok = await DisableOk.new(); - await expectEvent.inConstruction(ok, 'Initialized', { version: '1' }); - await expectEvent.inConstruction(ok, 'Initialized', { version: MAX_UINT64 }); + const ok = await ethers.deployContract('DisableOk'); + await expect(ok.deploymentTransaction()) + .to.emit(ok, 'Initialized') + .withArgs(1n) + .to.emit(ok, 'Initialized') + .withArgs(MAX_UINT64); }); }); }); diff --git a/test/proxy/utils/UUPSUpgradeable.test.js b/test/proxy/utils/UUPSUpgradeable.test.js index 0baa9052049..876a64cf915 100644 --- a/test/proxy/utils/UUPSUpgradeable.test.js +++ b/test/proxy/utils/UUPSUpgradeable.test.js @@ -1,28 +1,36 @@ -const { expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + const { getAddressInSlot, ImplementationSlot } = require('../../helpers/erc1967'); -const { expectRevertCustomError } = require('../../helpers/customError'); - -const ERC1967Proxy = artifacts.require('ERC1967Proxy'); -const UUPSUpgradeableMock = artifacts.require('UUPSUpgradeableMock'); -const UUPSUpgradeableUnsafeMock = artifacts.require('UUPSUpgradeableUnsafeMock'); -const NonUpgradeableMock = artifacts.require('NonUpgradeableMock'); -const UUPSUnsupportedProxiableUUID = artifacts.require('UUPSUnsupportedProxiableUUID'); -const Clones = artifacts.require('$Clones'); - -contract('UUPSUpgradeable', function () { - before(async function () { - this.implInitial = await UUPSUpgradeableMock.new(); - this.implUpgradeOk = await UUPSUpgradeableMock.new(); - this.implUpgradeUnsafe = await UUPSUpgradeableUnsafeMock.new(); - this.implUpgradeNonUUPS = await NonUpgradeableMock.new(); - this.implUnsupportedUUID = await UUPSUnsupportedProxiableUUID.new(); - // Used for testing non ERC1967 compliant proxies (clones are proxies that don't use the ERC1967 implementation slot) - this.cloneFactory = await Clones.new(); - }); +async function fixture() { + const implInitial = await ethers.deployContract('UUPSUpgradeableMock'); + const implUpgradeOk = await ethers.deployContract('UUPSUpgradeableMock'); + const implUpgradeUnsafe = await ethers.deployContract('UUPSUpgradeableUnsafeMock'); + const implUpgradeNonUUPS = await ethers.deployContract('NonUpgradeableMock'); + const implUnsupportedUUID = await ethers.deployContract('UUPSUnsupportedProxiableUUID'); + // Used for testing non ERC1967 compliant proxies (clones are proxies that don't use the ERC1967 implementation slot) + const cloneFactory = await ethers.deployContract('$Clones'); + + const instance = await ethers + .deployContract('ERC1967Proxy', [implInitial, '0x']) + .then(proxy => implInitial.attach(proxy.target)); + + return { + implInitial, + implUpgradeOk, + implUpgradeUnsafe, + implUpgradeNonUUPS, + implUnsupportedUUID, + cloneFactory, + instance, + }; +} + +describe('UUPSUpgradeable', function () { beforeEach(async function () { - const { address } = await ERC1967Proxy.new(this.implInitial.address, '0x'); - this.instance = await UUPSUpgradeableMock.at(address); + Object.assign(this, await loadFixture(fixture)); }); it('has an interface version', async function () { @@ -30,102 +38,83 @@ contract('UUPSUpgradeable', function () { }); it('upgrade to upgradeable implementation', async function () { - const { receipt } = await this.instance.upgradeToAndCall(this.implUpgradeOk.address, '0x'); - expect(receipt.logs.filter(({ event }) => event === 'Upgraded').length).to.be.equal(1); - expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeOk.address }); - expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.be.equal(this.implUpgradeOk.address); + await expect(this.instance.upgradeToAndCall(this.implUpgradeOk, '0x')) + .to.emit(this.instance, 'Upgraded') + .withArgs(this.implUpgradeOk.target); + + expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.equal(this.implUpgradeOk.target); }); it('upgrade to upgradeable implementation with call', async function () { - expect(await this.instance.current()).to.be.bignumber.equal('0'); + expect(await this.instance.current()).to.equal(0n); - const { receipt } = await this.instance.upgradeToAndCall( - this.implUpgradeOk.address, - this.implUpgradeOk.contract.methods.increment().encodeABI(), - ); - expect(receipt.logs.filter(({ event }) => event === 'Upgraded').length).to.be.equal(1); - expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeOk.address }); - expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.be.equal(this.implUpgradeOk.address); + await expect( + this.instance.upgradeToAndCall(this.implUpgradeOk, this.implUpgradeOk.interface.encodeFunctionData('increment')), + ) + .to.emit(this.instance, 'Upgraded') + .withArgs(this.implUpgradeOk.target); + + expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.equal(this.implUpgradeOk.target); - expect(await this.instance.current()).to.be.bignumber.equal('1'); + expect(await this.instance.current()).to.equal(1n); }); it('calling upgradeTo on the implementation reverts', async function () { - await expectRevertCustomError( - this.implInitial.upgradeToAndCall(this.implUpgradeOk.address, '0x'), + await expect(this.implInitial.upgradeToAndCall(this.implUpgradeOk, '0x')).to.be.revertedWithCustomError( + this.implInitial, 'UUPSUnauthorizedCallContext', - [], ); }); it('calling upgradeToAndCall on the implementation reverts', async function () { - await expectRevertCustomError( + await expect( this.implInitial.upgradeToAndCall( - this.implUpgradeOk.address, - this.implUpgradeOk.contract.methods.increment().encodeABI(), + this.implUpgradeOk, + this.implUpgradeOk.interface.encodeFunctionData('increment'), ), - 'UUPSUnauthorizedCallContext', - [], - ); - }); - - it('calling upgradeTo from a contract that is not an ERC1967 proxy (with the right implementation) reverts', async function () { - const receipt = await this.cloneFactory.$clone(this.implUpgradeOk.address); - const instance = await UUPSUpgradeableMock.at( - receipt.logs.find(({ event }) => event === 'return$clone').args.instance, - ); - - await expectRevertCustomError( - instance.upgradeToAndCall(this.implUpgradeUnsafe.address, '0x'), - 'UUPSUnauthorizedCallContext', - [], - ); + ).to.be.revertedWithCustomError(this.implUpgradeOk, 'UUPSUnauthorizedCallContext'); }); it('calling upgradeToAndCall from a contract that is not an ERC1967 proxy (with the right implementation) reverts', async function () { - const receipt = await this.cloneFactory.$clone(this.implUpgradeOk.address); - const instance = await UUPSUpgradeableMock.at( - receipt.logs.find(({ event }) => event === 'return$clone').args.instance, - ); + const instance = await this.cloneFactory.$clone + .staticCall(this.implUpgradeOk) + .then(address => this.implInitial.attach(address)); + await this.cloneFactory.$clone(this.implUpgradeOk); - await expectRevertCustomError( - instance.upgradeToAndCall(this.implUpgradeUnsafe.address, '0x'), + await expect(instance.upgradeToAndCall(this.implUpgradeUnsafe, '0x')).to.be.revertedWithCustomError( + instance, 'UUPSUnauthorizedCallContext', - [], ); }); it('rejects upgrading to an unsupported UUID', async function () { - await expectRevertCustomError( - this.instance.upgradeToAndCall(this.implUnsupportedUUID.address, '0x'), - 'UUPSUnsupportedProxiableUUID', - [web3.utils.keccak256('invalid UUID')], - ); + await expect(this.instance.upgradeToAndCall(this.implUnsupportedUUID, '0x')) + .to.be.revertedWithCustomError(this.instance, 'UUPSUnsupportedProxiableUUID') + .withArgs(ethers.id('invalid UUID')); }); it('upgrade to and unsafe upgradeable implementation', async function () { - const { receipt } = await this.instance.upgradeToAndCall(this.implUpgradeUnsafe.address, '0x'); - expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeUnsafe.address }); - expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.be.equal(this.implUpgradeUnsafe.address); + await expect(this.instance.upgradeToAndCall(this.implUpgradeUnsafe, '0x')) + .to.emit(this.instance, 'Upgraded') + .withArgs(this.implUpgradeUnsafe.target); + + expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.equal(this.implUpgradeUnsafe.target); }); // delegate to a non existing upgradeTo function causes a low level revert it('reject upgrade to non uups implementation', async function () { - await expectRevertCustomError( - this.instance.upgradeToAndCall(this.implUpgradeNonUUPS.address, '0x'), - 'ERC1967InvalidImplementation', - [this.implUpgradeNonUUPS.address], - ); + await expect(this.instance.upgradeToAndCall(this.implUpgradeNonUUPS, '0x')) + .to.be.revertedWithCustomError(this.instance, 'ERC1967InvalidImplementation') + .withArgs(this.implUpgradeNonUUPS.target); }); it('reject proxy address as implementation', async function () { - const { address } = await ERC1967Proxy.new(this.implInitial.address, '0x'); - const otherInstance = await UUPSUpgradeableMock.at(address); + const otherInstance = await ethers + .deployContract('ERC1967Proxy', [this.implInitial, '0x']) + .then(proxy => this.implInitial.attach(proxy.target)); - await expectRevertCustomError( - this.instance.upgradeToAndCall(otherInstance.address, '0x'), - 'ERC1967InvalidImplementation', - [otherInstance.address], - ); + await expect(this.instance.upgradeToAndCall(otherInstance, '0x')) + .to.be.revertedWithCustomError(this.instance, 'ERC1967InvalidImplementation') + .withArgs(otherInstance.target); }); }); From 552cffde563e83043a6c3a35012b626a25eba775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 30 Nov 2023 08:04:34 -0600 Subject: [PATCH 28/44] Add Governor note for state changes between proposal creation and execution (#4766) Co-authored-by: Hadrien Croubois --- contracts/governance/IGovernor.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 85fc281d7f9..63589612e2f 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -317,6 +317,12 @@ interface IGovernor is IERC165, IERC6372 { * duration specified by {IGovernor-votingPeriod}. * * Emits a {ProposalCreated} event. + * + * NOTE: The state of the Governor and `targets` may change between the proposal creation and its execution. + * This may be the result of third party actions on the targeted contracts, or other governor proposals. + * For example, the balance of this contract could be updated or its access control permissions may be modified, + * possibly compromising the proposal's ability to execute successfully (e.g. the governor doesn't have enough + * value to cover a proposal with multiple transfers). */ function propose( address[] memory targets, From ef699fa6a224de863ffe48347a5ab95d3d8ba2ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 1 Dec 2023 07:46:46 -0600 Subject: [PATCH 29/44] Update Math `try*` operations return reference (#4775) --- contracts/utils/math/Math.sol | 20 ++++++++++---------- docs/modules/ROOT/pages/utilities.adoc | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index 86316cb2971..3a1d5a4b24d 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -20,9 +20,9 @@ library Math { } /** - * @dev Returns the addition of two unsigned integers, with an overflow flag. + * @dev Returns the addition of two unsigned integers, with an success flag (no overflow). */ - function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { + function tryAdd(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { unchecked { uint256 c = a + b; if (c < a) return (false, 0); @@ -31,9 +31,9 @@ library Math { } /** - * @dev Returns the subtraction of two unsigned integers, with an overflow flag. + * @dev Returns the subtraction of two unsigned integers, with an success flag (no overflow). */ - function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { + function trySub(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { unchecked { if (b > a) return (false, 0); return (true, a - b); @@ -41,9 +41,9 @@ library Math { } /** - * @dev Returns the multiplication of two unsigned integers, with an overflow flag. + * @dev Returns the multiplication of two unsigned integers, with an success flag (no overflow). */ - function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { + function tryMul(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { unchecked { // Gas optimization: this is cheaper than requiring 'a' not being zero, but the // benefit is lost if 'b' is also tested. @@ -56,9 +56,9 @@ library Math { } /** - * @dev Returns the division of two unsigned integers, with a division by zero flag. + * @dev Returns the division of two unsigned integers, with a success flag (no division by zero). */ - function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { + function tryDiv(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { unchecked { if (b == 0) return (false, 0); return (true, a / b); @@ -66,9 +66,9 @@ library Math { } /** - * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag. + * @dev Returns the remainder of dividing two unsigned integers, with a success flag (no division by zero). */ - function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { + function tryMod(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { unchecked { if (b == 0) return (false, 0); return (true, a % b); diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index f940d0d2259..02ae4efffee 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -82,10 +82,10 @@ contract MyContract { using SignedMath for int256; function tryOperations(uint256 a, uint256 b) internal pure { - (bool overflowsAdd, uint256 resultAdd) = x.tryAdd(y); - (bool overflowsSub, uint256 resultSub) = x.trySub(y); - (bool overflowsMul, uint256 resultMul) = x.tryMul(y); - (bool overflowsDiv, uint256 resultDiv) = x.tryDiv(y); + (bool succededAdd, uint256 resultAdd) = x.tryAdd(y); + (bool succededSub, uint256 resultSub) = x.trySub(y); + (bool succededMul, uint256 resultMul) = x.tryMul(y); + (bool succededDiv, uint256 resultDiv) = x.tryDiv(y); // ... } From cffb2f1ddcd87efd68effc92cfd336c5145acabd Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 4 Dec 2023 20:00:00 +0100 Subject: [PATCH 30/44] Migrate math tests to ethers.js v6 (#4769) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- test/helpers/enums.js | 24 +- test/helpers/iterate.js | 6 - test/helpers/math.js | 21 +- test/metatx/ERC2771Forwarder.test.js | 2 +- .../extensions/ERC721Consecutive.test.js | 3 +- test/utils/math/Math.test.js | 500 +++++++++--------- test/utils/math/SafeCast.test.js | 157 +++--- test/utils/math/SignedMath.test.js | 91 +--- test/utils/types/Time.test.js | 3 +- 9 files changed, 366 insertions(+), 441 deletions(-) diff --git a/test/helpers/enums.js b/test/helpers/enums.js index 6280e0f319b..b75e73ba8dc 100644 --- a/test/helpers/enums.js +++ b/test/helpers/enums.js @@ -2,10 +2,20 @@ function Enum(...options) { return Object.fromEntries(options.map((key, i) => [key, web3.utils.toBN(i)])); } -module.exports = { - Enum, - ProposalState: Enum('Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'), - VoteType: Enum('Against', 'For', 'Abstain'), - Rounding: Enum('Floor', 'Ceil', 'Trunc', 'Expand'), - OperationState: Enum('Unset', 'Waiting', 'Ready', 'Done'), -}; +function EnumBigInt(...options) { + return Object.fromEntries(options.map((key, i) => [key, BigInt(i)])); +} + +// TODO: remove web3, simplify code +function createExport(Enum) { + return { + Enum, + ProposalState: Enum('Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'), + VoteType: Enum('Against', 'For', 'Abstain'), + Rounding: Enum('Floor', 'Ceil', 'Trunc', 'Expand'), + OperationState: Enum('Unset', 'Waiting', 'Ready', 'Done'), + }; +} + +module.exports = createExport(Enum); +module.exports.bigint = createExport(EnumBigInt); diff --git a/test/helpers/iterate.js b/test/helpers/iterate.js index 7f6e0e6780c..2a84dfbebdc 100644 --- a/test/helpers/iterate.js +++ b/test/helpers/iterate.js @@ -1,16 +1,10 @@ // Map values in an object const mapValues = (obj, fn) => Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fn(v)])); -// Array of number or bigint -const max = (...values) => values.slice(1).reduce((x, y) => (x > y ? x : y), values[0]); -const min = (...values) => values.slice(1).reduce((x, y) => (x < y ? x : y), values[0]); - // Cartesian product of a list of arrays const product = (...arrays) => arrays.reduce((a, b) => a.flatMap(ai => b.map(bi => [...ai, bi])), [[]]); module.exports = { mapValues, - max, - min, product, }; diff --git a/test/helpers/math.js b/test/helpers/math.js index 134f8b04509..708990519a3 100644 --- a/test/helpers/math.js +++ b/test/helpers/math.js @@ -1,12 +1,13 @@ +// Array of number or bigint +const max = (...values) => values.slice(1).reduce((x, y) => (x > y ? x : y), values.at(0)); +const min = (...values) => values.slice(1).reduce((x, y) => (x < y ? x : y), values.at(0)); +const sum = (...values) => values.slice(1).reduce((x, y) => x + y, values.at(0)); + module.exports = { - // sum of integer / bignumber - sum: (...args) => args.reduce((acc, n) => acc + n, 0), - bigintSum: (...args) => args.reduce((acc, n) => acc + n, 0n), - BNsum: (...args) => args.reduce((acc, n) => acc.add(n), web3.utils.toBN(0)), - // min of integer / bignumber - min: (...args) => args.slice(1).reduce((x, y) => (x < y ? x : y), args[0]), - BNmin: (...args) => args.slice(1).reduce((x, y) => (x.lt(y) ? x : y), args[0]), - // max of integer / bignumber - max: (...args) => args.slice(1).reduce((x, y) => (x > y ? x : y), args[0]), - BNmax: (...args) => args.slice(1).reduce((x, y) => (x.gt(y) ? x : y), args[0]), + // re-export min, max & sum of integer / bignumber + min, + max, + sum, + // deprecated: BN version of sum + BNsum: (...args) => args.slice(1).reduce((x, y) => x.add(y), args.at(0)), }; diff --git a/test/metatx/ERC2771Forwarder.test.js b/test/metatx/ERC2771Forwarder.test.js index a665471f358..e0d1090c4bf 100644 --- a/test/metatx/ERC2771Forwarder.test.js +++ b/test/metatx/ERC2771Forwarder.test.js @@ -4,7 +4,7 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { getDomain } = require('../helpers/eip712'); const { bigint: time } = require('../helpers/time'); -const { bigintSum: sum } = require('../helpers/math'); +const { sum } = require('../helpers/math'); async function fixture() { const [sender, refundReceiver, another, ...accounts] = await ethers.getSigners(); diff --git a/test/token/ERC721/extensions/ERC721Consecutive.test.js b/test/token/ERC721/extensions/ERC721Consecutive.test.js index d9e33aff29b..e4ee3196d44 100644 --- a/test/token/ERC721/extensions/ERC721Consecutive.test.js +++ b/test/token/ERC721/extensions/ERC721Consecutive.test.js @@ -70,7 +70,8 @@ contract('ERC721Consecutive', function (accounts) { it('balance & voting power are set', async function () { for (const account of accounts) { - const balance = sum(...batches.filter(({ receiver }) => receiver === account).map(({ amount }) => amount)); + const balance = + sum(...batches.filter(({ receiver }) => receiver === account).map(({ amount }) => amount)) ?? 0; expect(await this.token.balanceOf(account)).to.be.bignumber.equal(web3.utils.toBN(balance)); diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 7d4a58c81b1..6e4fa3b9c5a 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -1,320 +1,298 @@ -const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { MAX_UINT256 } = constants; -const { Rounding } = require('../../helpers/enums.js'); -const { expectRevertCustomError } = require('../../helpers/customError.js'); - -const Math = artifacts.require('$Math'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); +const { min, max } = require('../../helpers/math'); +const { + bigint: { Rounding }, +} = require('../../helpers/enums.js'); const RoundingDown = [Rounding.Floor, Rounding.Trunc]; const RoundingUp = [Rounding.Ceil, Rounding.Expand]; -function expectStruct(value, expected) { - for (const key in expected) { - if (BN.isBN(value[key])) { - expect(value[key]).to.be.bignumber.equal(expected[key]); - } else { - expect(value[key]).to.be.equal(expected[key]); - } - } +async function testCommutative(fn, lhs, rhs, expected, ...extra) { + expect(await fn(lhs, rhs, ...extra)).to.deep.equal(expected); + expect(await fn(rhs, lhs, ...extra)).to.deep.equal(expected); } -async function testCommutativeIterable(fn, lhs, rhs, expected, ...extra) { - expectStruct(await fn(lhs, rhs, ...extra), expected); - expectStruct(await fn(rhs, lhs, ...extra), expected); -} +async function fixture() { + const mock = await ethers.deployContract('$Math'); -contract('Math', function () { - const min = new BN('1234'); - const max = new BN('5678'); - const MAX_UINT256_SUB1 = MAX_UINT256.sub(new BN('1')); - const MAX_UINT256_SUB2 = MAX_UINT256.sub(new BN('2')); + // disambiguation, we use the version with explicit rounding + mock.$mulDiv = mock['$mulDiv(uint256,uint256,uint256,uint8)']; + mock.$sqrt = mock['$sqrt(uint256,uint8)']; + mock.$log2 = mock['$log2(uint256,uint8)']; + mock.$log10 = mock['$log10(uint256,uint8)']; + mock.$log256 = mock['$log256(uint256,uint8)']; + return { mock }; +} + +describe('Math', function () { beforeEach(async function () { - this.math = await Math.new(); + Object.assign(this, await loadFixture(fixture)); }); describe('tryAdd', function () { it('adds correctly', async function () { - const a = new BN('5678'); - const b = new BN('1234'); - - await testCommutativeIterable(this.math.$tryAdd, a, b, [true, a.add(b)]); + const a = 5678n; + const b = 1234n; + await testCommutative(this.mock.$tryAdd, a, b, [true, a + b]); }); it('reverts on addition overflow', async function () { - const a = MAX_UINT256; - const b = new BN('1'); - - await testCommutativeIterable(this.math.$tryAdd, a, b, [false, '0']); + const a = ethers.MaxUint256; + const b = 1n; + await testCommutative(this.mock.$tryAdd, a, b, [false, 0n]); }); }); describe('trySub', function () { it('subtracts correctly', async function () { - const a = new BN('5678'); - const b = new BN('1234'); - - expectStruct(await this.math.$trySub(a, b), [true, a.sub(b)]); + const a = 5678n; + const b = 1234n; + expect(await this.mock.$trySub(a, b)).to.deep.equal([true, a - b]); }); it('reverts if subtraction result would be negative', async function () { - const a = new BN('1234'); - const b = new BN('5678'); - - expectStruct(await this.math.$trySub(a, b), [false, '0']); + const a = 1234n; + const b = 5678n; + expect(await this.mock.$trySub(a, b)).to.deep.equal([false, 0n]); }); }); describe('tryMul', function () { it('multiplies correctly', async function () { - const a = new BN('1234'); - const b = new BN('5678'); - - await testCommutativeIterable(this.math.$tryMul, a, b, [true, a.mul(b)]); + const a = 1234n; + const b = 5678n; + await testCommutative(this.mock.$tryMul, a, b, [true, a * b]); }); it('multiplies by zero correctly', async function () { - const a = new BN('0'); - const b = new BN('5678'); - - await testCommutativeIterable(this.math.$tryMul, a, b, [true, a.mul(b)]); + const a = 0n; + const b = 5678n; + await testCommutative(this.mock.$tryMul, a, b, [true, a * b]); }); it('reverts on multiplication overflow', async function () { - const a = MAX_UINT256; - const b = new BN('2'); - - await testCommutativeIterable(this.math.$tryMul, a, b, [false, '0']); + const a = ethers.MaxUint256; + const b = 2n; + await testCommutative(this.mock.$tryMul, a, b, [false, 0n]); }); }); describe('tryDiv', function () { it('divides correctly', async function () { - const a = new BN('5678'); - const b = new BN('5678'); - - expectStruct(await this.math.$tryDiv(a, b), [true, a.div(b)]); + const a = 5678n; + const b = 5678n; + expect(await this.mock.$tryDiv(a, b)).to.deep.equal([true, a / b]); }); it('divides zero correctly', async function () { - const a = new BN('0'); - const b = new BN('5678'); - - expectStruct(await this.math.$tryDiv(a, b), [true, a.div(b)]); + const a = 0n; + const b = 5678n; + expect(await this.mock.$tryDiv(a, b)).to.deep.equal([true, a / b]); }); it('returns complete number result on non-even division', async function () { - const a = new BN('7000'); - const b = new BN('5678'); - - expectStruct(await this.math.$tryDiv(a, b), [true, a.div(b)]); + const a = 7000n; + const b = 5678n; + expect(await this.mock.$tryDiv(a, b)).to.deep.equal([true, a / b]); }); it('reverts on division by zero', async function () { - const a = new BN('5678'); - const b = new BN('0'); - - expectStruct(await this.math.$tryDiv(a, b), [false, '0']); + const a = 5678n; + const b = 0n; + expect(await this.mock.$tryDiv(a, b)).to.deep.equal([false, 0n]); }); }); describe('tryMod', function () { - describe('modulos correctly', async function () { + describe('modulos correctly', function () { it('when the dividend is smaller than the divisor', async function () { - const a = new BN('284'); - const b = new BN('5678'); - - expectStruct(await this.math.$tryMod(a, b), [true, a.mod(b)]); + const a = 284n; + const b = 5678n; + expect(await this.mock.$tryMod(a, b)).to.deep.equal([true, a % b]); }); it('when the dividend is equal to the divisor', async function () { - const a = new BN('5678'); - const b = new BN('5678'); - - expectStruct(await this.math.$tryMod(a, b), [true, a.mod(b)]); + const a = 5678n; + const b = 5678n; + expect(await this.mock.$tryMod(a, b)).to.deep.equal([true, a % b]); }); it('when the dividend is larger than the divisor', async function () { - const a = new BN('7000'); - const b = new BN('5678'); - - expectStruct(await this.math.$tryMod(a, b), [true, a.mod(b)]); + const a = 7000n; + const b = 5678n; + expect(await this.mock.$tryMod(a, b)).to.deep.equal([true, a % b]); }); it('when the dividend is a multiple of the divisor', async function () { - const a = new BN('17034'); // 17034 == 5678 * 3 - const b = new BN('5678'); - - expectStruct(await this.math.$tryMod(a, b), [true, a.mod(b)]); + const a = 17034n; // 17034 == 5678 * 3 + const b = 5678n; + expect(await this.mock.$tryMod(a, b)).to.deep.equal([true, a % b]); }); }); it('reverts with a 0 divisor', async function () { - const a = new BN('5678'); - const b = new BN('0'); - - expectStruct(await this.math.$tryMod(a, b), [false, '0']); + const a = 5678n; + const b = 0n; + expect(await this.mock.$tryMod(a, b)).to.deep.equal([false, 0n]); }); }); describe('max', function () { - it('is correctly detected in first argument position', async function () { - expect(await this.math.$max(max, min)).to.be.bignumber.equal(max); - }); - - it('is correctly detected in second argument position', async function () { - expect(await this.math.$max(min, max)).to.be.bignumber.equal(max); + it('is correctly detected in both position', async function () { + await testCommutative(this.mock.$max, 1234n, 5678n, max(1234n, 5678n)); }); }); describe('min', function () { - it('is correctly detected in first argument position', async function () { - expect(await this.math.$min(min, max)).to.be.bignumber.equal(min); - }); - - it('is correctly detected in second argument position', async function () { - expect(await this.math.$min(max, min)).to.be.bignumber.equal(min); + it('is correctly detected in both position', async function () { + await testCommutative(this.mock.$min, 1234n, 5678n, min(1234n, 5678n)); }); }); describe('average', function () { - function bnAverage(a, b) { - return a.add(b).divn(2); - } - it('is correctly calculated with two odd numbers', async function () { - const a = new BN('57417'); - const b = new BN('95431'); - expect(await this.math.$average(a, b)).to.be.bignumber.equal(bnAverage(a, b)); + const a = 57417n; + const b = 95431n; + expect(await this.mock.$average(a, b)).to.equal((a + b) / 2n); }); it('is correctly calculated with two even numbers', async function () { - const a = new BN('42304'); - const b = new BN('84346'); - expect(await this.math.$average(a, b)).to.be.bignumber.equal(bnAverage(a, b)); + const a = 42304n; + const b = 84346n; + expect(await this.mock.$average(a, b)).to.equal((a + b) / 2n); }); it('is correctly calculated with one even and one odd number', async function () { - const a = new BN('57417'); - const b = new BN('84346'); - expect(await this.math.$average(a, b)).to.be.bignumber.equal(bnAverage(a, b)); + const a = 57417n; + const b = 84346n; + expect(await this.mock.$average(a, b)).to.equal((a + b) / 2n); }); it('is correctly calculated with two max uint256 numbers', async function () { - const a = MAX_UINT256; - expect(await this.math.$average(a, a)).to.be.bignumber.equal(bnAverage(a, a)); + const a = ethers.MaxUint256; + expect(await this.mock.$average(a, a)).to.equal(a); }); }); describe('ceilDiv', function () { it('reverts on zero division', async function () { - const a = new BN('2'); - const b = new BN('0'); + const a = 2n; + const b = 0n; // It's unspecified because it's a low level 0 division error - await expectRevert.unspecified(this.math.$ceilDiv(a, b)); + await expect(this.mock.$ceilDiv(a, b)).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO); }); it('does not round up a zero result', async function () { - const a = new BN('0'); - const b = new BN('2'); - expect(await this.math.$ceilDiv(a, b)).to.be.bignumber.equal('0'); + const a = 0n; + const b = 2n; + const r = 0n; + expect(await this.mock.$ceilDiv(a, b)).to.equal(r); }); it('does not round up on exact division', async function () { - const a = new BN('10'); - const b = new BN('5'); - expect(await this.math.$ceilDiv(a, b)).to.be.bignumber.equal('2'); + const a = 10n; + const b = 5n; + const r = 2n; + expect(await this.mock.$ceilDiv(a, b)).to.equal(r); }); it('rounds up on division with remainders', async function () { - const a = new BN('42'); - const b = new BN('13'); - expect(await this.math.$ceilDiv(a, b)).to.be.bignumber.equal('4'); + const a = 42n; + const b = 13n; + const r = 4n; + expect(await this.mock.$ceilDiv(a, b)).to.equal(r); }); it('does not overflow', async function () { - const b = new BN('2'); - const result = new BN('1').shln(255); - expect(await this.math.$ceilDiv(MAX_UINT256, b)).to.be.bignumber.equal(result); + const a = ethers.MaxUint256; + const b = 2n; + const r = 1n << 255n; + expect(await this.mock.$ceilDiv(a, b)).to.equal(r); }); it('correctly computes max uint256 divided by 1', async function () { - const b = new BN('1'); - expect(await this.math.$ceilDiv(MAX_UINT256, b)).to.be.bignumber.equal(MAX_UINT256); + const a = ethers.MaxUint256; + const b = 1n; + const r = ethers.MaxUint256; + expect(await this.mock.$ceilDiv(a, b)).to.equal(r); }); }); describe('muldiv', function () { it('divide by 0', async function () { - await expectRevert.unspecified(this.math.$mulDiv(1, 1, 0, Rounding.Floor)); + const a = 1n; + const b = 1n; + const c = 0n; + await expect(this.mock.$mulDiv(a, b, c, Rounding.Floor)).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO); }); it('reverts with result higher than 2 ^ 256', async function () { - await expectRevertCustomError(this.math.$mulDiv(5, MAX_UINT256, 2, Rounding.Floor), 'MathOverflowedMulDiv', []); + const a = 5n; + const b = ethers.MaxUint256; + const c = 2n; + await expect(this.mock.$mulDiv(a, b, c, Rounding.Floor)).to.be.revertedWithCustomError( + this.mock, + 'MathOverflowedMulDiv', + ); }); - describe('does round down', async function () { + describe('does round down', function () { it('small values', async function () { for (const rounding of RoundingDown) { - expect(await this.math.$mulDiv('3', '4', '5', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.$mulDiv('3', '5', '5', rounding)).to.be.bignumber.equal('3'); + expect(await this.mock.$mulDiv(3n, 4n, 5n, rounding)).to.equal(2n); + expect(await this.mock.$mulDiv(3n, 5n, 5n, rounding)).to.equal(3n); } }); it('large values', async function () { for (const rounding of RoundingDown) { - expect(await this.math.$mulDiv(new BN('42'), MAX_UINT256_SUB1, MAX_UINT256, rounding)).to.be.bignumber.equal( - new BN('41'), - ); + expect(await this.mock.$mulDiv(42n, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding)).to.equal(41n); - expect(await this.math.$mulDiv(new BN('17'), MAX_UINT256, MAX_UINT256, rounding)).to.be.bignumber.equal( - new BN('17'), - ); + expect(await this.mock.$mulDiv(17n, ethers.MaxUint256, ethers.MaxUint256, rounding)).to.equal(17n); expect( - await this.math.$mulDiv(MAX_UINT256_SUB1, MAX_UINT256_SUB1, MAX_UINT256, rounding), - ).to.be.bignumber.equal(MAX_UINT256_SUB2); + await this.mock.$mulDiv(ethers.MaxUint256 - 1n, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding), + ).to.equal(ethers.MaxUint256 - 2n); - expect(await this.math.$mulDiv(MAX_UINT256, MAX_UINT256_SUB1, MAX_UINT256, rounding)).to.be.bignumber.equal( - MAX_UINT256_SUB1, - ); + expect( + await this.mock.$mulDiv(ethers.MaxUint256, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding), + ).to.equal(ethers.MaxUint256 - 1n); - expect(await this.math.$mulDiv(MAX_UINT256, MAX_UINT256, MAX_UINT256, rounding)).to.be.bignumber.equal( - MAX_UINT256, + expect(await this.mock.$mulDiv(ethers.MaxUint256, ethers.MaxUint256, ethers.MaxUint256, rounding)).to.equal( + ethers.MaxUint256, ); } }); }); - describe('does round up', async function () { + describe('does round up', function () { it('small values', async function () { for (const rounding of RoundingUp) { - expect(await this.math.$mulDiv('3', '4', '5', rounding)).to.be.bignumber.equal('3'); - expect(await this.math.$mulDiv('3', '5', '5', rounding)).to.be.bignumber.equal('3'); + expect(await this.mock.$mulDiv(3n, 4n, 5n, rounding)).to.equal(3n); + expect(await this.mock.$mulDiv(3n, 5n, 5n, rounding)).to.equal(3n); } }); it('large values', async function () { for (const rounding of RoundingUp) { - expect(await this.math.$mulDiv(new BN('42'), MAX_UINT256_SUB1, MAX_UINT256, rounding)).to.be.bignumber.equal( - new BN('42'), - ); + expect(await this.mock.$mulDiv(42n, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding)).to.equal(42n); - expect(await this.math.$mulDiv(new BN('17'), MAX_UINT256, MAX_UINT256, rounding)).to.be.bignumber.equal( - new BN('17'), - ); + expect(await this.mock.$mulDiv(17n, ethers.MaxUint256, ethers.MaxUint256, rounding)).to.equal(17n); expect( - await this.math.$mulDiv(MAX_UINT256_SUB1, MAX_UINT256_SUB1, MAX_UINT256, rounding), - ).to.be.bignumber.equal(MAX_UINT256_SUB1); + await this.mock.$mulDiv(ethers.MaxUint256 - 1n, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding), + ).to.equal(ethers.MaxUint256 - 1n); - expect(await this.math.$mulDiv(MAX_UINT256, MAX_UINT256_SUB1, MAX_UINT256, rounding)).to.be.bignumber.equal( - MAX_UINT256_SUB1, - ); + expect( + await this.mock.$mulDiv(ethers.MaxUint256, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding), + ).to.equal(ethers.MaxUint256 - 1n); - expect(await this.math.$mulDiv(MAX_UINT256, MAX_UINT256, MAX_UINT256, rounding)).to.be.bignumber.equal( - MAX_UINT256, + expect(await this.mock.$mulDiv(ethers.MaxUint256, ethers.MaxUint256, ethers.MaxUint256, rounding)).to.equal( + ethers.MaxUint256, ); } }); @@ -324,39 +302,35 @@ contract('Math', function () { describe('sqrt', function () { it('rounds down', async function () { for (const rounding of RoundingDown) { - expect(await this.math.$sqrt('0', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.$sqrt('1', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.$sqrt('2', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.$sqrt('3', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.$sqrt('4', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.$sqrt('144', rounding)).to.be.bignumber.equal('12'); - expect(await this.math.$sqrt('999999', rounding)).to.be.bignumber.equal('999'); - expect(await this.math.$sqrt('1000000', rounding)).to.be.bignumber.equal('1000'); - expect(await this.math.$sqrt('1000001', rounding)).to.be.bignumber.equal('1000'); - expect(await this.math.$sqrt('1002000', rounding)).to.be.bignumber.equal('1000'); - expect(await this.math.$sqrt('1002001', rounding)).to.be.bignumber.equal('1001'); - expect(await this.math.$sqrt(MAX_UINT256, rounding)).to.be.bignumber.equal( - '340282366920938463463374607431768211455', - ); + expect(await this.mock.$sqrt(0n, rounding)).to.equal(0n); + expect(await this.mock.$sqrt(1n, rounding)).to.equal(1n); + expect(await this.mock.$sqrt(2n, rounding)).to.equal(1n); + expect(await this.mock.$sqrt(3n, rounding)).to.equal(1n); + expect(await this.mock.$sqrt(4n, rounding)).to.equal(2n); + expect(await this.mock.$sqrt(144n, rounding)).to.equal(12n); + expect(await this.mock.$sqrt(999999n, rounding)).to.equal(999n); + expect(await this.mock.$sqrt(1000000n, rounding)).to.equal(1000n); + expect(await this.mock.$sqrt(1000001n, rounding)).to.equal(1000n); + expect(await this.mock.$sqrt(1002000n, rounding)).to.equal(1000n); + expect(await this.mock.$sqrt(1002001n, rounding)).to.equal(1001n); + expect(await this.mock.$sqrt(ethers.MaxUint256, rounding)).to.equal(340282366920938463463374607431768211455n); } }); it('rounds up', async function () { for (const rounding of RoundingUp) { - expect(await this.math.$sqrt('0', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.$sqrt('1', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.$sqrt('2', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.$sqrt('3', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.$sqrt('4', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.$sqrt('144', rounding)).to.be.bignumber.equal('12'); - expect(await this.math.$sqrt('999999', rounding)).to.be.bignumber.equal('1000'); - expect(await this.math.$sqrt('1000000', rounding)).to.be.bignumber.equal('1000'); - expect(await this.math.$sqrt('1000001', rounding)).to.be.bignumber.equal('1001'); - expect(await this.math.$sqrt('1002000', rounding)).to.be.bignumber.equal('1001'); - expect(await this.math.$sqrt('1002001', rounding)).to.be.bignumber.equal('1001'); - expect(await this.math.$sqrt(MAX_UINT256, rounding)).to.be.bignumber.equal( - '340282366920938463463374607431768211456', - ); + expect(await this.mock.$sqrt(0n, rounding)).to.equal(0n); + expect(await this.mock.$sqrt(1n, rounding)).to.equal(1n); + expect(await this.mock.$sqrt(2n, rounding)).to.equal(2n); + expect(await this.mock.$sqrt(3n, rounding)).to.equal(2n); + expect(await this.mock.$sqrt(4n, rounding)).to.equal(2n); + expect(await this.mock.$sqrt(144n, rounding)).to.equal(12n); + expect(await this.mock.$sqrt(999999n, rounding)).to.equal(1000n); + expect(await this.mock.$sqrt(1000000n, rounding)).to.equal(1000n); + expect(await this.mock.$sqrt(1000001n, rounding)).to.equal(1001n); + expect(await this.mock.$sqrt(1002000n, rounding)).to.equal(1001n); + expect(await this.mock.$sqrt(1002001n, rounding)).to.equal(1001n); + expect(await this.mock.$sqrt(ethers.MaxUint256, rounding)).to.equal(340282366920938463463374607431768211456n); } }); }); @@ -365,33 +339,33 @@ contract('Math', function () { describe('log2', function () { it('rounds down', async function () { for (const rounding of RoundingDown) { - expect(await this.math.methods['$log2(uint256,uint8)']('0', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.methods['$log2(uint256,uint8)']('1', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.methods['$log2(uint256,uint8)']('2', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.methods['$log2(uint256,uint8)']('3', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.methods['$log2(uint256,uint8)']('4', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.methods['$log2(uint256,uint8)']('5', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.methods['$log2(uint256,uint8)']('6', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.methods['$log2(uint256,uint8)']('7', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.methods['$log2(uint256,uint8)']('8', rounding)).to.be.bignumber.equal('3'); - expect(await this.math.methods['$log2(uint256,uint8)']('9', rounding)).to.be.bignumber.equal('3'); - expect(await this.math.methods['$log2(uint256,uint8)'](MAX_UINT256, rounding)).to.be.bignumber.equal('255'); + expect(await this.mock.$log2(0n, rounding)).to.equal(0n); + expect(await this.mock.$log2(1n, rounding)).to.equal(0n); + expect(await this.mock.$log2(2n, rounding)).to.equal(1n); + expect(await this.mock.$log2(3n, rounding)).to.equal(1n); + expect(await this.mock.$log2(4n, rounding)).to.equal(2n); + expect(await this.mock.$log2(5n, rounding)).to.equal(2n); + expect(await this.mock.$log2(6n, rounding)).to.equal(2n); + expect(await this.mock.$log2(7n, rounding)).to.equal(2n); + expect(await this.mock.$log2(8n, rounding)).to.equal(3n); + expect(await this.mock.$log2(9n, rounding)).to.equal(3n); + expect(await this.mock.$log2(ethers.MaxUint256, rounding)).to.equal(255n); } }); it('rounds up', async function () { for (const rounding of RoundingUp) { - expect(await this.math.methods['$log2(uint256,uint8)']('0', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.methods['$log2(uint256,uint8)']('1', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.methods['$log2(uint256,uint8)']('2', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.methods['$log2(uint256,uint8)']('3', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.methods['$log2(uint256,uint8)']('4', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.methods['$log2(uint256,uint8)']('5', rounding)).to.be.bignumber.equal('3'); - expect(await this.math.methods['$log2(uint256,uint8)']('6', rounding)).to.be.bignumber.equal('3'); - expect(await this.math.methods['$log2(uint256,uint8)']('7', rounding)).to.be.bignumber.equal('3'); - expect(await this.math.methods['$log2(uint256,uint8)']('8', rounding)).to.be.bignumber.equal('3'); - expect(await this.math.methods['$log2(uint256,uint8)']('9', rounding)).to.be.bignumber.equal('4'); - expect(await this.math.methods['$log2(uint256,uint8)'](MAX_UINT256, rounding)).to.be.bignumber.equal('256'); + expect(await this.mock.$log2(0n, rounding)).to.equal(0n); + expect(await this.mock.$log2(1n, rounding)).to.equal(0n); + expect(await this.mock.$log2(2n, rounding)).to.equal(1n); + expect(await this.mock.$log2(3n, rounding)).to.equal(2n); + expect(await this.mock.$log2(4n, rounding)).to.equal(2n); + expect(await this.mock.$log2(5n, rounding)).to.equal(3n); + expect(await this.mock.$log2(6n, rounding)).to.equal(3n); + expect(await this.mock.$log2(7n, rounding)).to.equal(3n); + expect(await this.mock.$log2(8n, rounding)).to.equal(3n); + expect(await this.mock.$log2(9n, rounding)).to.equal(4n); + expect(await this.mock.$log2(ethers.MaxUint256, rounding)).to.equal(256n); } }); }); @@ -399,37 +373,37 @@ contract('Math', function () { describe('log10', function () { it('rounds down', async function () { for (const rounding of RoundingDown) { - expect(await this.math.$log10('0', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.$log10('1', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.$log10('2', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.$log10('9', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.$log10('10', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.$log10('11', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.$log10('99', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.$log10('100', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.$log10('101', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.$log10('999', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.$log10('1000', rounding)).to.be.bignumber.equal('3'); - expect(await this.math.$log10('1001', rounding)).to.be.bignumber.equal('3'); - expect(await this.math.$log10(MAX_UINT256, rounding)).to.be.bignumber.equal('77'); + expect(await this.mock.$log10(0n, rounding)).to.equal(0n); + expect(await this.mock.$log10(1n, rounding)).to.equal(0n); + expect(await this.mock.$log10(2n, rounding)).to.equal(0n); + expect(await this.mock.$log10(9n, rounding)).to.equal(0n); + expect(await this.mock.$log10(10n, rounding)).to.equal(1n); + expect(await this.mock.$log10(11n, rounding)).to.equal(1n); + expect(await this.mock.$log10(99n, rounding)).to.equal(1n); + expect(await this.mock.$log10(100n, rounding)).to.equal(2n); + expect(await this.mock.$log10(101n, rounding)).to.equal(2n); + expect(await this.mock.$log10(999n, rounding)).to.equal(2n); + expect(await this.mock.$log10(1000n, rounding)).to.equal(3n); + expect(await this.mock.$log10(1001n, rounding)).to.equal(3n); + expect(await this.mock.$log10(ethers.MaxUint256, rounding)).to.equal(77n); } }); it('rounds up', async function () { for (const rounding of RoundingUp) { - expect(await this.math.$log10('0', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.$log10('1', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.$log10('2', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.$log10('9', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.$log10('10', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.$log10('11', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.$log10('99', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.$log10('100', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.$log10('101', rounding)).to.be.bignumber.equal('3'); - expect(await this.math.$log10('999', rounding)).to.be.bignumber.equal('3'); - expect(await this.math.$log10('1000', rounding)).to.be.bignumber.equal('3'); - expect(await this.math.$log10('1001', rounding)).to.be.bignumber.equal('4'); - expect(await this.math.$log10(MAX_UINT256, rounding)).to.be.bignumber.equal('78'); + expect(await this.mock.$log10(0n, rounding)).to.equal(0n); + expect(await this.mock.$log10(1n, rounding)).to.equal(0n); + expect(await this.mock.$log10(2n, rounding)).to.equal(1n); + expect(await this.mock.$log10(9n, rounding)).to.equal(1n); + expect(await this.mock.$log10(10n, rounding)).to.equal(1n); + expect(await this.mock.$log10(11n, rounding)).to.equal(2n); + expect(await this.mock.$log10(99n, rounding)).to.equal(2n); + expect(await this.mock.$log10(100n, rounding)).to.equal(2n); + expect(await this.mock.$log10(101n, rounding)).to.equal(3n); + expect(await this.mock.$log10(999n, rounding)).to.equal(3n); + expect(await this.mock.$log10(1000n, rounding)).to.equal(3n); + expect(await this.mock.$log10(1001n, rounding)).to.equal(4n); + expect(await this.mock.$log10(ethers.MaxUint256, rounding)).to.equal(78n); } }); }); @@ -437,31 +411,31 @@ contract('Math', function () { describe('log256', function () { it('rounds down', async function () { for (const rounding of RoundingDown) { - expect(await this.math.$log256('0', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.$log256('1', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.$log256('2', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.$log256('255', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.$log256('256', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.$log256('257', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.$log256('65535', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.$log256('65536', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.$log256('65537', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.$log256(MAX_UINT256, rounding)).to.be.bignumber.equal('31'); + expect(await this.mock.$log256(0n, rounding)).to.equal(0n); + expect(await this.mock.$log256(1n, rounding)).to.equal(0n); + expect(await this.mock.$log256(2n, rounding)).to.equal(0n); + expect(await this.mock.$log256(255n, rounding)).to.equal(0n); + expect(await this.mock.$log256(256n, rounding)).to.equal(1n); + expect(await this.mock.$log256(257n, rounding)).to.equal(1n); + expect(await this.mock.$log256(65535n, rounding)).to.equal(1n); + expect(await this.mock.$log256(65536n, rounding)).to.equal(2n); + expect(await this.mock.$log256(65537n, rounding)).to.equal(2n); + expect(await this.mock.$log256(ethers.MaxUint256, rounding)).to.equal(31n); } }); it('rounds up', async function () { for (const rounding of RoundingUp) { - expect(await this.math.$log256('0', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.$log256('1', rounding)).to.be.bignumber.equal('0'); - expect(await this.math.$log256('2', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.$log256('255', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.$log256('256', rounding)).to.be.bignumber.equal('1'); - expect(await this.math.$log256('257', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.$log256('65535', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.$log256('65536', rounding)).to.be.bignumber.equal('2'); - expect(await this.math.$log256('65537', rounding)).to.be.bignumber.equal('3'); - expect(await this.math.$log256(MAX_UINT256, rounding)).to.be.bignumber.equal('32'); + expect(await this.mock.$log256(0n, rounding)).to.equal(0n); + expect(await this.mock.$log256(1n, rounding)).to.equal(0n); + expect(await this.mock.$log256(2n, rounding)).to.equal(1n); + expect(await this.mock.$log256(255n, rounding)).to.equal(1n); + expect(await this.mock.$log256(256n, rounding)).to.equal(1n); + expect(await this.mock.$log256(257n, rounding)).to.equal(2n); + expect(await this.mock.$log256(65535n, rounding)).to.equal(2n); + expect(await this.mock.$log256(65536n, rounding)).to.equal(2n); + expect(await this.mock.$log256(65537n, rounding)).to.equal(3n); + expect(await this.mock.$log256(ethers.MaxUint256, rounding)).to.equal(32n); } }); }); diff --git a/test/utils/math/SafeCast.test.js b/test/utils/math/SafeCast.test.js index 4b8ec5a7203..dd04f75ba1a 100644 --- a/test/utils/math/SafeCast.test.js +++ b/test/utils/math/SafeCast.test.js @@ -1,161 +1,148 @@ -const { BN } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { range } = require('../../../scripts/helpers'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const SafeCast = artifacts.require('$SafeCast'); +async function fixture() { + const mock = await ethers.deployContract('$SafeCast'); + return { mock }; +} -contract('SafeCast', async function () { +contract('SafeCast', function () { beforeEach(async function () { - this.safeCast = await SafeCast.new(); + Object.assign(this, await loadFixture(fixture)); }); - function testToUint(bits) { - describe(`toUint${bits}`, () => { - const maxValue = new BN('2').pow(new BN(bits)).subn(1); + for (const bits of range(8, 256, 8).map(ethers.toBigInt)) { + const maxValue = 2n ** bits - 1n; + describe(`toUint${bits}`, () => { it('downcasts 0', async function () { - expect(await this.safeCast[`$toUint${bits}`](0)).to.be.bignumber.equal('0'); + expect(await this.mock[`$toUint${bits}`](0n)).is.equal(0n); }); it('downcasts 1', async function () { - expect(await this.safeCast[`$toUint${bits}`](1)).to.be.bignumber.equal('1'); + expect(await this.mock[`$toUint${bits}`](1n)).is.equal(1n); }); it(`downcasts 2^${bits} - 1 (${maxValue})`, async function () { - expect(await this.safeCast[`$toUint${bits}`](maxValue)).to.be.bignumber.equal(maxValue); + expect(await this.mock[`$toUint${bits}`](maxValue)).is.equal(maxValue); }); - it(`reverts when downcasting 2^${bits} (${maxValue.addn(1)})`, async function () { - await expectRevertCustomError( - this.safeCast[`$toUint${bits}`](maxValue.addn(1)), - `SafeCastOverflowedUintDowncast`, - [bits, maxValue.addn(1)], - ); + it(`reverts when downcasting 2^${bits} (${maxValue + 1n})`, async function () { + await expect(this.mock[`$toUint${bits}`](maxValue + 1n)) + .to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedUintDowncast') + .withArgs(bits, maxValue + 1n); }); - it(`reverts when downcasting 2^${bits} + 1 (${maxValue.addn(2)})`, async function () { - await expectRevertCustomError( - this.safeCast[`$toUint${bits}`](maxValue.addn(2)), - `SafeCastOverflowedUintDowncast`, - [bits, maxValue.addn(2)], - ); + it(`reverts when downcasting 2^${bits} + 1 (${maxValue + 2n})`, async function () { + await expect(this.mock[`$toUint${bits}`](maxValue + 2n)) + .to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedUintDowncast') + .withArgs(bits, maxValue + 2n); }); }); } - range(8, 256, 8).forEach(bits => testToUint(bits)); - describe('toUint256', () => { - const maxInt256 = new BN('2').pow(new BN(255)).subn(1); - const minInt256 = new BN('2').pow(new BN(255)).neg(); - it('casts 0', async function () { - expect(await this.safeCast.$toUint256(0)).to.be.bignumber.equal('0'); + expect(await this.mock.$toUint256(0n)).is.equal(0n); }); it('casts 1', async function () { - expect(await this.safeCast.$toUint256(1)).to.be.bignumber.equal('1'); + expect(await this.mock.$toUint256(1n)).is.equal(1n); }); - it(`casts INT256_MAX (${maxInt256})`, async function () { - expect(await this.safeCast.$toUint256(maxInt256)).to.be.bignumber.equal(maxInt256); + it(`casts INT256_MAX (${ethers.MaxInt256})`, async function () { + expect(await this.mock.$toUint256(ethers.MaxInt256)).is.equal(ethers.MaxInt256); }); it('reverts when casting -1', async function () { - await expectRevertCustomError(this.safeCast.$toUint256(-1), `SafeCastOverflowedIntToUint`, [-1]); + await expect(this.mock.$toUint256(-1n)) + .to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedIntToUint') + .withArgs(-1n); }); - it(`reverts when casting INT256_MIN (${minInt256})`, async function () { - await expectRevertCustomError(this.safeCast.$toUint256(minInt256), `SafeCastOverflowedIntToUint`, [minInt256]); + it(`reverts when casting INT256_MIN (${ethers.MinInt256})`, async function () { + await expect(this.mock.$toUint256(ethers.MinInt256)) + .to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedIntToUint') + .withArgs(ethers.MinInt256); }); }); - function testToInt(bits) { - describe(`toInt${bits}`, () => { - const minValue = new BN('-2').pow(new BN(bits - 1)); - const maxValue = new BN('2').pow(new BN(bits - 1)).subn(1); + for (const bits of range(8, 256, 8).map(ethers.toBigInt)) { + const minValue = -(2n ** (bits - 1n)); + const maxValue = 2n ** (bits - 1n) - 1n; + describe(`toInt${bits}`, () => { it('downcasts 0', async function () { - expect(await this.safeCast[`$toInt${bits}`](0)).to.be.bignumber.equal('0'); + expect(await this.mock[`$toInt${bits}`](0n)).is.equal(0n); }); it('downcasts 1', async function () { - expect(await this.safeCast[`$toInt${bits}`](1)).to.be.bignumber.equal('1'); + expect(await this.mock[`$toInt${bits}`](1n)).is.equal(1n); }); it('downcasts -1', async function () { - expect(await this.safeCast[`$toInt${bits}`](-1)).to.be.bignumber.equal('-1'); + expect(await this.mock[`$toInt${bits}`](-1n)).is.equal(-1n); }); - it(`downcasts -2^${bits - 1} (${minValue})`, async function () { - expect(await this.safeCast[`$toInt${bits}`](minValue)).to.be.bignumber.equal(minValue); + it(`downcasts -2^${bits - 1n} (${minValue})`, async function () { + expect(await this.mock[`$toInt${bits}`](minValue)).is.equal(minValue); }); - it(`downcasts 2^${bits - 1} - 1 (${maxValue})`, async function () { - expect(await this.safeCast[`$toInt${bits}`](maxValue)).to.be.bignumber.equal(maxValue); + it(`downcasts 2^${bits - 1n} - 1 (${maxValue})`, async function () { + expect(await this.mock[`$toInt${bits}`](maxValue)).is.equal(maxValue); }); - it(`reverts when downcasting -2^${bits - 1} - 1 (${minValue.subn(1)})`, async function () { - await expectRevertCustomError( - this.safeCast[`$toInt${bits}`](minValue.subn(1)), - `SafeCastOverflowedIntDowncast`, - [bits, minValue.subn(1)], - ); + it(`reverts when downcasting -2^${bits - 1n} - 1 (${minValue - 1n})`, async function () { + await expect(this.mock[`$toInt${bits}`](minValue - 1n)) + .to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedIntDowncast') + .withArgs(bits, minValue - 1n); }); - it(`reverts when downcasting -2^${bits - 1} - 2 (${minValue.subn(2)})`, async function () { - await expectRevertCustomError( - this.safeCast[`$toInt${bits}`](minValue.subn(2)), - `SafeCastOverflowedIntDowncast`, - [bits, minValue.subn(2)], - ); + it(`reverts when downcasting -2^${bits - 1n} - 2 (${minValue - 2n})`, async function () { + await expect(this.mock[`$toInt${bits}`](minValue - 2n)) + .to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedIntDowncast') + .withArgs(bits, minValue - 2n); }); - it(`reverts when downcasting 2^${bits - 1} (${maxValue.addn(1)})`, async function () { - await expectRevertCustomError( - this.safeCast[`$toInt${bits}`](maxValue.addn(1)), - `SafeCastOverflowedIntDowncast`, - [bits, maxValue.addn(1)], - ); + it(`reverts when downcasting 2^${bits - 1n} (${maxValue + 1n})`, async function () { + await expect(this.mock[`$toInt${bits}`](maxValue + 1n)) + .to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedIntDowncast') + .withArgs(bits, maxValue + 1n); }); - it(`reverts when downcasting 2^${bits - 1} + 1 (${maxValue.addn(2)})`, async function () { - await expectRevertCustomError( - this.safeCast[`$toInt${bits}`](maxValue.addn(2)), - `SafeCastOverflowedIntDowncast`, - [bits, maxValue.addn(2)], - ); + it(`reverts when downcasting 2^${bits - 1n} + 1 (${maxValue + 2n})`, async function () { + await expect(this.mock[`$toInt${bits}`](maxValue + 2n)) + .to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedIntDowncast') + .withArgs(bits, maxValue + 2n); }); }); } - range(8, 256, 8).forEach(bits => testToInt(bits)); - describe('toInt256', () => { - const maxUint256 = new BN('2').pow(new BN(256)).subn(1); - const maxInt256 = new BN('2').pow(new BN(255)).subn(1); - it('casts 0', async function () { - expect(await this.safeCast.$toInt256(0)).to.be.bignumber.equal('0'); + expect(await this.mock.$toInt256(0)).is.equal(0n); }); it('casts 1', async function () { - expect(await this.safeCast.$toInt256(1)).to.be.bignumber.equal('1'); + expect(await this.mock.$toInt256(1)).is.equal(1n); }); - it(`casts INT256_MAX (${maxInt256})`, async function () { - expect(await this.safeCast.$toInt256(maxInt256)).to.be.bignumber.equal(maxInt256); + it(`casts INT256_MAX (${ethers.MaxInt256})`, async function () { + expect(await this.mock.$toInt256(ethers.MaxInt256)).is.equal(ethers.MaxInt256); }); - it(`reverts when casting INT256_MAX + 1 (${maxInt256.addn(1)})`, async function () { - await expectRevertCustomError(this.safeCast.$toInt256(maxInt256.addn(1)), 'SafeCastOverflowedUintToInt', [ - maxInt256.addn(1), - ]); + it(`reverts when casting INT256_MAX + 1 (${ethers.MaxInt256 + 1n})`, async function () { + await expect(this.mock.$toInt256(ethers.MaxInt256 + 1n)) + .to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedUintToInt') + .withArgs(ethers.MaxInt256 + 1n); }); - it(`reverts when casting UINT256_MAX (${maxUint256})`, async function () { - await expectRevertCustomError(this.safeCast.$toInt256(maxUint256), 'SafeCastOverflowedUintToInt', [maxUint256]); + it(`reverts when casting UINT256_MAX (${ethers.MaxUint256})`, async function () { + await expect(this.mock.$toInt256(ethers.MaxUint256)) + .to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedUintToInt') + .withArgs(ethers.MaxUint256); }); }); }); diff --git a/test/utils/math/SignedMath.test.js b/test/utils/math/SignedMath.test.js index c014e22ba9d..253e7235752 100644 --- a/test/utils/math/SignedMath.test.js +++ b/test/utils/math/SignedMath.test.js @@ -1,94 +1,51 @@ -const { BN, constants } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { MIN_INT256, MAX_INT256 } = constants; +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { min, max } = require('../../helpers/math'); -const SignedMath = artifacts.require('$SignedMath'); +async function testCommutative(fn, lhs, rhs, expected, ...extra) { + expect(await fn(lhs, rhs, ...extra)).to.deep.equal(expected); + expect(await fn(rhs, lhs, ...extra)).to.deep.equal(expected); +} -contract('SignedMath', function () { - const min = new BN('-1234'); - const max = new BN('5678'); +async function fixture() { + const mock = await ethers.deployContract('$SignedMath'); + return { mock }; +} +contract('SignedMath', function () { beforeEach(async function () { - this.math = await SignedMath.new(); + Object.assign(this, await loadFixture(fixture)); }); describe('max', function () { - it('is correctly detected in first argument position', async function () { - expect(await this.math.$max(max, min)).to.be.bignumber.equal(max); - }); - - it('is correctly detected in second argument position', async function () { - expect(await this.math.$max(min, max)).to.be.bignumber.equal(max); + it('is correctly detected in both position', async function () { + await testCommutative(this.mock.$max, -1234n, 5678n, max(-1234n, 5678n)); }); }); describe('min', function () { - it('is correctly detected in first argument position', async function () { - expect(await this.math.$min(min, max)).to.be.bignumber.equal(min); - }); - - it('is correctly detected in second argument position', async function () { - expect(await this.math.$min(max, min)).to.be.bignumber.equal(min); + it('is correctly detected in both position', async function () { + await testCommutative(this.mock.$min, -1234n, 5678n, min(-1234n, 5678n)); }); }); describe('average', function () { - function bnAverage(a, b) { - return a.add(b).divn(2); - } - it('is correctly calculated with various input', async function () { - const valuesX = [ - new BN('0'), - new BN('3'), - new BN('-3'), - new BN('4'), - new BN('-4'), - new BN('57417'), - new BN('-57417'), - new BN('42304'), - new BN('-42304'), - MIN_INT256, - MAX_INT256, - ]; - - const valuesY = [ - new BN('0'), - new BN('5'), - new BN('-5'), - new BN('2'), - new BN('-2'), - new BN('57417'), - new BN('-57417'), - new BN('42304'), - new BN('-42304'), - MIN_INT256, - MAX_INT256, - ]; - - for (const x of valuesX) { - for (const y of valuesY) { - expect(await this.math.$average(x, y)).to.be.bignumber.equal( - bnAverage(x, y), - `Bad result for average(${x}, ${y})`, - ); + for (const x of [ethers.MinInt256, -57417n, -42304n, -4n, -3n, 0n, 3n, 4n, 42304n, 57417n, ethers.MaxInt256]) { + for (const y of [ethers.MinInt256, -57417n, -42304n, -5n, -2n, 0n, 2n, 5n, 42304n, 57417n, ethers.MaxInt256]) { + expect(await this.mock.$average(x, y)).to.equal((x + y) / 2n); } } }); }); describe('abs', function () { - for (const n of [ - MIN_INT256, - MIN_INT256.addn(1), - new BN('-1'), - new BN('0'), - new BN('1'), - MAX_INT256.subn(1), - MAX_INT256, - ]) { + const abs = x => (x < 0n ? -x : x); + + for (const n of [ethers.MinInt256, ethers.MinInt256 + 1n, -1n, 0n, 1n, ethers.MaxInt256 - 1n, ethers.MaxInt256]) { it(`correctly computes the absolute value of ${n}`, async function () { - expect(await this.math.$abs(n)).to.be.bignumber.equal(n.abs()); + expect(await this.mock.$abs(n)).to.equal(abs(n)); }); } }); diff --git a/test/utils/types/Time.test.js b/test/utils/types/Time.test.js index 614911738d2..d30daffc2c4 100644 --- a/test/utils/types/Time.test.js +++ b/test/utils/types/Time.test.js @@ -2,7 +2,8 @@ require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { clock } = require('../../helpers/time'); -const { product, max } = require('../../helpers/iterate'); +const { product } = require('../../helpers/iterate'); +const { max } = require('../../helpers/math'); const Time = artifacts.require('$Time'); From 3af62716dded52b323c688da0721099a420adfb8 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 7 Dec 2023 12:37:52 -0600 Subject: [PATCH 31/44] Make Multicall context-aware --- .changeset/rude-weeks-beg.md | 5 +++++ .changeset/strong-points-invent.md | 5 +++++ contracts/metatx/ERC2771Context.sol | 29 +++++++++++++++++-------- contracts/mocks/ERC2771ContextMock.sol | 7 +++++- contracts/utils/Context.sol | 4 ++++ contracts/utils/Multicall.sol | 18 ++++++++++++++-- test/metatx/ERC2771Context.test.js | 30 ++++++++++++++++++++++++-- 7 files changed, 84 insertions(+), 14 deletions(-) create mode 100644 .changeset/rude-weeks-beg.md create mode 100644 .changeset/strong-points-invent.md diff --git a/.changeset/rude-weeks-beg.md b/.changeset/rude-weeks-beg.md new file mode 100644 index 00000000000..77fe423c64f --- /dev/null +++ b/.changeset/rude-weeks-beg.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ERC2771Context` and `Context`: Introduce a `_contextPrefixLength()` getter, used to trim extra information appended to `msg.data`. diff --git a/.changeset/strong-points-invent.md b/.changeset/strong-points-invent.md new file mode 100644 index 00000000000..980000c4245 --- /dev/null +++ b/.changeset/strong-points-invent.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`Multicall`: Make aware of non-canonical context (i.e. `msg.sender` is not `_msgSender()`), allowing compatibility with `ERC2771Context`. diff --git a/contracts/metatx/ERC2771Context.sol b/contracts/metatx/ERC2771Context.sol index 0724a0b697c..2c17f10e446 100644 --- a/contracts/metatx/ERC2771Context.sol +++ b/contracts/metatx/ERC2771Context.sol @@ -13,6 +13,10 @@ import {Context} from "../utils/Context.sol"; * specification adding the address size in bytes (20) to the calldata size. An example of an unexpected * behavior could be an unintended fallback (or another function) invocation while trying to invoke the `receive` * function only accessible if `msg.data.length == 0`. + * + * WARNING: The usage of `delegatecall` in this contract is dangerous and may result in context corruption. + * Any forwarded request to this contract triggering a `delegatecall` to itself will result in an invalid {_msgSender} + * recovery. */ abstract contract ERC2771Context is Context { /// @custom:oz-upgrades-unsafe-allow state-variable-immutable @@ -48,13 +52,11 @@ abstract contract ERC2771Context is Context { * a call is not performed by the trusted forwarder or the calldata length is less than * 20 bytes (an address length). */ - function _msgSender() internal view virtual override returns (address sender) { - if (isTrustedForwarder(msg.sender) && msg.data.length >= 20) { - // The assembly code is more direct than the Solidity version using `abi.decode`. - /// @solidity memory-safe-assembly - assembly { - sender := shr(96, calldataload(sub(calldatasize(), 20))) - } + function _msgSender() internal view virtual override returns (address) { + uint256 calldataLength = msg.data.length; + uint256 contextSuffixLength = _contextSuffixLength(); + if (isTrustedForwarder(msg.sender) && calldataLength >= contextSuffixLength) { + return address(bytes20(msg.data[calldataLength - contextSuffixLength:])); } else { return super._msgSender(); } @@ -66,10 +68,19 @@ abstract contract ERC2771Context is Context { * 20 bytes (an address length). */ function _msgData() internal view virtual override returns (bytes calldata) { - if (isTrustedForwarder(msg.sender) && msg.data.length >= 20) { - return msg.data[:msg.data.length - 20]; + uint256 calldataLength = msg.data.length; + uint256 contextSuffixLength = _contextSuffixLength(); + if (isTrustedForwarder(msg.sender) && calldataLength >= contextSuffixLength) { + return msg.data[:calldataLength - contextSuffixLength]; } else { return super._msgData(); } } + + /** + * @dev ERC-2771 specifies the context as being a single address (20 bytes). + */ + function _contextSuffixLength() internal view virtual override returns (uint256) { + return 20; + } } diff --git a/contracts/mocks/ERC2771ContextMock.sol b/contracts/mocks/ERC2771ContextMock.sol index 22b9203e7a3..33887cf4575 100644 --- a/contracts/mocks/ERC2771ContextMock.sol +++ b/contracts/mocks/ERC2771ContextMock.sol @@ -4,10 +4,11 @@ pragma solidity ^0.8.20; import {ContextMock} from "./ContextMock.sol"; import {Context} from "../utils/Context.sol"; +import {Multicall} from "../utils/Multicall.sol"; import {ERC2771Context} from "../metatx/ERC2771Context.sol"; // By inheriting from ERC2771Context, Context's internal functions are overridden automatically -contract ERC2771ContextMock is ContextMock, ERC2771Context { +contract ERC2771ContextMock is ContextMock, ERC2771Context, Multicall { /// @custom:oz-upgrades-unsafe-allow constructor constructor(address trustedForwarder) ERC2771Context(trustedForwarder) { emit Sender(_msgSender()); // _msgSender() should be accessible during construction @@ -20,4 +21,8 @@ contract ERC2771ContextMock is ContextMock, ERC2771Context { function _msgData() internal view override(Context, ERC2771Context) returns (bytes calldata) { return ERC2771Context._msgData(); } + + function _contextSuffixLength() internal view override(Context, ERC2771Context) returns (uint256) { + return ERC2771Context._contextSuffixLength(); + } } diff --git a/contracts/utils/Context.sol b/contracts/utils/Context.sol index 9037dcd149c..f881fb6f781 100644 --- a/contracts/utils/Context.sol +++ b/contracts/utils/Context.sol @@ -21,4 +21,8 @@ abstract contract Context { function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } + + function _contextSuffixLength() internal view virtual returns (uint256) { + return 0; + } } diff --git a/contracts/utils/Multicall.sol b/contracts/utils/Multicall.sol index 2d925a91eed..91c2323ca50 100644 --- a/contracts/utils/Multicall.sol +++ b/contracts/utils/Multicall.sol @@ -4,19 +4,33 @@ pragma solidity ^0.8.20; import {Address} from "./Address.sol"; +import {Context} from "./Context.sol"; /** * @dev Provides a function to batch together multiple calls in a single external call. + * + * Consider any assumption about calldata validation performed by the sender may be violated if it's not especially + * careful about sending transactions invoking {multicall}. For example, a relay address that filters function + * selectors won't filter calls nested within a {multicall} operation. + * + * NOTE: Since 5.0.1 and 4.9.4, this contract identifies non-canonical contexts (i.e. `msg.sender` is not {_msgSender}). + * If a non-canonical context is identified, the following self `delegatecall` appends the last bytes of `msg.data` + * to the subcall. This makes it safe to use with {ERC2771Context}. Contexts that don't affect the resolution of + * {_msgSender} are not propagated to subcalls. */ -abstract contract Multicall { +abstract contract Multicall is Context { /** * @dev Receives and executes a batch of function calls on this contract. * @custom:oz-upgrades-unsafe-allow-reachable delegatecall */ function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results) { + bytes memory context = msg.sender == _msgSender() + ? new bytes(0) + : msg.data[msg.data.length - _contextSuffixLength():]; + results = new bytes[](data.length); for (uint256 i = 0; i < data.length; i++) { - results[i] = Address.functionDelegateCall(address(this), data[i]); + results[i] = Address.functionDelegateCall(address(this), bytes.concat(data[i], context)); } return results; } diff --git a/test/metatx/ERC2771Context.test.js b/test/metatx/ERC2771Context.test.js index bb6718ce229..1ae48e6f273 100644 --- a/test/metatx/ERC2771Context.test.js +++ b/test/metatx/ERC2771Context.test.js @@ -9,7 +9,7 @@ const { MAX_UINT48 } = require('../helpers/constants'); const { shouldBehaveLikeRegularContext } = require('../utils/Context.behavior'); async function fixture() { - const [sender] = await ethers.getSigners(); + const [sender, other] = await ethers.getSigners(); const forwarder = await ethers.deployContract('ERC2771Forwarder', []); const forwarderAsSigner = await impersonate(forwarder.target); @@ -27,7 +27,7 @@ async function fixture() { ], }; - return { sender, forwarder, forwarderAsSigner, context, domain, types }; + return { sender, other, forwarder, forwarderAsSigner, context, domain, types }; } describe('ERC2771Context', function () { @@ -114,4 +114,30 @@ describe('ERC2771Context', function () { .withArgs(data); }); }); + + it('multicall poison attack', async function () { + const nonce = await this.forwarder.nonces(this.sender); + const data = this.context.interface.encodeFunctionData('multicall', [ + [ + // poisonned call to 'msgSender()' + ethers.concat([this.context.interface.encodeFunctionData('msgSender'), this.other.address]), + ], + ]); + + const req = { + from: await this.sender.getAddress(), + to: await this.context.getAddress(), + value: 0n, + data, + gas: 100000n, + nonce, + deadline: MAX_UINT48, + }; + + req.signature = await this.sender.signTypedData(this.domain, this.types, req); + + expect(await this.forwarder.verify(req)).to.equal(true); + + await expect(this.forwarder.execute(req)).to.emit(this.context, 'Sender').withArgs(this.sender.address); + }); }); From 6ba452dea4258afe77726293435f10baf2bed265 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Dec 2023 15:47:12 +0000 Subject: [PATCH 32/44] Merge release-v5.0 branch (#4787) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: Francisco Giordano Co-authored-by: Ernesto García Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Eric Lau Co-authored-by: Hadrien Croubois Co-authored-by: Zack Reneau-Wedeen --- CHANGELOG.md | 6 ++++++ contracts/metatx/ERC2771Context.sol | 2 +- contracts/package.json | 2 +- contracts/token/ERC1155/IERC1155.sol | 2 +- contracts/utils/Context.sol | 2 +- contracts/utils/Multicall.sol | 2 +- lib/forge-std | 2 +- package.json | 2 +- 8 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf9a27a1a97..68afd7edd99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog + +## 5.0.1 (2023-12-07) + +- `ERC2771Context` and `Context`: Introduce a `_contextPrefixLength()` getter, used to trim extra information appended to `msg.data`. +- `Multicall`: Make aware of non-canonical context (i.e. `msg.sender` is not `_msgSender()`), allowing compatibility with `ERC2771Context`. + ## 5.0.0 (2023-10-05) ### Additions Summary diff --git a/contracts/metatx/ERC2771Context.sol b/contracts/metatx/ERC2771Context.sol index 2c17f10e446..d448b24b128 100644 --- a/contracts/metatx/ERC2771Context.sol +++ b/contracts/metatx/ERC2771Context.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (metatx/ERC2771Context.sol) +// OpenZeppelin Contracts (last updated v5.0.1) (metatx/ERC2771Context.sol) pragma solidity ^0.8.20; diff --git a/contracts/package.json b/contracts/package.json index be3e741e339..6ab89138a25 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,7 +1,7 @@ { "name": "@openzeppelin/contracts", "description": "Secure Smart Contract library for Solidity", - "version": "5.0.0", + "version": "5.0.1", "files": [ "**/*.sol", "/build/contracts/*.json", diff --git a/contracts/token/ERC1155/IERC1155.sol b/contracts/token/ERC1155/IERC1155.sol index 62ad4a9bd2c..db865a72f1a 100644 --- a/contracts/token/ERC1155/IERC1155.sol +++ b/contracts/token/ERC1155/IERC1155.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC1155/IERC1155.sol) +// OpenZeppelin Contracts (last updated v5.0.1) (token/ERC1155/IERC1155.sol) pragma solidity ^0.8.20; diff --git a/contracts/utils/Context.sol b/contracts/utils/Context.sol index f881fb6f781..4e535fe03c2 100644 --- a/contracts/utils/Context.sol +++ b/contracts/utils/Context.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/Context.sol) +// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol) pragma solidity ^0.8.20; diff --git a/contracts/utils/Multicall.sol b/contracts/utils/Multicall.sol index 91c2323ca50..0dd5b4adc37 100644 --- a/contracts/utils/Multicall.sol +++ b/contracts/utils/Multicall.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/Multicall.sol) +// OpenZeppelin Contracts (last updated v5.0.1) (utils/Multicall.sol) pragma solidity ^0.8.20; diff --git a/lib/forge-std b/lib/forge-std index eb980e1d4f0..c2236853aad 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit eb980e1d4f0e8173ec27da77297ae411840c8ccb +Subproject commit c2236853aadb8e2d9909bbecdc490099519b70a4 diff --git a/package.json b/package.json index 7eb4603888e..d1005679ca7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openzeppelin-solidity", "description": "Secure Smart Contract library for Solidity", - "version": "5.0.0", + "version": "5.0.1", "private": true, "files": [ "/contracts/**/*.sol", From 88512b23d2bf5714b5d1bd59feee1d912439689d Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Wed, 13 Dec 2023 19:31:56 -0300 Subject: [PATCH 33/44] Migrate ERC20 extensions tests to ethers v6 (#4773) Co-authored-by: Hadrien Croubois Co-authored-by: ernestognw --- test/governance/Governor.test.js | 6 +- .../extensions/GovernorWithParams.test.js | 6 +- test/governance/utils/Votes.behavior.js | 6 +- test/helpers/eip712-types.js | 96 +- test/helpers/eip712.js | 2 +- test/helpers/time.js | 9 +- test/metatx/ERC2771Context.test.js | 14 +- test/metatx/ERC2771Forwarder.test.js | 14 +- .../extensions/ERC20Burnable.behavior.js | 116 -- .../ERC20/extensions/ERC20Burnable.test.js | 110 +- .../ERC20/extensions/ERC20Capped.behavior.js | 31 - .../ERC20/extensions/ERC20Capped.test.js | 57 +- .../ERC20/extensions/ERC20FlashMint.test.js | 248 ++- .../ERC20/extensions/ERC20Pausable.test.js | 103 +- .../ERC20/extensions/ERC20Permit.test.js | 158 +- .../token/ERC20/extensions/ERC20Votes.test.js | 6 +- .../ERC20/extensions/ERC20Wrapper.test.js | 19 +- test/token/ERC20/extensions/ERC4626.test.js | 1427 +++++++---------- test/utils/cryptography/EIP712.test.js | 9 +- 19 files changed, 1046 insertions(+), 1391 deletions(-) delete mode 100644 test/token/ERC20/extensions/ERC20Burnable.behavior.js delete mode 100644 test/token/ERC20/extensions/ERC20Capped.behavior.js diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js index 71e80d7379f..b277d8c1241 100644 --- a/test/governance/Governor.test.js +++ b/test/governance/Governor.test.js @@ -4,11 +4,7 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; const Enums = require('../helpers/enums'); -const { - getDomain, - domainType, - types: { Ballot }, -} = require('../helpers/eip712'); +const { getDomain, domainType, Ballot } = require('../helpers/eip712'); const { GovernorHelper, proposalStatesToBitMap } = require('../helpers/governance'); const { clockFromReceipt } = require('../helpers/time'); const { expectRevertCustomError } = require('../helpers/customError'); diff --git a/test/governance/extensions/GovernorWithParams.test.js b/test/governance/extensions/GovernorWithParams.test.js index da392b3ea98..bbac688a23c 100644 --- a/test/governance/extensions/GovernorWithParams.test.js +++ b/test/governance/extensions/GovernorWithParams.test.js @@ -4,11 +4,7 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; const Enums = require('../../helpers/enums'); -const { - getDomain, - domainType, - types: { ExtendedBallot }, -} = require('../../helpers/eip712'); +const { getDomain, domainType, ExtendedBallot } = require('../../helpers/eip712'); const { GovernorHelper } = require('../../helpers/governance'); const { expectRevertCustomError } = require('../../helpers/customError'); diff --git a/test/governance/utils/Votes.behavior.js b/test/governance/utils/Votes.behavior.js index 68445f0fb65..6243cf4e447 100644 --- a/test/governance/utils/Votes.behavior.js +++ b/test/governance/utils/Votes.behavior.js @@ -7,11 +7,7 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; const { shouldBehaveLikeERC6372 } = require('./ERC6372.behavior'); -const { - getDomain, - domainType, - types: { Delegation }, -} = require('../../helpers/eip712'); +const { getDomain, domainType, Delegation } = require('../../helpers/eip712'); const { clockFromReceipt } = require('../../helpers/time'); const { expectRevertCustomError } = require('../../helpers/customError'); diff --git a/test/helpers/eip712-types.js b/test/helpers/eip712-types.js index 8aacf5325f6..b2b6ccf837b 100644 --- a/test/helpers/eip712-types.js +++ b/test/helpers/eip712-types.js @@ -1,44 +1,52 @@ -module.exports = { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' }, - { name: 'chainId', type: 'uint256' }, - { name: 'verifyingContract', type: 'address' }, - { name: 'salt', type: 'bytes32' }, - ], - Permit: [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, - ], - Ballot: [ - { name: 'proposalId', type: 'uint256' }, - { name: 'support', type: 'uint8' }, - { name: 'voter', type: 'address' }, - { name: 'nonce', type: 'uint256' }, - ], - ExtendedBallot: [ - { name: 'proposalId', type: 'uint256' }, - { name: 'support', type: 'uint8' }, - { name: 'voter', type: 'address' }, - { name: 'nonce', type: 'uint256' }, - { name: 'reason', type: 'string' }, - { name: 'params', type: 'bytes' }, - ], - Delegation: [ - { name: 'delegatee', type: 'address' }, - { name: 'nonce', type: 'uint256' }, - { name: 'expiry', type: 'uint256' }, - ], - ForwardRequest: [ - { name: 'from', type: 'address' }, - { name: 'to', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'gas', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint48' }, - { name: 'data', type: 'bytes' }, - ], -}; +const { mapValues } = require('./iterate'); + +const formatType = schema => Object.entries(schema).map(([name, type]) => ({ name, type })); + +module.exports = mapValues( + { + EIP712Domain: { + name: 'string', + version: 'string', + chainId: 'uint256', + verifyingContract: 'address', + salt: 'bytes32', + }, + Permit: { + owner: 'address', + spender: 'address', + value: 'uint256', + nonce: 'uint256', + deadline: 'uint256', + }, + Ballot: { + proposalId: 'uint256', + support: 'uint8', + voter: 'address', + nonce: 'uint256', + }, + ExtendedBallot: { + proposalId: 'uint256', + support: 'uint8', + voter: 'address', + nonce: 'uint256', + reason: 'string', + params: 'bytes', + }, + Delegation: { + delegatee: 'address', + nonce: 'uint256', + expiry: 'uint256', + }, + ForwardRequest: { + from: 'address', + to: 'address', + value: 'uint256', + gas: 'uint256', + nonce: 'uint256', + deadline: 'uint48', + data: 'bytes', + }, + }, + formatType, +); +module.exports.formatType = formatType; diff --git a/test/helpers/eip712.js b/test/helpers/eip712.js index 295c1b95315..278e86cce93 100644 --- a/test/helpers/eip712.js +++ b/test/helpers/eip712.js @@ -38,9 +38,9 @@ function hashTypedData(domain, structHash) { } module.exports = { - types, getDomain, domainType, domainSeparator: ethers.TypedDataEncoder.hashDomain, hashTypedData, + ...types, }; diff --git a/test/helpers/time.js b/test/helpers/time.js index 1f83a4aa223..874713ee535 100644 --- a/test/helpers/time.js +++ b/test/helpers/time.js @@ -1,6 +1,5 @@ const { time, mineUpTo } = require('@nomicfoundation/hardhat-network-helpers'); - -const mapObject = (obj, fn) => Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, fn(value)])); +const { mapValues } = require('./iterate'); module.exports = { clock: { @@ -22,8 +21,8 @@ module.exports = { // TODO: deprecate the old version in favor of this one module.exports.bigint = { - clock: mapObject(module.exports.clock, fn => () => fn().then(BigInt)), - clockFromReceipt: mapObject(module.exports.clockFromReceipt, fn => receipt => fn(receipt).then(BigInt)), + clock: mapValues(module.exports.clock, fn => () => fn().then(BigInt)), + clockFromReceipt: mapValues(module.exports.clockFromReceipt, fn => receipt => fn(receipt).then(BigInt)), forward: module.exports.forward, - duration: mapObject(module.exports.duration, fn => n => BigInt(fn(n))), + duration: mapValues(module.exports.duration, fn => n => BigInt(fn(n))), }; diff --git a/test/metatx/ERC2771Context.test.js b/test/metatx/ERC2771Context.test.js index 1ae48e6f273..07c4ff335a6 100644 --- a/test/metatx/ERC2771Context.test.js +++ b/test/metatx/ERC2771Context.test.js @@ -3,7 +3,7 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { impersonate } = require('../helpers/account'); -const { getDomain } = require('../helpers/eip712'); +const { getDomain, ForwardRequest } = require('../helpers/eip712'); const { MAX_UINT48 } = require('../helpers/constants'); const { shouldBehaveLikeRegularContext } = require('../utils/Context.behavior'); @@ -15,17 +15,7 @@ async function fixture() { const forwarderAsSigner = await impersonate(forwarder.target); const context = await ethers.deployContract('ERC2771ContextMock', [forwarder]); const domain = await getDomain(forwarder); - const types = { - ForwardRequest: [ - { name: 'from', type: 'address' }, - { name: 'to', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'gas', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint48' }, - { name: 'data', type: 'bytes' }, - ], - }; + const types = { ForwardRequest }; return { sender, other, forwarder, forwarderAsSigner, context, domain, types }; } diff --git a/test/metatx/ERC2771Forwarder.test.js b/test/metatx/ERC2771Forwarder.test.js index e0d1090c4bf..3bf2645303b 100644 --- a/test/metatx/ERC2771Forwarder.test.js +++ b/test/metatx/ERC2771Forwarder.test.js @@ -2,7 +2,7 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { getDomain } = require('../helpers/eip712'); +const { getDomain, ForwardRequest } = require('../helpers/eip712'); const { bigint: time } = require('../helpers/time'); const { sum } = require('../helpers/math'); @@ -12,17 +12,7 @@ async function fixture() { const forwarder = await ethers.deployContract('ERC2771Forwarder', ['ERC2771Forwarder']); const receiver = await ethers.deployContract('CallReceiverMockTrustingForwarder', [forwarder]); const domain = await getDomain(forwarder); - const types = { - ForwardRequest: [ - { name: 'from', type: 'address' }, - { name: 'to', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'gas', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint48' }, - { name: 'data', type: 'bytes' }, - ], - }; + const types = { ForwardRequest }; const forgeRequest = async (override = {}, signer = sender) => { const req = { diff --git a/test/token/ERC20/extensions/ERC20Burnable.behavior.js b/test/token/ERC20/extensions/ERC20Burnable.behavior.js deleted file mode 100644 index 937491bdfe8..00000000000 --- a/test/token/ERC20/extensions/ERC20Burnable.behavior.js +++ /dev/null @@ -1,116 +0,0 @@ -const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS } = constants; - -const { expect } = require('chai'); -const { expectRevertCustomError } = require('../../../helpers/customError'); - -function shouldBehaveLikeERC20Burnable(owner, initialBalance, [burner]) { - describe('burn', function () { - describe('when the given value is not greater than balance of the sender', function () { - context('for a zero value', function () { - shouldBurn(new BN(0)); - }); - - context('for a non-zero value', function () { - shouldBurn(new BN(100)); - }); - - function shouldBurn(value) { - beforeEach(async function () { - this.receipt = await this.token.burn(value, { from: owner }); - }); - - it('burns the requested value', async function () { - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal(initialBalance.sub(value)); - }); - - it('emits a transfer event', async function () { - expectEvent(this.receipt, 'Transfer', { - from: owner, - to: ZERO_ADDRESS, - value: value, - }); - }); - } - }); - - describe('when the given value is greater than the balance of the sender', function () { - const value = initialBalance.addn(1); - - it('reverts', async function () { - await expectRevertCustomError(this.token.burn(value, { from: owner }), 'ERC20InsufficientBalance', [ - owner, - initialBalance, - value, - ]); - }); - }); - }); - - describe('burnFrom', function () { - describe('on success', function () { - context('for a zero value', function () { - shouldBurnFrom(new BN(0)); - }); - - context('for a non-zero value', function () { - shouldBurnFrom(new BN(100)); - }); - - function shouldBurnFrom(value) { - const originalAllowance = value.muln(3); - - beforeEach(async function () { - await this.token.approve(burner, originalAllowance, { from: owner }); - this.receipt = await this.token.burnFrom(owner, value, { from: burner }); - }); - - it('burns the requested value', async function () { - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal(initialBalance.sub(value)); - }); - - it('decrements allowance', async function () { - expect(await this.token.allowance(owner, burner)).to.be.bignumber.equal(originalAllowance.sub(value)); - }); - - it('emits a transfer event', async function () { - expectEvent(this.receipt, 'Transfer', { - from: owner, - to: ZERO_ADDRESS, - value: value, - }); - }); - } - }); - - describe('when the given value is greater than the balance of the sender', function () { - const value = initialBalance.addn(1); - - it('reverts', async function () { - await this.token.approve(burner, value, { from: owner }); - await expectRevertCustomError(this.token.burnFrom(owner, value, { from: burner }), 'ERC20InsufficientBalance', [ - owner, - initialBalance, - value, - ]); - }); - }); - - describe('when the given value is greater than the allowance', function () { - const allowance = new BN(100); - - it('reverts', async function () { - await this.token.approve(burner, allowance, { from: owner }); - await expectRevertCustomError( - this.token.burnFrom(owner, allowance.addn(1), { from: burner }), - 'ERC20InsufficientAllowance', - [burner, allowance, allowance.addn(1)], - ); - }); - }); - }); -} - -module.exports = { - shouldBehaveLikeERC20Burnable, -}; diff --git a/test/token/ERC20/extensions/ERC20Burnable.test.js b/test/token/ERC20/extensions/ERC20Burnable.test.js index 00acc81edfc..8253acbf15f 100644 --- a/test/token/ERC20/extensions/ERC20Burnable.test.js +++ b/test/token/ERC20/extensions/ERC20Burnable.test.js @@ -1,20 +1,108 @@ -const { BN } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { shouldBehaveLikeERC20Burnable } = require('./ERC20Burnable.behavior'); -const ERC20Burnable = artifacts.require('$ERC20Burnable'); +const name = 'My Token'; +const symbol = 'MTKN'; +const initialBalance = 1000n; -contract('ERC20Burnable', function (accounts) { - const [owner, ...otherAccounts] = accounts; +async function fixture() { + const [owner, burner] = await ethers.getSigners(); - const initialBalance = new BN(1000); + const token = await ethers.deployContract('$ERC20Burnable', [name, symbol], owner); + await token.$_mint(owner, initialBalance); - const name = 'My Token'; - const symbol = 'MTKN'; + return { owner, burner, token, initialBalance }; +} +describe('ERC20Burnable', function () { beforeEach(async function () { - this.token = await ERC20Burnable.new(name, symbol, { from: owner }); - await this.token.$_mint(owner, initialBalance); + Object.assign(this, await loadFixture(fixture)); }); - shouldBehaveLikeERC20Burnable(owner, initialBalance, otherAccounts); + describe('burn', function () { + it('reverts if not enough balance', async function () { + const value = this.initialBalance + 1n; + + await expect(this.token.connect(this.owner).burn(value)) + .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') + .withArgs(this.owner.address, this.initialBalance, value); + }); + + describe('on success', function () { + for (const { title, value } of [ + { title: 'for a zero value', value: 0n }, + { title: 'for a non-zero value', value: 100n }, + ]) { + describe(title, function () { + beforeEach(async function () { + this.tx = await this.token.connect(this.owner).burn(value); + }); + + it('burns the requested value', async function () { + await expect(this.tx).to.changeTokenBalance(this.token, this.owner, -value); + }); + + it('emits a transfer event', async function () { + await expect(this.tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, value); + }); + }); + } + }); + }); + + describe('burnFrom', function () { + describe('reverts', function () { + it('if not enough balance', async function () { + const value = this.initialBalance + 1n; + + await this.token.connect(this.owner).approve(this.burner, value); + + await expect(this.token.connect(this.burner).burnFrom(this.owner, value)) + .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') + .withArgs(this.owner.address, this.initialBalance, value); + }); + + it('if not enough allowance', async function () { + const allowance = 100n; + + await this.token.connect(this.owner).approve(this.burner, allowance); + + await expect(this.token.connect(this.burner).burnFrom(this.owner, allowance + 1n)) + .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientAllowance') + .withArgs(this.burner.address, allowance, allowance + 1n); + }); + }); + + describe('on success', function () { + for (const { title, value } of [ + { title: 'for a zero value', value: 0n }, + { title: 'for a non-zero value', value: 100n }, + ]) { + describe(title, function () { + const originalAllowance = value * 3n; + + beforeEach(async function () { + await this.token.connect(this.owner).approve(this.burner, originalAllowance); + this.tx = await this.token.connect(this.burner).burnFrom(this.owner, value); + }); + + it('burns the requested value', async function () { + await expect(this.tx).to.changeTokenBalance(this.token, this.owner, -value); + }); + + it('decrements allowance', async function () { + expect(await this.token.allowance(this.owner, this.burner)).to.equal(originalAllowance - value); + }); + + it('emits a transfer event', async function () { + await expect(this.tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, value); + }); + }); + } + }); + }); }); diff --git a/test/token/ERC20/extensions/ERC20Capped.behavior.js b/test/token/ERC20/extensions/ERC20Capped.behavior.js deleted file mode 100644 index 5af5c3ddcd2..00000000000 --- a/test/token/ERC20/extensions/ERC20Capped.behavior.js +++ /dev/null @@ -1,31 +0,0 @@ -const { expect } = require('chai'); -const { expectRevertCustomError } = require('../../../helpers/customError'); - -function shouldBehaveLikeERC20Capped(accounts, cap) { - describe('capped token', function () { - const user = accounts[0]; - - it('starts with the correct cap', async function () { - expect(await this.token.cap()).to.be.bignumber.equal(cap); - }); - - it('mints when value is less than cap', async function () { - await this.token.$_mint(user, cap.subn(1)); - expect(await this.token.totalSupply()).to.be.bignumber.equal(cap.subn(1)); - }); - - it('fails to mint if the value exceeds the cap', async function () { - await this.token.$_mint(user, cap.subn(1)); - await expectRevertCustomError(this.token.$_mint(user, 2), 'ERC20ExceededCap', [cap.addn(1), cap]); - }); - - it('fails to mint after cap is reached', async function () { - await this.token.$_mint(user, cap); - await expectRevertCustomError(this.token.$_mint(user, 1), 'ERC20ExceededCap', [cap.addn(1), cap]); - }); - }); -} - -module.exports = { - shouldBehaveLikeERC20Capped, -}; diff --git a/test/token/ERC20/extensions/ERC20Capped.test.js b/test/token/ERC20/extensions/ERC20Capped.test.js index 1f4a2bee3bc..a32ec43a8ea 100644 --- a/test/token/ERC20/extensions/ERC20Capped.test.js +++ b/test/token/ERC20/extensions/ERC20Capped.test.js @@ -1,24 +1,55 @@ -const { ether } = require('@openzeppelin/test-helpers'); -const { shouldBehaveLikeERC20Capped } = require('./ERC20Capped.behavior'); -const { expectRevertCustomError } = require('../../../helpers/customError'); +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const ERC20Capped = artifacts.require('$ERC20Capped'); +const name = 'My Token'; +const symbol = 'MTKN'; +const cap = 1000n; -contract('ERC20Capped', function (accounts) { - const cap = ether('1000'); +async function fixture() { + const [user] = await ethers.getSigners(); - const name = 'My Token'; - const symbol = 'MTKN'; + const token = await ethers.deployContract('$ERC20Capped', [name, symbol, cap]); + + return { user, token, cap }; +} + +describe('ERC20Capped', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); it('requires a non-zero cap', async function () { - await expectRevertCustomError(ERC20Capped.new(name, symbol, 0), 'ERC20InvalidCap', [0]); + const ERC20Capped = await ethers.getContractFactory('$ERC20Capped'); + + await expect(ERC20Capped.deploy(name, symbol, 0)) + .to.be.revertedWithCustomError(ERC20Capped, 'ERC20InvalidCap') + .withArgs(0); }); - context('once deployed', async function () { - beforeEach(async function () { - this.token = await ERC20Capped.new(name, symbol, cap); + describe('capped token', function () { + it('starts with the correct cap', async function () { + expect(await this.token.cap()).to.equal(this.cap); }); - shouldBehaveLikeERC20Capped(accounts, cap); + it('mints when value is less than cap', async function () { + const value = this.cap - 1n; + await this.token.$_mint(this.user, value); + expect(await this.token.totalSupply()).to.equal(value); + }); + + it('fails to mint if the value exceeds the cap', async function () { + await this.token.$_mint(this.user, this.cap - 1n); + await expect(this.token.$_mint(this.user, 2)) + .to.be.revertedWithCustomError(this.token, 'ERC20ExceededCap') + .withArgs(this.cap + 1n, this.cap); + }); + + it('fails to mint after cap is reached', async function () { + await this.token.$_mint(this.user, this.cap); + await expect(this.token.$_mint(this.user, 1)) + .to.be.revertedWithCustomError(this.token, 'ERC20ExceededCap') + .withArgs(this.cap + 1n, this.cap); + }); }); }); diff --git a/test/token/ERC20/extensions/ERC20FlashMint.test.js b/test/token/ERC20/extensions/ERC20FlashMint.test.js index 13d5b3ef45d..cee00db0f44 100644 --- a/test/token/ERC20/extensions/ERC20FlashMint.test.js +++ b/test/token/ERC20/extensions/ERC20FlashMint.test.js @@ -1,209 +1,163 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { expectRevertCustomError } = require('../../../helpers/customError'); -const { MAX_UINT256, ZERO_ADDRESS } = constants; +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const ERC20FlashMintMock = artifacts.require('$ERC20FlashMintMock'); -const ERC3156FlashBorrowerMock = artifacts.require('ERC3156FlashBorrowerMock'); +const name = 'My Token'; +const symbol = 'MTKN'; +const initialSupply = 100n; +const loanValue = 10_000_000_000_000n; -contract('ERC20FlashMint', function (accounts) { - const [initialHolder, other, anotherAccount] = accounts; +async function fixture() { + const [initialHolder, other, anotherAccount] = await ethers.getSigners(); - const name = 'My Token'; - const symbol = 'MTKN'; + const token = await ethers.deployContract('$ERC20FlashMintMock', [name, symbol]); + await token.$_mint(initialHolder, initialSupply); - const initialSupply = new BN(100); - const loanValue = new BN(10000000000000); + return { initialHolder, other, anotherAccount, token }; +} +describe('ERC20FlashMint', function () { beforeEach(async function () { - this.token = await ERC20FlashMintMock.new(name, symbol); - await this.token.$_mint(initialHolder, initialSupply); + Object.assign(this, await loadFixture(fixture)); }); describe('maxFlashLoan', function () { it('token match', async function () { - expect(await this.token.maxFlashLoan(this.token.address)).to.be.bignumber.equal(MAX_UINT256.sub(initialSupply)); + expect(await this.token.maxFlashLoan(this.token)).to.equal(ethers.MaxUint256 - initialSupply); }); it('token mismatch', async function () { - expect(await this.token.maxFlashLoan(ZERO_ADDRESS)).to.be.bignumber.equal('0'); + expect(await this.token.maxFlashLoan(ethers.ZeroAddress)).to.equal(0n); }); }); describe('flashFee', function () { it('token match', async function () { - expect(await this.token.flashFee(this.token.address, loanValue)).to.be.bignumber.equal('0'); + expect(await this.token.flashFee(this.token, loanValue)).to.equal(0n); }); it('token mismatch', async function () { - await expectRevertCustomError(this.token.flashFee(ZERO_ADDRESS, loanValue), 'ERC3156UnsupportedToken', [ - ZERO_ADDRESS, - ]); + await expect(this.token.flashFee(ethers.ZeroAddress, loanValue)) + .to.be.revertedWithCustomError(this.token, 'ERC3156UnsupportedToken') + .withArgs(ethers.ZeroAddress); }); }); describe('flashFeeReceiver', function () { it('default receiver', async function () { - expect(await this.token.$_flashFeeReceiver()).to.be.eq(ZERO_ADDRESS); + expect(await this.token.$_flashFeeReceiver()).to.equal(ethers.ZeroAddress); }); }); describe('flashLoan', function () { it('success', async function () { - const receiver = await ERC3156FlashBorrowerMock.new(true, true); - const { tx } = await this.token.flashLoan(receiver.address, this.token.address, loanValue, '0x'); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: ZERO_ADDRESS, - to: receiver.address, - value: loanValue, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: receiver.address, - to: ZERO_ADDRESS, - value: loanValue, - }); - await expectEvent.inTransaction(tx, receiver, 'BalanceOf', { - token: this.token.address, - account: receiver.address, - value: loanValue, - }); - await expectEvent.inTransaction(tx, receiver, 'TotalSupply', { - token: this.token.address, - value: initialSupply.add(loanValue), - }); - - expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply); - expect(await this.token.balanceOf(receiver.address)).to.be.bignumber.equal('0'); - expect(await this.token.allowance(receiver.address, this.token.address)).to.be.bignumber.equal('0'); + const receiver = await ethers.deployContract('ERC3156FlashBorrowerMock', [true, true]); + + const tx = await this.token.flashLoan(receiver, this.token, loanValue, '0x'); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, receiver.target, loanValue) + .to.emit(this.token, 'Transfer') + .withArgs(receiver.target, ethers.ZeroAddress, loanValue) + .to.emit(receiver, 'BalanceOf') + .withArgs(this.token.target, receiver.target, loanValue) + .to.emit(receiver, 'TotalSupply') + .withArgs(this.token.target, initialSupply + loanValue); + await expect(tx).to.changeTokenBalance(this.token, receiver, 0); + + expect(await this.token.totalSupply()).to.equal(initialSupply); + expect(await this.token.allowance(receiver, this.token)).to.equal(0n); }); it('missing return value', async function () { - const receiver = await ERC3156FlashBorrowerMock.new(false, true); - await expectRevertCustomError( - this.token.flashLoan(receiver.address, this.token.address, loanValue, '0x'), - 'ERC3156InvalidReceiver', - [receiver.address], - ); + const receiver = await ethers.deployContract('ERC3156FlashBorrowerMock', [false, true]); + await expect(this.token.flashLoan(receiver, this.token, loanValue, '0x')) + .to.be.revertedWithCustomError(this.token, 'ERC3156InvalidReceiver') + .withArgs(receiver.target); }); it('missing approval', async function () { - const receiver = await ERC3156FlashBorrowerMock.new(true, false); - await expectRevertCustomError( - this.token.flashLoan(receiver.address, this.token.address, loanValue, '0x'), - 'ERC20InsufficientAllowance', - [this.token.address, 0, loanValue], - ); + const receiver = await ethers.deployContract('ERC3156FlashBorrowerMock', [true, false]); + await expect(this.token.flashLoan(receiver, this.token, loanValue, '0x')) + .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientAllowance') + .withArgs(this.token.target, 0, loanValue); }); it('unavailable funds', async function () { - const receiver = await ERC3156FlashBorrowerMock.new(true, true); - const data = this.token.contract.methods.transfer(other, 10).encodeABI(); - await expectRevertCustomError( - this.token.flashLoan(receiver.address, this.token.address, loanValue, data), - 'ERC20InsufficientBalance', - [receiver.address, loanValue - 10, loanValue], - ); + const receiver = await ethers.deployContract('ERC3156FlashBorrowerMock', [true, true]); + const data = this.token.interface.encodeFunctionData('transfer', [this.other.address, 10]); + await expect(this.token.flashLoan(receiver, this.token, loanValue, data)) + .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') + .withArgs(receiver.target, loanValue - 10n, loanValue); }); it('more than maxFlashLoan', async function () { - const receiver = await ERC3156FlashBorrowerMock.new(true, true); - const data = this.token.contract.methods.transfer(other, 10).encodeABI(); - // _mint overflow reverts using a panic code. No reason string. - await expectRevert.unspecified(this.token.flashLoan(receiver.address, this.token.address, MAX_UINT256, data)); + const receiver = await ethers.deployContract('ERC3156FlashBorrowerMock', [true, true]); + const data = this.token.interface.encodeFunctionData('transfer', [this.other.address, 10]); + await expect(this.token.flashLoan(receiver, this.token, ethers.MaxUint256, data)) + .to.be.revertedWithCustomError(this.token, 'ERC3156ExceededMaxLoan') + .withArgs(ethers.MaxUint256 - initialSupply); }); describe('custom flash fee & custom fee receiver', function () { - const receiverInitialBalance = new BN(200000); - const flashFee = new BN(5000); + const receiverInitialBalance = 200_000n; + const flashFee = 5_000n; beforeEach('init receiver balance & set flash fee', async function () { - this.receiver = await ERC3156FlashBorrowerMock.new(true, true); - const receipt = await this.token.$_mint(this.receiver.address, receiverInitialBalance); - await expectEvent(receipt, 'Transfer', { - from: ZERO_ADDRESS, - to: this.receiver.address, - value: receiverInitialBalance, - }); - expect(await this.token.balanceOf(this.receiver.address)).to.be.bignumber.equal(receiverInitialBalance); + this.receiver = await ethers.deployContract('ERC3156FlashBorrowerMock', [true, true]); + + const tx = await this.token.$_mint(this.receiver, receiverInitialBalance); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.receiver.target, receiverInitialBalance); + await expect(tx).to.changeTokenBalance(this.token, this.receiver, receiverInitialBalance); await this.token.setFlashFee(flashFee); - expect(await this.token.flashFee(this.token.address, loanValue)).to.be.bignumber.equal(flashFee); + expect(await this.token.flashFee(this.token, loanValue)).to.equal(flashFee); }); it('default flash fee receiver', async function () { - const { tx } = await this.token.flashLoan(this.receiver.address, this.token.address, loanValue, '0x'); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: ZERO_ADDRESS, - to: this.receiver.address, - value: loanValue, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.receiver.address, - to: ZERO_ADDRESS, - value: loanValue.add(flashFee), - }); - await expectEvent.inTransaction(tx, this.receiver, 'BalanceOf', { - token: this.token.address, - account: this.receiver.address, - value: receiverInitialBalance.add(loanValue), - }); - await expectEvent.inTransaction(tx, this.receiver, 'TotalSupply', { - token: this.token.address, - value: initialSupply.add(receiverInitialBalance).add(loanValue), - }); - - expect(await this.token.totalSupply()).to.be.bignumber.equal( - initialSupply.add(receiverInitialBalance).sub(flashFee), - ); - expect(await this.token.balanceOf(this.receiver.address)).to.be.bignumber.equal( - receiverInitialBalance.sub(flashFee), - ); - expect(await this.token.balanceOf(await this.token.$_flashFeeReceiver())).to.be.bignumber.equal('0'); - expect(await this.token.allowance(this.receiver.address, this.token.address)).to.be.bignumber.equal('0'); + const tx = await this.token.flashLoan(this.receiver, this.token, loanValue, '0x'); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.receiver.target, loanValue) + .to.emit(this.token, 'Transfer') + .withArgs(this.receiver.target, ethers.ZeroAddress, loanValue + flashFee) + .to.emit(this.receiver, 'BalanceOf') + .withArgs(this.token.target, this.receiver.target, receiverInitialBalance + loanValue) + .to.emit(this.receiver, 'TotalSupply') + .withArgs(this.token.target, initialSupply + receiverInitialBalance + loanValue); + await expect(tx).to.changeTokenBalances(this.token, [this.receiver, ethers.ZeroAddress], [-flashFee, 0]); + + expect(await this.token.totalSupply()).to.equal(initialSupply + receiverInitialBalance - flashFee); + expect(await this.token.allowance(this.receiver, this.token)).to.equal(0n); }); it('custom flash fee receiver', async function () { - const flashFeeReceiverAddress = anotherAccount; + const flashFeeReceiverAddress = this.anotherAccount; await this.token.setFlashFeeReceiver(flashFeeReceiverAddress); - expect(await this.token.$_flashFeeReceiver()).to.be.eq(flashFeeReceiverAddress); - - expect(await this.token.balanceOf(flashFeeReceiverAddress)).to.be.bignumber.equal('0'); - - const { tx } = await this.token.flashLoan(this.receiver.address, this.token.address, loanValue, '0x'); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: ZERO_ADDRESS, - to: this.receiver.address, - value: loanValue, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.receiver.address, - to: ZERO_ADDRESS, - value: loanValue, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.receiver.address, - to: flashFeeReceiverAddress, - value: flashFee, - }); - await expectEvent.inTransaction(tx, this.receiver, 'BalanceOf', { - token: this.token.address, - account: this.receiver.address, - value: receiverInitialBalance.add(loanValue), - }); - await expectEvent.inTransaction(tx, this.receiver, 'TotalSupply', { - token: this.token.address, - value: initialSupply.add(receiverInitialBalance).add(loanValue), - }); - - expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply.add(receiverInitialBalance)); - expect(await this.token.balanceOf(this.receiver.address)).to.be.bignumber.equal( - receiverInitialBalance.sub(flashFee), + expect(await this.token.$_flashFeeReceiver()).to.equal(flashFeeReceiverAddress.address); + + const tx = await this.token.flashLoan(this.receiver, this.token, loanValue, '0x'); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.receiver.target, loanValue) + .to.emit(this.token, 'Transfer') + .withArgs(this.receiver.target, ethers.ZeroAddress, loanValue) + .to.emit(this.token, 'Transfer') + .withArgs(this.receiver.target, flashFeeReceiverAddress.address, flashFee) + .to.emit(this.receiver, 'BalanceOf') + .withArgs(this.token.target, this.receiver.target, receiverInitialBalance + loanValue) + .to.emit(this.receiver, 'TotalSupply') + .withArgs(this.token.target, initialSupply + receiverInitialBalance + loanValue); + await expect(tx).to.changeTokenBalances( + this.token, + [this.receiver, flashFeeReceiverAddress], + [-flashFee, flashFee], ); - expect(await this.token.balanceOf(flashFeeReceiverAddress)).to.be.bignumber.equal(flashFee); - expect(await this.token.allowance(this.receiver.address, flashFeeReceiverAddress)).to.be.bignumber.equal('0'); + + expect(await this.token.totalSupply()).to.equal(initialSupply + receiverInitialBalance); + expect(await this.token.allowance(this.receiver, flashFeeReceiverAddress)).to.equal(0n); }); }); }); diff --git a/test/token/ERC20/extensions/ERC20Pausable.test.js b/test/token/ERC20/extensions/ERC20Pausable.test.js index 92c90b9b8df..1f1157c19e9 100644 --- a/test/token/ERC20/extensions/ERC20Pausable.test.js +++ b/test/token/ERC20/extensions/ERC20Pausable.test.js @@ -1,135 +1,128 @@ -const { BN } = require('@openzeppelin/test-helpers'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { expectRevertCustomError } = require('../../../helpers/customError'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const ERC20Pausable = artifacts.require('$ERC20Pausable'); +const name = 'My Token'; +const symbol = 'MTKN'; +const initialSupply = 100n; -contract('ERC20Pausable', function (accounts) { - const [holder, recipient, anotherAccount] = accounts; +async function fixture() { + const [holder, recipient, approved] = await ethers.getSigners(); - const initialSupply = new BN(100); + const token = await ethers.deployContract('$ERC20Pausable', [name, symbol]); + await token.$_mint(holder, initialSupply); - const name = 'My Token'; - const symbol = 'MTKN'; + return { holder, recipient, approved, token }; +} +describe('ERC20Pausable', function () { beforeEach(async function () { - this.token = await ERC20Pausable.new(name, symbol); - await this.token.$_mint(holder, initialSupply); + Object.assign(this, await loadFixture(fixture)); }); describe('pausable token', function () { describe('transfer', function () { it('allows to transfer when unpaused', async function () { - await this.token.transfer(recipient, initialSupply, { from: holder }); - - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('0'); - expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(initialSupply); + await expect(this.token.connect(this.holder).transfer(this.recipient, initialSupply)).to.changeTokenBalances( + this.token, + [this.holder, this.recipient], + [-initialSupply, initialSupply], + ); }); it('allows to transfer when paused and then unpaused', async function () { await this.token.$_pause(); await this.token.$_unpause(); - await this.token.transfer(recipient, initialSupply, { from: holder }); - - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('0'); - expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(initialSupply); + await expect(this.token.connect(this.holder).transfer(this.recipient, initialSupply)).to.changeTokenBalances( + this.token, + [this.holder, this.recipient], + [-initialSupply, initialSupply], + ); }); it('reverts when trying to transfer when paused', async function () { await this.token.$_pause(); - await expectRevertCustomError( - this.token.transfer(recipient, initialSupply, { from: holder }), - 'EnforcedPause', - [], - ); + await expect( + this.token.connect(this.holder).transfer(this.recipient, initialSupply), + ).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); }); describe('transfer from', function () { - const allowance = new BN(40); + const allowance = 40n; beforeEach(async function () { - await this.token.approve(anotherAccount, allowance, { from: holder }); + await this.token.connect(this.holder).approve(this.approved, allowance); }); it('allows to transfer from when unpaused', async function () { - await this.token.transferFrom(holder, recipient, allowance, { from: anotherAccount }); - - expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(allowance); - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply.sub(allowance)); + await expect( + this.token.connect(this.approved).transferFrom(this.holder, this.recipient, allowance), + ).to.changeTokenBalances(this.token, [this.holder, this.recipient], [-allowance, allowance]); }); it('allows to transfer when paused and then unpaused', async function () { await this.token.$_pause(); await this.token.$_unpause(); - await this.token.transferFrom(holder, recipient, allowance, { from: anotherAccount }); - - expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(allowance); - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply.sub(allowance)); + await expect( + this.token.connect(this.approved).transferFrom(this.holder, this.recipient, allowance), + ).to.changeTokenBalances(this.token, [this.holder, this.recipient], [-allowance, allowance]); }); it('reverts when trying to transfer from when paused', async function () { await this.token.$_pause(); - await expectRevertCustomError( - this.token.transferFrom(holder, recipient, allowance, { from: anotherAccount }), - 'EnforcedPause', - [], - ); + await expect( + this.token.connect(this.approved).transferFrom(this.holder, this.recipient, allowance), + ).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); }); describe('mint', function () { - const value = new BN('42'); + const value = 42n; it('allows to mint when unpaused', async function () { - await this.token.$_mint(recipient, value); - - expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(value); + await expect(this.token.$_mint(this.recipient, value)).to.changeTokenBalance(this.token, this.recipient, value); }); it('allows to mint when paused and then unpaused', async function () { await this.token.$_pause(); await this.token.$_unpause(); - await this.token.$_mint(recipient, value); - - expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(value); + await expect(this.token.$_mint(this.recipient, value)).to.changeTokenBalance(this.token, this.recipient, value); }); it('reverts when trying to mint when paused', async function () { await this.token.$_pause(); - await expectRevertCustomError(this.token.$_mint(recipient, value), 'EnforcedPause', []); + await expect(this.token.$_mint(this.recipient, value)).to.be.revertedWithCustomError( + this.token, + 'EnforcedPause', + ); }); }); describe('burn', function () { - const value = new BN('42'); + const value = 42n; it('allows to burn when unpaused', async function () { - await this.token.$_burn(holder, value); - - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply.sub(value)); + await expect(this.token.$_burn(this.holder, value)).to.changeTokenBalance(this.token, this.holder, -value); }); it('allows to burn when paused and then unpaused', async function () { await this.token.$_pause(); await this.token.$_unpause(); - await this.token.$_burn(holder, value); - - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply.sub(value)); + await expect(this.token.$_burn(this.holder, value)).to.changeTokenBalance(this.token, this.holder, -value); }); it('reverts when trying to burn when paused', async function () { await this.token.$_pause(); - await expectRevertCustomError(this.token.$_burn(holder, value), 'EnforcedPause', []); + await expect(this.token.$_burn(this.holder, value)).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); }); }); diff --git a/test/token/ERC20/extensions/ERC20Permit.test.js b/test/token/ERC20/extensions/ERC20Permit.test.js index db2363cd26c..e27a98239bb 100644 --- a/test/token/ERC20/extensions/ERC20Permit.test.js +++ b/test/token/ERC20/extensions/ERC20Permit.test.js @@ -1,41 +1,38 @@ -/* eslint-disable */ - -const { BN, constants, time } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { MAX_UINT256 } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const ERC20Permit = artifacts.require('$ERC20Permit'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { getDomain, domainSeparator, Permit } = require('../../../helpers/eip712'); const { - types: { Permit }, - getDomain, - domainType, - domainSeparator, -} = require('../../../helpers/eip712'); -const { getChainId } = require('../../../helpers/chainid'); -const { expectRevertCustomError } = require('../../../helpers/customError'); + bigint: { clock, duration }, +} = require('../../../helpers/time'); -contract('ERC20Permit', function (accounts) { - const [initialHolder, spender] = accounts; +const name = 'My Token'; +const symbol = 'MTKN'; +const initialSupply = 100n; - const name = 'My Token'; - const symbol = 'MTKN'; +async function fixture() { + const [initialHolder, spender, owner, other] = await ethers.getSigners(); - const initialSupply = new BN(100); + const token = await ethers.deployContract('$ERC20Permit', [name, symbol, name]); + await token.$_mint(initialHolder, initialSupply); - beforeEach(async function () { - this.chainId = await getChainId(); + return { + initialHolder, + spender, + owner, + other, + token, + }; +} - this.token = await ERC20Permit.new(name, symbol, name); - await this.token.$_mint(initialHolder, initialSupply); +describe('ERC20Permit', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); }); it('initial nonce is 0', async function () { - expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); + expect(await this.token.nonces(this.initialHolder)).to.equal(0n); }); it('domain separator', async function () { @@ -43,81 +40,72 @@ contract('ERC20Permit', function (accounts) { }); describe('permit', function () { - const wallet = Wallet.generate(); - - const owner = wallet.getAddressString(); - const value = new BN(42); - const nonce = 0; - const maxDeadline = MAX_UINT256; - - const buildData = (contract, deadline = maxDeadline) => - getDomain(contract).then(domain => ({ - primaryType: 'Permit', - types: { EIP712Domain: domainType(domain), Permit }, - domain, - message: { owner, spender, value, nonce, deadline }, - })); + const value = 42n; + const nonce = 0n; + const maxDeadline = ethers.MaxUint256; + + beforeEach(function () { + this.buildData = (contract, deadline = maxDeadline) => + getDomain(contract).then(domain => ({ + domain, + types: { Permit }, + message: { + owner: this.owner.address, + spender: this.spender.address, + value, + nonce, + deadline, + }, + })); + }); it('accepts owner signature', async function () { - const { v, r, s } = await buildData(this.token) - .then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data })) - .then(fromRpcSig); + const { v, r, s } = await this.buildData(this.token) + .then(({ domain, types, message }) => this.owner.signTypedData(domain, types, message)) + .then(ethers.Signature.from); - await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + await this.token.permit(this.owner, this.spender, value, maxDeadline, v, r, s); - expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); - expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value); + expect(await this.token.nonces(this.owner)).to.equal(1n); + expect(await this.token.allowance(this.owner, this.spender)).to.equal(value); }); it('rejects reused signature', async function () { - const sig = await buildData(this.token).then(data => - ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }), - ); - const { r, s, v } = fromRpcSig(sig); - - await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - const domain = await getDomain(this.token); - const typedMessage = { - primaryType: 'Permit', - types: { EIP712Domain: domainType(domain), Permit }, - domain, - message: { owner, spender, value, nonce: nonce + 1, deadline: maxDeadline }, - }; - - await expectRevertCustomError( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC2612InvalidSigner', - [ethSigUtil.recoverTypedSignature({ data: typedMessage, sig }), owner], + const { v, r, s, serialized } = await this.buildData(this.token) + .then(({ domain, types, message }) => this.owner.signTypedData(domain, types, message)) + .then(ethers.Signature.from); + + await this.token.permit(this.owner, this.spender, value, maxDeadline, v, r, s); + + const recovered = await this.buildData(this.token).then(({ domain, types, message }) => + ethers.verifyTypedData(domain, types, { ...message, nonce: nonce + 1n, deadline: maxDeadline }, serialized), ); + + await expect(this.token.permit(this.owner, this.spender, value, maxDeadline, v, r, s)) + .to.be.revertedWithCustomError(this.token, 'ERC2612InvalidSigner') + .withArgs(recovered, this.owner.address); }); it('rejects other signature', async function () { - const otherWallet = Wallet.generate(); + const { v, r, s } = await this.buildData(this.token) + .then(({ domain, types, message }) => this.other.signTypedData(domain, types, message)) + .then(ethers.Signature.from); - const { v, r, s } = await buildData(this.token) - .then(data => ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data })) - .then(fromRpcSig); - - await expectRevertCustomError( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC2612InvalidSigner', - [await otherWallet.getAddressString(), owner], - ); + await expect(this.token.permit(this.owner, this.spender, value, maxDeadline, v, r, s)) + .to.be.revertedWithCustomError(this.token, 'ERC2612InvalidSigner') + .withArgs(this.other.address, this.owner.address); }); it('rejects expired permit', async function () { - const deadline = (await time.latest()) - time.duration.weeks(1); + const deadline = (await clock.timestamp()) - duration.weeks(1); - const { v, r, s } = await buildData(this.token, deadline) - .then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data })) - .then(fromRpcSig); + const { v, r, s } = await this.buildData(this.token, deadline) + .then(({ domain, types, message }) => this.owner.signTypedData(domain, types, message)) + .then(ethers.Signature.from); - await expectRevertCustomError( - this.token.permit(owner, spender, value, deadline, v, r, s), - 'ERC2612ExpiredSignature', - [deadline], - ); + await expect(this.token.permit(this.owner, this.spender, value, deadline, v, r, s)) + .to.be.revertedWithCustomError(this.token, 'ERC2612ExpiredSignature') + .withArgs(deadline); }); }); }); diff --git a/test/token/ERC20/extensions/ERC20Votes.test.js b/test/token/ERC20/extensions/ERC20Votes.test.js index a0da162a425..9ec1c09e935 100644 --- a/test/token/ERC20/extensions/ERC20Votes.test.js +++ b/test/token/ERC20/extensions/ERC20Votes.test.js @@ -10,11 +10,7 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; const { batchInBlock } = require('../../../helpers/txpool'); -const { - getDomain, - domainType, - types: { Delegation }, -} = require('../../../helpers/eip712'); +const { getDomain, domainType, Delegation } = require('../../../helpers/eip712'); const { clock, clockFromReceipt } = require('../../../helpers/time'); const { expectRevertCustomError } = require('../../../helpers/customError'); diff --git a/test/token/ERC20/extensions/ERC20Wrapper.test.js b/test/token/ERC20/extensions/ERC20Wrapper.test.js index b61573edd88..af746d65a54 100644 --- a/test/token/ERC20/extensions/ERC20Wrapper.test.js +++ b/test/token/ERC20/extensions/ERC20Wrapper.test.js @@ -6,12 +6,13 @@ const { shouldBehaveLikeERC20 } = require('../ERC20.behavior'); const name = 'My Token'; const symbol = 'MTKN'; +const decimals = 9n; const initialSupply = 100n; async function fixture() { const [initialHolder, recipient, anotherAccount] = await ethers.getSigners(); - const underlying = await ethers.deployContract('$ERC20DecimalsMock', [name, symbol, 9]); + const underlying = await ethers.deployContract('$ERC20DecimalsMock', [name, symbol, decimals]); await underlying.$_mint(initialHolder, initialSupply); const token = await ethers.deployContract('$ERC20Wrapper', [`Wrapped ${name}`, `W${symbol}`, underlying]); @@ -20,9 +21,6 @@ async function fixture() { } describe('ERC20Wrapper', function () { - const name = 'My Token'; - const symbol = 'MTKN'; - beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); @@ -40,7 +38,7 @@ describe('ERC20Wrapper', function () { }); it('has the same decimals as the underlying token', async function () { - expect(await this.token.decimals()).to.be.equal(9n); + expect(await this.token.decimals()).to.be.equal(decimals); }); it('decimals default back to 18 if token has no metadata', async function () { @@ -56,13 +54,13 @@ describe('ERC20Wrapper', function () { describe('deposit', function () { it('executes with approval', async function () { await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply); + const tx = await this.token.connect(this.initialHolder).depositFor(this.initialHolder, initialSupply); await expect(tx) .to.emit(this.underlying, 'Transfer') .withArgs(this.initialHolder.address, this.token.target, initialSupply) .to.emit(this.token, 'Transfer') .withArgs(ethers.ZeroAddress, this.initialHolder.address, initialSupply); - await expect(tx).to.changeTokenBalances( this.underlying, [this.initialHolder, this.token], @@ -79,6 +77,7 @@ describe('ERC20Wrapper', function () { it('reverts when inssuficient balance', async function () { await this.underlying.connect(this.initialHolder).approve(this.token, ethers.MaxUint256); + await expect(this.token.connect(this.initialHolder).depositFor(this.initialHolder, ethers.MaxUint256)) .to.be.revertedWithCustomError(this.underlying, 'ERC20InsufficientBalance') .withArgs(this.initialHolder.address, initialSupply, ethers.MaxUint256); @@ -86,13 +85,13 @@ describe('ERC20Wrapper', function () { it('deposits to other account', async function () { await this.underlying.connect(this.initialHolder).approve(this.token, initialSupply); + const tx = await this.token.connect(this.initialHolder).depositFor(this.recipient, initialSupply); await expect(tx) .to.emit(this.underlying, 'Transfer') .withArgs(this.initialHolder.address, this.token.target, initialSupply) .to.emit(this.token, 'Transfer') .withArgs(ethers.ZeroAddress, this.recipient.address, initialSupply); - await expect(tx).to.changeTokenBalances( this.underlying, [this.initialHolder, this.token], @@ -103,6 +102,7 @@ describe('ERC20Wrapper', function () { it('reverts minting to the wrapper contract', async function () { await this.underlying.connect(this.initialHolder).approve(this.token, ethers.MaxUint256); + await expect(this.token.connect(this.initialHolder).depositFor(this.token, ethers.MaxUint256)) .to.be.revertedWithCustomError(this.token, 'ERC20InvalidReceiver') .withArgs(this.token.target); @@ -130,7 +130,6 @@ describe('ERC20Wrapper', function () { .withArgs(this.token.target, this.initialHolder.address, value) .to.emit(this.token, 'Transfer') .withArgs(this.initialHolder.address, ethers.ZeroAddress, value); - await expect(tx).to.changeTokenBalances(this.underlying, [this.token, this.initialHolder], [-value, value]); await expect(tx).to.changeTokenBalance(this.token, this.initialHolder, -value); }); @@ -142,7 +141,6 @@ describe('ERC20Wrapper', function () { .withArgs(this.token.target, this.initialHolder.address, initialSupply) .to.emit(this.token, 'Transfer') .withArgs(this.initialHolder.address, ethers.ZeroAddress, initialSupply); - await expect(tx).to.changeTokenBalances( this.underlying, [this.token, this.initialHolder], @@ -158,7 +156,6 @@ describe('ERC20Wrapper', function () { .withArgs(this.token.target, this.recipient.address, initialSupply) .to.emit(this.token, 'Transfer') .withArgs(this.initialHolder.address, ethers.ZeroAddress, initialSupply); - await expect(tx).to.changeTokenBalances( this.underlying, [this.token, this.initialHolder, this.recipient], @@ -181,7 +178,6 @@ describe('ERC20Wrapper', function () { const tx = await this.token.$_recover(this.recipient); await expect(tx).to.emit(this.token, 'Transfer').withArgs(ethers.ZeroAddress, this.recipient.address, 0n); - await expect(tx).to.changeTokenBalance(this.token, this.recipient, 0); }); @@ -192,7 +188,6 @@ describe('ERC20Wrapper', function () { await expect(tx) .to.emit(this.token, 'Transfer') .withArgs(ethers.ZeroAddress, this.recipient.address, initialSupply); - await expect(tx).to.changeTokenBalance(this.token, this.recipient, initialSupply); }); }); diff --git a/test/token/ERC20/extensions/ERC4626.test.js b/test/token/ERC20/extensions/ERC4626.test.js index fa66785f070..907855efeb7 100644 --- a/test/token/ERC20/extensions/ERC4626.test.js +++ b/test/token/ERC20/extensions/ERC4626.test.js @@ -1,72 +1,71 @@ -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); -const { Enum } = require('../../../helpers/enums'); -const { expectRevertCustomError } = require('../../../helpers/customError'); +const { + bigint: { Enum }, +} = require('../../../helpers/enums'); -const ERC20Decimals = artifacts.require('$ERC20DecimalsMock'); -const ERC4626 = artifacts.require('$ERC4626'); -const ERC4626LimitsMock = artifacts.require('$ERC4626LimitsMock'); -const ERC4626OffsetMock = artifacts.require('$ERC4626OffsetMock'); -const ERC4626FeesMock = artifacts.require('$ERC4626FeesMock'); -const ERC20ExcessDecimalsMock = artifacts.require('ERC20ExcessDecimalsMock'); -const ERC20Reentrant = artifacts.require('$ERC20Reentrant'); +const name = 'My Token'; +const symbol = 'MTKN'; +const decimals = 18n; -contract('ERC4626', function (accounts) { - const [holder, recipient, spender, other, user1, user2] = accounts; +async function fixture() { + const [holder, recipient, spender, other, ...accounts] = await ethers.getSigners(); + return { holder, recipient, spender, other, accounts }; +} - const name = 'My Token'; - const symbol = 'MTKN'; - const decimals = web3.utils.toBN(18); +describe('ERC4626', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); it('inherit decimals if from asset', async function () { - for (const decimals of [0, 9, 12, 18, 36].map(web3.utils.toBN)) { - const token = await ERC20Decimals.new('', '', decimals); - const vault = await ERC4626.new('', '', token.address); - expect(await vault.decimals()).to.be.bignumber.equal(decimals); + for (const decimals of [0n, 9n, 12n, 18n, 36n]) { + const token = await ethers.deployContract('$ERC20DecimalsMock', ['', '', decimals]); + const vault = await ethers.deployContract('$ERC4626', ['', '', token]); + expect(await vault.decimals()).to.equal(decimals); } }); it('asset has not yet been created', async function () { - const vault = await ERC4626.new('', '', other); - expect(await vault.decimals()).to.be.bignumber.equal(decimals); + const vault = await ethers.deployContract('$ERC4626', ['', '', this.other.address]); + expect(await vault.decimals()).to.equal(decimals); }); it('underlying excess decimals', async function () { - const token = await ERC20ExcessDecimalsMock.new(); - const vault = await ERC4626.new('', '', token.address); - expect(await vault.decimals()).to.be.bignumber.equal(decimals); + const token = await ethers.deployContract('$ERC20ExcessDecimalsMock'); + const vault = await ethers.deployContract('$ERC4626', ['', '', token]); + expect(await vault.decimals()).to.equal(decimals); }); it('decimals overflow', async function () { - for (const offset of [243, 250, 255].map(web3.utils.toBN)) { - const token = await ERC20Decimals.new('', '', decimals); - const vault = await ERC4626OffsetMock.new(name + ' Vault', symbol + 'V', token.address, offset); - await expectRevert( - vault.decimals(), - 'reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)', - ); + for (const offset of [243n, 250n, 255n]) { + const token = await ethers.deployContract('$ERC20DecimalsMock', ['', '', decimals]); + const vault = await ethers.deployContract('$ERC4626OffsetMock', ['', '', token, offset]); + await expect(vault.decimals()).to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW); } }); describe('reentrancy', async function () { const reenterType = Enum('No', 'Before', 'After'); - const value = web3.utils.toBN(1000000000000000000); - const reenterValue = web3.utils.toBN(1000000000); - let token; - let vault; + const value = 1_000_000_000_000_000_000n; + const reenterValue = 1_000_000_000n; beforeEach(async function () { - token = await ERC20Reentrant.new(); // Use offset 1 so the rate is not 1:1 and we can't possibly confuse assets and shares - vault = await ERC4626OffsetMock.new('', '', token.address, 1); + const token = await ethers.deployContract('$ERC20Reentrant'); + const vault = await ethers.deployContract('$ERC4626OffsetMock', ['', '', token, 1n]); // Funds and approval for tests - await token.$_mint(holder, value); - await token.$_mint(other, value); - await token.$_approve(holder, vault.address, constants.MAX_UINT256); - await token.$_approve(other, vault.address, constants.MAX_UINT256); - await token.$_approve(token.address, vault.address, constants.MAX_UINT256); + await token.$_mint(this.holder, value); + await token.$_mint(this.other, value); + await token.$_approve(this.holder, vault, ethers.MaxUint256); + await token.$_approve(this.other, vault, ethers.MaxUint256); + await token.$_approve(token, vault, ethers.MaxUint256); + + Object.assign(this, { token, vault }); }); // During a `_deposit`, the vault does `transferFrom(depositor, vault, assets)` -> `_mint(receiver, shares)` @@ -75,40 +74,29 @@ contract('ERC4626', function (accounts) { // intermediate state in which the ratio of assets/shares has been decreased (more shares than assets). it('correct share price is observed during reentrancy before deposit', async function () { // mint token for deposit - await token.$_mint(token.address, reenterValue); + await this.token.$_mint(this.token, reenterValue); // Schedules a reentrancy from the token contract - await token.scheduleReenter( + await this.token.scheduleReenter( reenterType.Before, - vault.address, - vault.contract.methods.deposit(reenterValue, holder).encodeABI(), + this.vault, + this.vault.interface.encodeFunctionData('deposit', [reenterValue, this.holder.address]), ); // Initial share price - const sharesForDeposit = await vault.previewDeposit(value, { from: holder }); - const sharesForReenter = await vault.previewDeposit(reenterValue, { from: holder }); - - // Deposit normally, reentering before the internal `_update` - const receipt = await vault.deposit(value, holder, { from: holder }); - - // Main deposit event - await expectEvent(receipt, 'Deposit', { - sender: holder, - owner: holder, - assets: value, - shares: sharesForDeposit, - }); - // Reentrant deposit event → uses the same price - await expectEvent(receipt, 'Deposit', { - sender: token.address, - owner: holder, - assets: reenterValue, - shares: sharesForReenter, - }); + const sharesForDeposit = await this.vault.previewDeposit(value); + const sharesForReenter = await this.vault.previewDeposit(reenterValue); + + await expect(this.vault.connect(this.holder).deposit(value, this.holder)) + // Deposit normally, reentering before the internal `_update` + .to.emit(this.vault, 'Deposit') + .withArgs(this.holder.address, this.holder.address, value, sharesForDeposit) + // Reentrant deposit event → uses the same price + .to.emit(this.vault, 'Deposit') + .withArgs(this.token.target, this.holder.address, reenterValue, sharesForReenter); // Assert prices is kept - const sharesAfter = await vault.previewDeposit(value, { from: holder }); - expect(sharesForDeposit).to.be.bignumber.eq(sharesAfter); + expect(await this.vault.previewDeposit(value)).to.equal(sharesForDeposit); }); // During a `_withdraw`, the vault does `_burn(owner, shares)` -> `transfer(receiver, assets)` @@ -117,43 +105,31 @@ contract('ERC4626', function (accounts) { // intermediate state in which the ratio of shares/assets has been decreased (more assets than shares). it('correct share price is observed during reentrancy after withdraw', async function () { // Deposit into the vault: holder gets `value` share, token.address gets `reenterValue` shares - await vault.deposit(value, holder, { from: holder }); - await vault.deposit(reenterValue, token.address, { from: other }); + await this.vault.connect(this.holder).deposit(value, this.holder); + await this.vault.connect(this.other).deposit(reenterValue, this.token); // Schedules a reentrancy from the token contract - await token.scheduleReenter( + await this.token.scheduleReenter( reenterType.After, - vault.address, - vault.contract.methods.withdraw(reenterValue, holder, token.address).encodeABI(), + this.vault, + this.vault.interface.encodeFunctionData('withdraw', [reenterValue, this.holder.address, this.token.target]), ); // Initial share price - const sharesForWithdraw = await vault.previewWithdraw(value, { from: holder }); - const sharesForReenter = await vault.previewWithdraw(reenterValue, { from: holder }); + const sharesForWithdraw = await this.vault.previewWithdraw(value); + const sharesForReenter = await this.vault.previewWithdraw(reenterValue); // Do withdraw normally, triggering the _afterTokenTransfer hook - const receipt = await vault.withdraw(value, holder, holder, { from: holder }); - - // Main withdraw event - await expectEvent(receipt, 'Withdraw', { - sender: holder, - receiver: holder, - owner: holder, - assets: value, - shares: sharesForWithdraw, - }); - // Reentrant withdraw event → uses the same price - await expectEvent(receipt, 'Withdraw', { - sender: token.address, - receiver: holder, - owner: token.address, - assets: reenterValue, - shares: sharesForReenter, - }); + await expect(this.vault.connect(this.holder).withdraw(value, this.holder, this.holder)) + // Main withdraw event + .to.emit(this.vault, 'Withdraw') + .withArgs(this.holder.address, this.holder.address, this.holder.address, value, sharesForWithdraw) + // Reentrant withdraw event → uses the same price + .to.emit(this.vault, 'Withdraw') + .withArgs(this.token.target, this.holder.address, this.token.target, reenterValue, sharesForReenter); // Assert price is kept - const sharesAfter = await vault.previewWithdraw(value, { from: holder }); - expect(sharesForWithdraw).to.be.bignumber.eq(sharesAfter); + expect(await this.vault.previewWithdraw(value)).to.equal(sharesForWithdraw); }); // Donate newly minted tokens to the vault during the reentracy causes the share price to increase. @@ -161,254 +137,210 @@ contract('ERC4626', function (accounts) { // Further deposits will get a different price (getting fewer shares for the same value of assets) it('share price change during reentracy does not affect deposit', async function () { // Schedules a reentrancy from the token contract that mess up the share price - await token.scheduleReenter( + await this.token.scheduleReenter( reenterType.Before, - token.address, - token.contract.methods.$_mint(vault.address, reenterValue).encodeABI(), + this.token, + this.token.interface.encodeFunctionData('$_mint', [this.vault.target, reenterValue]), ); // Price before - const sharesBefore = await vault.previewDeposit(value); + const sharesBefore = await this.vault.previewDeposit(value); // Deposit, reentering before the internal `_update` - const receipt = await vault.deposit(value, holder, { from: holder }); - - // Price is as previewed - await expectEvent(receipt, 'Deposit', { - sender: holder, - owner: holder, - assets: value, - shares: sharesBefore, - }); + await expect(this.vault.connect(this.holder).deposit(value, this.holder)) + // Price is as previewed + .to.emit(this.vault, 'Deposit') + .withArgs(this.holder.address, this.holder.address, value, sharesBefore); // Price was modified during reentrancy - const sharesAfter = await vault.previewDeposit(value); - expect(sharesAfter).to.be.bignumber.lt(sharesBefore); + expect(await this.vault.previewDeposit(value)).to.lt(sharesBefore); }); // Burn some tokens from the vault during the reentracy causes the share price to drop. // Still, the withdraw that trigger the reentracy is not affected and get the previewed price. // Further withdraw will get a different price (needing more shares for the same value of assets) it('share price change during reentracy does not affect withdraw', async function () { - await vault.deposit(value, other, { from: other }); - await vault.deposit(value, holder, { from: holder }); + await this.vault.connect(this.holder).deposit(value, this.holder); + await this.vault.connect(this.other).deposit(value, this.other); // Schedules a reentrancy from the token contract that mess up the share price - await token.scheduleReenter( + await this.token.scheduleReenter( reenterType.After, - token.address, - token.contract.methods.$_burn(vault.address, reenterValue).encodeABI(), + this.token, + this.token.interface.encodeFunctionData('$_burn', [this.vault.target, reenterValue]), ); // Price before - const sharesBefore = await vault.previewWithdraw(value); + const sharesBefore = await this.vault.previewWithdraw(value); // Withdraw, triggering the _afterTokenTransfer hook - const receipt = await vault.withdraw(value, holder, holder, { from: holder }); - - // Price is as previewed - await expectEvent(receipt, 'Withdraw', { - sender: holder, - receiver: holder, - owner: holder, - assets: value, - shares: sharesBefore, - }); + await expect(this.vault.connect(this.holder).withdraw(value, this.holder, this.holder)) + // Price is as previewed + .to.emit(this.vault, 'Withdraw') + .withArgs(this.holder.address, this.holder.address, this.holder.address, value, sharesBefore); // Price was modified during reentrancy - const sharesAfter = await vault.previewWithdraw(value); - expect(sharesAfter).to.be.bignumber.gt(sharesBefore); + expect(await this.vault.previewWithdraw(value)).to.gt(sharesBefore); }); }); describe('limits', async function () { beforeEach(async function () { - this.token = await ERC20Decimals.new(name, symbol, decimals); - this.vault = await ERC4626LimitsMock.new(name + ' Vault', symbol + 'V', this.token.address); + const token = await ethers.deployContract('$ERC20DecimalsMock', [name, symbol, decimals]); + const vault = await ethers.deployContract('$ERC4626LimitsMock', ['', '', token]); + + Object.assign(this, { token, vault }); }); it('reverts on deposit() above max deposit', async function () { - const maxDeposit = await this.vault.maxDeposit(holder); - await expectRevertCustomError(this.vault.deposit(maxDeposit.addn(1), recipient), 'ERC4626ExceededMaxDeposit', [ - recipient, - maxDeposit.addn(1), - maxDeposit, - ]); + const maxDeposit = await this.vault.maxDeposit(this.holder); + await expect(this.vault.connect(this.holder).deposit(maxDeposit + 1n, this.recipient)) + .to.be.revertedWithCustomError(this.vault, 'ERC4626ExceededMaxDeposit') + .withArgs(this.recipient.address, maxDeposit + 1n, maxDeposit); }); it('reverts on mint() above max mint', async function () { - const maxMint = await this.vault.maxMint(holder); - await expectRevertCustomError(this.vault.mint(maxMint.addn(1), recipient), 'ERC4626ExceededMaxMint', [ - recipient, - maxMint.addn(1), - maxMint, - ]); + const maxMint = await this.vault.maxMint(this.holder); + + await expect(this.vault.connect(this.holder).mint(maxMint + 1n, this.recipient)) + .to.be.revertedWithCustomError(this.vault, 'ERC4626ExceededMaxMint') + .withArgs(this.recipient.address, maxMint + 1n, maxMint); }); it('reverts on withdraw() above max withdraw', async function () { - const maxWithdraw = await this.vault.maxWithdraw(holder); - await expectRevertCustomError( - this.vault.withdraw(maxWithdraw.addn(1), recipient, holder), - 'ERC4626ExceededMaxWithdraw', - [holder, maxWithdraw.addn(1), maxWithdraw], - ); + const maxWithdraw = await this.vault.maxWithdraw(this.holder); + + await expect(this.vault.connect(this.holder).withdraw(maxWithdraw + 1n, this.recipient, this.holder)) + .to.be.revertedWithCustomError(this.vault, 'ERC4626ExceededMaxWithdraw') + .withArgs(this.holder.address, maxWithdraw + 1n, maxWithdraw); }); it('reverts on redeem() above max redeem', async function () { - const maxRedeem = await this.vault.maxRedeem(holder); - await expectRevertCustomError( - this.vault.redeem(maxRedeem.addn(1), recipient, holder), - 'ERC4626ExceededMaxRedeem', - [holder, maxRedeem.addn(1), maxRedeem], - ); + const maxRedeem = await this.vault.maxRedeem(this.holder); + + await expect(this.vault.connect(this.holder).redeem(maxRedeem + 1n, this.recipient, this.holder)) + .to.be.revertedWithCustomError(this.vault, 'ERC4626ExceededMaxRedeem') + .withArgs(this.holder.address, maxRedeem + 1n, maxRedeem); }); }); - for (const offset of [0, 6, 18].map(web3.utils.toBN)) { - const parseToken = token => web3.utils.toBN(10).pow(decimals).muln(token); - const parseShare = share => web3.utils.toBN(10).pow(decimals.add(offset)).muln(share); + for (const offset of [0n, 6n, 18n]) { + const parseToken = token => token * 10n ** decimals; + const parseShare = share => share * 10n ** (decimals + offset); - const virtualAssets = web3.utils.toBN(1); - const virtualShares = web3.utils.toBN(10).pow(offset); + const virtualAssets = 1n; + const virtualShares = 10n ** offset; describe(`offset: ${offset}`, function () { beforeEach(async function () { - this.token = await ERC20Decimals.new(name, symbol, decimals); - this.vault = await ERC4626OffsetMock.new(name + ' Vault', symbol + 'V', this.token.address, offset); + const token = await ethers.deployContract('$ERC20DecimalsMock', [name, symbol, decimals]); + const vault = await ethers.deployContract('$ERC4626OffsetMock', [name + ' Vault', symbol + 'V', token, offset]); - await this.token.$_mint(holder, constants.MAX_INT256); // 50% of maximum - await this.token.approve(this.vault.address, constants.MAX_UINT256, { from: holder }); - await this.vault.approve(spender, constants.MAX_UINT256, { from: holder }); + await token.$_mint(this.holder, ethers.MaxUint256 / 2n); // 50% of maximum + await token.$_approve(this.holder, vault, ethers.MaxUint256); + await vault.$_approve(this.holder, this.spender, ethers.MaxUint256); + + Object.assign(this, { token, vault }); }); it('metadata', async function () { - expect(await this.vault.name()).to.be.equal(name + ' Vault'); - expect(await this.vault.symbol()).to.be.equal(symbol + 'V'); - expect(await this.vault.decimals()).to.be.bignumber.equal(decimals.add(offset)); - expect(await this.vault.asset()).to.be.equal(this.token.address); + expect(await this.vault.name()).to.equal(name + ' Vault'); + expect(await this.vault.symbol()).to.equal(symbol + 'V'); + expect(await this.vault.decimals()).to.equal(decimals + offset); + expect(await this.vault.asset()).to.equal(this.token.target); }); describe('empty vault: no assets & no shares', function () { it('status', async function () { - expect(await this.vault.totalAssets()).to.be.bignumber.equal('0'); + expect(await this.vault.totalAssets()).to.equal(0n); }); it('deposit', async function () { - expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256); - expect(await this.vault.previewDeposit(parseToken(1))).to.be.bignumber.equal(parseShare(1)); - - const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: parseToken(1), - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: parseShare(1), - }); - - await expectEvent.inTransaction(tx, this.vault, 'Deposit', { - sender: holder, - owner: recipient, - assets: parseToken(1), - shares: parseShare(1), - }); + expect(await this.vault.maxDeposit(this.holder)).to.equal(ethers.MaxUint256); + expect(await this.vault.previewDeposit(parseToken(1n))).to.equal(parseShare(1n)); + + const tx = this.vault.connect(this.holder).deposit(parseToken(1n), this.recipient); + + await expect(tx).to.changeTokenBalances( + this.token, + [this.holder, this.vault], + [-parseToken(1n), parseToken(1n)], + ); + await expect(tx).to.changeTokenBalance(this.vault, this.recipient, parseShare(1n)); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.vault.target, parseToken(1n)) + .to.emit(this.vault, 'Transfer') + .withArgs(ethers.ZeroAddress, this.recipient.address, parseShare(1n)) + .to.emit(this.vault, 'Deposit') + .withArgs(this.holder.address, this.recipient.address, parseToken(1n), parseShare(1n)); }); it('mint', async function () { - expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256); - expect(await this.vault.previewMint(parseShare(1))).to.be.bignumber.equal(parseToken(1)); - - const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: parseToken(1), - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: parseShare(1), - }); - - await expectEvent.inTransaction(tx, this.vault, 'Deposit', { - sender: holder, - owner: recipient, - assets: parseToken(1), - shares: parseShare(1), - }); + expect(await this.vault.maxMint(this.holder)).to.equal(ethers.MaxUint256); + expect(await this.vault.previewMint(parseShare(1n))).to.equal(parseToken(1n)); + + const tx = this.vault.connect(this.holder).mint(parseShare(1n), this.recipient); + + await expect(tx).to.changeTokenBalances( + this.token, + [this.holder, this.vault], + [-parseToken(1n), parseToken(1n)], + ); + await expect(tx).to.changeTokenBalance(this.vault, this.recipient, parseShare(1n)); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.vault.target, parseToken(1n)) + .to.emit(this.vault, 'Transfer') + .withArgs(ethers.ZeroAddress, this.recipient.address, parseShare(1n)) + .to.emit(this.vault, 'Deposit') + .withArgs(this.holder.address, this.recipient.address, parseToken(1n), parseShare(1n)); }); it('withdraw', async function () { - expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0'); - expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0'); - - const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: '0', - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: '0', - }); - - await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { - sender: holder, - receiver: recipient, - owner: holder, - assets: '0', - shares: '0', - }); + expect(await this.vault.maxWithdraw(this.holder)).to.equal(0n); + expect(await this.vault.previewWithdraw(0n)).to.equal(0n); + + const tx = this.vault.connect(this.holder).withdraw(0n, this.recipient, this.holder); + + await expect(tx).to.changeTokenBalances(this.token, [this.vault, this.recipient], [0n, 0n]); + await expect(tx).to.changeTokenBalance(this.vault, this.holder, 0n); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.vault.target, this.recipient.address, 0n) + .to.emit(this.vault, 'Transfer') + .withArgs(this.holder.address, ethers.ZeroAddress, 0n) + .to.emit(this.vault, 'Withdraw') + .withArgs(this.holder.address, this.recipient.address, this.holder.address, 0n, 0n); }); it('redeem', async function () { - expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal('0'); - expect(await this.vault.previewRedeem('0')).to.be.bignumber.equal('0'); - - const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: '0', - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: '0', - }); - - await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { - sender: holder, - receiver: recipient, - owner: holder, - assets: '0', - shares: '0', - }); + expect(await this.vault.maxRedeem(this.holder)).to.equal(0n); + expect(await this.vault.previewRedeem(0n)).to.equal(0n); + + const tx = this.vault.connect(this.holder).redeem(0n, this.recipient, this.holder); + + await expect(tx).to.changeTokenBalances(this.token, [this.vault, this.recipient], [0n, 0n]); + await expect(tx).to.changeTokenBalance(this.vault, this.holder, 0n); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.vault.target, this.recipient.address, 0n) + .to.emit(this.vault, 'Transfer') + .withArgs(this.holder.address, ethers.ZeroAddress, 0n) + .to.emit(this.vault, 'Withdraw') + .withArgs(this.holder.address, this.recipient.address, this.holder.address, 0n, 0n); }); }); describe('inflation attack: offset price by direct deposit of assets', function () { beforeEach(async function () { // Donate 1 token to the vault to offset the price - await this.token.$_mint(this.vault.address, parseToken(1)); + await this.token.$_mint(this.vault, parseToken(1n)); }); it('status', async function () { - expect(await this.vault.totalSupply()).to.be.bignumber.equal('0'); - expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1)); + expect(await this.vault.totalSupply()).to.equal(0n); + expect(await this.vault.totalAssets()).to.equal(parseToken(1n)); }); /** @@ -423,35 +355,30 @@ contract('ERC4626', function (accounts) { * was trying to deposit */ it('deposit', async function () { - const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); - const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); - - const depositAssets = parseToken(1); - const expectedShares = depositAssets.mul(effectiveShares).div(effectiveAssets); - - expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256); - expect(await this.vault.previewDeposit(depositAssets)).to.be.bignumber.equal(expectedShares); - - const { tx } = await this.vault.deposit(depositAssets, recipient, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: depositAssets, - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: expectedShares, - }); - - await expectEvent.inTransaction(tx, this.vault, 'Deposit', { - sender: holder, - owner: recipient, - assets: depositAssets, - shares: expectedShares, - }); + const effectiveAssets = (await this.vault.totalAssets()) + virtualAssets; + const effectiveShares = (await this.vault.totalSupply()) + virtualShares; + + const depositAssets = parseToken(1n); + const expectedShares = (depositAssets * effectiveShares) / effectiveAssets; + + expect(await this.vault.maxDeposit(this.holder)).to.equal(ethers.MaxUint256); + expect(await this.vault.previewDeposit(depositAssets)).to.equal(expectedShares); + + const tx = this.vault.connect(this.holder).deposit(depositAssets, this.recipient); + + await expect(tx).to.changeTokenBalances( + this.token, + [this.holder, this.vault], + [-depositAssets, depositAssets], + ); + await expect(tx).to.changeTokenBalance(this.vault, this.recipient, expectedShares); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.vault.target, depositAssets) + .to.emit(this.vault, 'Transfer') + .withArgs(ethers.ZeroAddress, this.recipient.address, expectedShares) + .to.emit(this.vault, 'Deposit') + .withArgs(this.holder.address, this.recipient.address, depositAssets, expectedShares); }); /** @@ -466,102 +393,77 @@ contract('ERC4626', function (accounts) { * large deposits. */ it('mint', async function () { - const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); - const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); - - const mintShares = parseShare(1); - const expectedAssets = mintShares.mul(effectiveAssets).div(effectiveShares); - - expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256); - expect(await this.vault.previewMint(mintShares)).to.be.bignumber.equal(expectedAssets); - - const { tx } = await this.vault.mint(mintShares, recipient, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: expectedAssets, - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: mintShares, - }); - - await expectEvent.inTransaction(tx, this.vault, 'Deposit', { - sender: holder, - owner: recipient, - assets: expectedAssets, - shares: mintShares, - }); + const effectiveAssets = (await this.vault.totalAssets()) + virtualAssets; + const effectiveShares = (await this.vault.totalSupply()) + virtualShares; + + const mintShares = parseShare(1n); + const expectedAssets = (mintShares * effectiveAssets) / effectiveShares; + + expect(await this.vault.maxMint(this.holder)).to.equal(ethers.MaxUint256); + expect(await this.vault.previewMint(mintShares)).to.equal(expectedAssets); + + const tx = this.vault.connect(this.holder).mint(mintShares, this.recipient); + + await expect(tx).to.changeTokenBalances( + this.token, + [this.holder, this.vault], + [-expectedAssets, expectedAssets], + ); + await expect(tx).to.changeTokenBalance(this.vault, this.recipient, mintShares); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.vault.target, expectedAssets) + .to.emit(this.vault, 'Transfer') + .withArgs(ethers.ZeroAddress, this.recipient.address, mintShares) + .to.emit(this.vault, 'Deposit') + .withArgs(this.holder.address, this.recipient.address, expectedAssets, mintShares); }); it('withdraw', async function () { - expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0'); - expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0'); - - const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: '0', - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: '0', - }); - - await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { - sender: holder, - receiver: recipient, - owner: holder, - assets: '0', - shares: '0', - }); + expect(await this.vault.maxWithdraw(this.holder)).to.equal(0n); + expect(await this.vault.previewWithdraw(0n)).to.equal(0n); + + const tx = this.vault.connect(this.holder).withdraw(0n, this.recipient, this.holder); + + await expect(tx).to.changeTokenBalances(this.token, [this.vault, this.recipient], [0n, 0n]); + await expect(tx).to.changeTokenBalance(this.vault, this.holder, 0n); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.vault.target, this.recipient.address, 0n) + .to.emit(this.vault, 'Transfer') + .withArgs(this.holder.address, ethers.ZeroAddress, 0n) + .to.emit(this.vault, 'Withdraw') + .withArgs(this.holder.address, this.recipient.address, this.holder.address, 0n, 0n); }); it('redeem', async function () { - expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal('0'); - expect(await this.vault.previewRedeem('0')).to.be.bignumber.equal('0'); - - const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: '0', - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: '0', - }); - - await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { - sender: holder, - receiver: recipient, - owner: holder, - assets: '0', - shares: '0', - }); + expect(await this.vault.maxRedeem(this.holder)).to.equal(0n); + expect(await this.vault.previewRedeem(0n)).to.equal(0n); + + const tx = this.vault.connect(this.holder).redeem(0n, this.recipient, this.holder); + + await expect(tx).to.changeTokenBalances(this.token, [this.vault, this.recipient], [0n, 0n]); + await expect(tx).to.changeTokenBalance(this.vault, this.holder, 0n); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.vault.target, this.recipient.address, 0n) + .to.emit(this.vault, 'Transfer') + .withArgs(this.holder.address, ethers.ZeroAddress, 0n) + .to.emit(this.vault, 'Withdraw') + .withArgs(this.holder.address, this.recipient.address, this.holder.address, 0n, 0n); }); }); describe('full vault: assets & shares', function () { beforeEach(async function () { // Add 1 token of underlying asset and 100 shares to the vault - await this.token.$_mint(this.vault.address, parseToken(1)); - await this.vault.$_mint(holder, parseShare(100)); + await this.token.$_mint(this.vault, parseToken(1n)); + await this.vault.$_mint(this.holder, parseShare(100n)); }); it('status', async function () { - expect(await this.vault.totalSupply()).to.be.bignumber.equal(parseShare(100)); - expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1)); + expect(await this.vault.totalSupply()).to.equal(parseShare(100n)); + expect(await this.vault.totalAssets()).to.equal(parseToken(1n)); }); /** @@ -574,35 +476,30 @@ contract('ERC4626', function (accounts) { * Virtual shares & assets captures part of the value */ it('deposit', async function () { - const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); - const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); - - const depositAssets = parseToken(1); - const expectedShares = depositAssets.mul(effectiveShares).div(effectiveAssets); - - expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256); - expect(await this.vault.previewDeposit(depositAssets)).to.be.bignumber.equal(expectedShares); - - const { tx } = await this.vault.deposit(depositAssets, recipient, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: depositAssets, - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: expectedShares, - }); - - await expectEvent.inTransaction(tx, this.vault, 'Deposit', { - sender: holder, - owner: recipient, - assets: depositAssets, - shares: expectedShares, - }); + const effectiveAssets = (await this.vault.totalAssets()) + virtualAssets; + const effectiveShares = (await this.vault.totalSupply()) + virtualShares; + + const depositAssets = parseToken(1n); + const expectedShares = (depositAssets * effectiveShares) / effectiveAssets; + + expect(await this.vault.maxDeposit(this.holder)).to.equal(ethers.MaxUint256); + expect(await this.vault.previewDeposit(depositAssets)).to.equal(expectedShares); + + const tx = this.vault.connect(this.holder).deposit(depositAssets, this.recipient); + + await expect(tx).to.changeTokenBalances( + this.token, + [this.holder, this.vault], + [-depositAssets, depositAssets], + ); + await expect(tx).to.changeTokenBalance(this.vault, this.recipient, expectedShares); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.vault.target, depositAssets) + .to.emit(this.vault, 'Transfer') + .withArgs(ethers.ZeroAddress, this.recipient.address, expectedShares) + .to.emit(this.vault, 'Deposit') + .withArgs(this.holder.address, this.recipient.address, depositAssets, expectedShares); }); /** @@ -615,249 +512,216 @@ contract('ERC4626', function (accounts) { * Virtual shares & assets captures part of the value */ it('mint', async function () { - const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); - const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); - - const mintShares = parseShare(1); - const expectedAssets = mintShares.mul(effectiveAssets).div(effectiveShares).addn(1); // add for the rounding - - expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256); - expect(await this.vault.previewMint(mintShares)).to.be.bignumber.equal(expectedAssets); - - const { tx } = await this.vault.mint(mintShares, recipient, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: expectedAssets, - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: mintShares, - }); - - await expectEvent.inTransaction(tx, this.vault, 'Deposit', { - sender: holder, - owner: recipient, - assets: expectedAssets, - shares: mintShares, - }); + const effectiveAssets = (await this.vault.totalAssets()) + virtualAssets; + const effectiveShares = (await this.vault.totalSupply()) + virtualShares; + + const mintShares = parseShare(1n); + const expectedAssets = (mintShares * effectiveAssets) / effectiveShares + 1n; // add for the rounding + + expect(await this.vault.maxMint(this.holder)).to.equal(ethers.MaxUint256); + expect(await this.vault.previewMint(mintShares)).to.equal(expectedAssets); + + const tx = this.vault.connect(this.holder).mint(mintShares, this.recipient); + + await expect(tx).to.changeTokenBalances( + this.token, + [this.holder, this.vault], + [-expectedAssets, expectedAssets], + ); + await expect(tx).to.changeTokenBalance(this.vault, this.recipient, mintShares); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.vault.target, expectedAssets) + .to.emit(this.vault, 'Transfer') + .withArgs(ethers.ZeroAddress, this.recipient.address, mintShares) + .to.emit(this.vault, 'Deposit') + .withArgs(this.holder.address, this.recipient.address, expectedAssets, mintShares); }); it('withdraw', async function () { - const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); - const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); - - const withdrawAssets = parseToken(1); - const expectedShares = withdrawAssets.mul(effectiveShares).div(effectiveAssets).addn(1); // add for the rounding - - expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal(withdrawAssets); - expect(await this.vault.previewWithdraw(withdrawAssets)).to.be.bignumber.equal(expectedShares); - - const { tx } = await this.vault.withdraw(withdrawAssets, recipient, holder, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: withdrawAssets, - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: expectedShares, - }); - - await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { - sender: holder, - receiver: recipient, - owner: holder, - assets: withdrawAssets, - shares: expectedShares, - }); + const effectiveAssets = (await this.vault.totalAssets()) + virtualAssets; + const effectiveShares = (await this.vault.totalSupply()) + virtualShares; + + const withdrawAssets = parseToken(1n); + const expectedShares = (withdrawAssets * effectiveShares) / effectiveAssets + 1n; // add for the rounding + + expect(await this.vault.maxWithdraw(this.holder)).to.equal(withdrawAssets); + expect(await this.vault.previewWithdraw(withdrawAssets)).to.equal(expectedShares); + + const tx = this.vault.connect(this.holder).withdraw(withdrawAssets, this.recipient, this.holder); + + await expect(tx).to.changeTokenBalances( + this.token, + [this.vault, this.recipient], + [-withdrawAssets, withdrawAssets], + ); + await expect(tx).to.changeTokenBalance(this.vault, this.holder, -expectedShares); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.vault.target, this.recipient.address, withdrawAssets) + .to.emit(this.vault, 'Transfer') + .withArgs(this.holder.address, ethers.ZeroAddress, expectedShares) + .to.emit(this.vault, 'Withdraw') + .withArgs(this.holder.address, this.recipient.address, this.holder.address, withdrawAssets, expectedShares); }); it('withdraw with approval', async function () { - const assets = await this.vault.previewWithdraw(parseToken(1)); - await expectRevertCustomError( - this.vault.withdraw(parseToken(1), recipient, holder, { from: other }), - 'ERC20InsufficientAllowance', - [other, 0, assets], - ); + const assets = await this.vault.previewWithdraw(parseToken(1n)); - await this.vault.withdraw(parseToken(1), recipient, holder, { from: spender }); + await expect(this.vault.connect(this.other).withdraw(parseToken(1n), this.recipient, this.holder)) + .to.be.revertedWithCustomError(this.vault, 'ERC20InsufficientAllowance') + .withArgs(this.other.address, 0n, assets); + + await expect(this.vault.connect(this.spender).withdraw(parseToken(1n), this.recipient, this.holder)).to.not.be + .reverted; }); it('redeem', async function () { - const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); - const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); - - const redeemShares = parseShare(100); - const expectedAssets = redeemShares.mul(effectiveAssets).div(effectiveShares); - - expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal(redeemShares); - expect(await this.vault.previewRedeem(redeemShares)).to.be.bignumber.equal(expectedAssets); - - const { tx } = await this.vault.redeem(redeemShares, recipient, holder, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: expectedAssets, - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: redeemShares, - }); - - await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { - sender: holder, - receiver: recipient, - owner: holder, - assets: expectedAssets, - shares: redeemShares, - }); + const effectiveAssets = (await this.vault.totalAssets()) + virtualAssets; + const effectiveShares = (await this.vault.totalSupply()) + virtualShares; + + const redeemShares = parseShare(100n); + const expectedAssets = (redeemShares * effectiveAssets) / effectiveShares; + + expect(await this.vault.maxRedeem(this.holder)).to.equal(redeemShares); + expect(await this.vault.previewRedeem(redeemShares)).to.equal(expectedAssets); + + const tx = this.vault.connect(this.holder).redeem(redeemShares, this.recipient, this.holder); + + await expect(tx).to.changeTokenBalances( + this.token, + [this.vault, this.recipient], + [-expectedAssets, expectedAssets], + ); + await expect(tx).to.changeTokenBalance(this.vault, this.holder, -redeemShares); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.vault.target, this.recipient.address, expectedAssets) + .to.emit(this.vault, 'Transfer') + .withArgs(this.holder.address, ethers.ZeroAddress, redeemShares) + .to.emit(this.vault, 'Withdraw') + .withArgs(this.holder.address, this.recipient.address, this.holder.address, expectedAssets, redeemShares); }); it('redeem with approval', async function () { - await expectRevertCustomError( - this.vault.redeem(parseShare(100), recipient, holder, { from: other }), - 'ERC20InsufficientAllowance', - [other, 0, parseShare(100)], - ); + await expect(this.vault.connect(this.other).redeem(parseShare(100n), this.recipient, this.holder)) + .to.be.revertedWithCustomError(this.vault, 'ERC20InsufficientAllowance') + .withArgs(this.other.address, 0n, parseShare(100n)); - await this.vault.redeem(parseShare(100), recipient, holder, { from: spender }); + await expect(this.vault.connect(this.spender).redeem(parseShare(100n), this.recipient, this.holder)).to.not.be + .reverted; }); }); }); } describe('ERC4626Fees', function () { - const feeBasisPoints = web3.utils.toBN(5e3); - const valueWithoutFees = web3.utils.toBN(10000); - const fees = valueWithoutFees.mul(feeBasisPoints).divn(1e4); - const valueWithFees = valueWithoutFees.add(fees); + const feeBasisPoints = 500n; // 5% + const valueWithoutFees = 10_000n; + const fees = (valueWithoutFees * feeBasisPoints) / 10_000n; + const valueWithFees = valueWithoutFees + fees; describe('input fees', function () { beforeEach(async function () { - this.token = await ERC20Decimals.new(name, symbol, 18); - this.vault = await ERC4626FeesMock.new( - name + ' Vault', - symbol + 'V', - this.token.address, + const token = await ethers.deployContract('$ERC20DecimalsMock', [name, symbol, 18n]); + const vault = await ethers.deployContract('$ERC4626FeesMock', [ + '', + '', + token, feeBasisPoints, - other, - 0, - constants.ZERO_ADDRESS, - ); + this.other, + 0n, + ethers.ZeroAddress, + ]); - await this.token.$_mint(holder, constants.MAX_INT256); - await this.token.approve(this.vault.address, constants.MAX_INT256, { from: holder }); + await token.$_mint(this.holder, ethers.MaxUint256 / 2n); + await token.$_approve(this.holder, vault, ethers.MaxUint256 / 2n); + + Object.assign(this, { token, vault }); }); it('deposit', async function () { - expect(await this.vault.previewDeposit(valueWithFees)).to.be.bignumber.equal(valueWithoutFees); - ({ tx: this.tx } = await this.vault.deposit(valueWithFees, recipient, { from: holder })); + expect(await this.vault.previewDeposit(valueWithFees)).to.equal(valueWithoutFees); + this.tx = this.vault.connect(this.holder).deposit(valueWithFees, this.recipient); }); it('mint', async function () { - expect(await this.vault.previewMint(valueWithoutFees)).to.be.bignumber.equal(valueWithFees); - ({ tx: this.tx } = await this.vault.mint(valueWithoutFees, recipient, { from: holder })); + expect(await this.vault.previewMint(valueWithoutFees)).to.equal(valueWithFees); + this.tx = this.vault.connect(this.holder).mint(valueWithoutFees, this.recipient); }); afterEach(async function () { - // get total - await expectEvent.inTransaction(this.tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: valueWithFees, - }); - - // redirect fees - await expectEvent.inTransaction(this.tx, this.token, 'Transfer', { - from: this.vault.address, - to: other, - value: fees, - }); - - // mint shares - await expectEvent.inTransaction(this.tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: valueWithoutFees, - }); - - // deposit event - await expectEvent.inTransaction(this.tx, this.vault, 'Deposit', { - sender: holder, - owner: recipient, - assets: valueWithFees, - shares: valueWithoutFees, - }); + await expect(this.tx).to.changeTokenBalances( + this.token, + [this.holder, this.vault, this.other], + [-valueWithFees, valueWithoutFees, fees], + ); + await expect(this.tx).to.changeTokenBalance(this.vault, this.recipient, valueWithoutFees); + await expect(this.tx) + // get total + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.vault.target, valueWithFees) + // redirect fees + .to.emit(this.token, 'Transfer') + .withArgs(this.vault.target, this.other.address, fees) + // mint shares + .to.emit(this.vault, 'Transfer') + .withArgs(ethers.ZeroAddress, this.recipient.address, valueWithoutFees) + // deposit event + .to.emit(this.vault, 'Deposit') + .withArgs(this.holder.address, this.recipient.address, valueWithFees, valueWithoutFees); }); }); describe('output fees', function () { beforeEach(async function () { - this.token = await ERC20Decimals.new(name, symbol, 18); - this.vault = await ERC4626FeesMock.new( - name + ' Vault', - symbol + 'V', - this.token.address, - 0, - constants.ZERO_ADDRESS, - 5e3, // 5% - other, - ); + const token = await ethers.deployContract('$ERC20DecimalsMock', [name, symbol, 18n]); + const vault = await ethers.deployContract('$ERC4626FeesMock', [ + '', + '', + token, + 0n, + ethers.ZeroAddress, + feeBasisPoints, + this.other, + ]); - await this.token.$_mint(this.vault.address, constants.MAX_INT256); - await this.vault.$_mint(holder, constants.MAX_INT256); + await token.$_mint(vault, ethers.MaxUint256 / 2n); + await vault.$_mint(this.holder, ethers.MaxUint256 / 2n); + + Object.assign(this, { token, vault }); }); it('redeem', async function () { - expect(await this.vault.previewRedeem(valueWithFees)).to.be.bignumber.equal(valueWithoutFees); - ({ tx: this.tx } = await this.vault.redeem(valueWithFees, recipient, holder, { from: holder })); + expect(await this.vault.previewRedeem(valueWithFees)).to.equal(valueWithoutFees); + this.tx = this.vault.connect(this.holder).redeem(valueWithFees, this.recipient, this.holder); }); it('withdraw', async function () { - expect(await this.vault.previewWithdraw(valueWithoutFees)).to.be.bignumber.equal(valueWithFees); - ({ tx: this.tx } = await this.vault.withdraw(valueWithoutFees, recipient, holder, { from: holder })); + expect(await this.vault.previewWithdraw(valueWithoutFees)).to.equal(valueWithFees); + this.tx = this.vault.connect(this.holder).withdraw(valueWithoutFees, this.recipient, this.holder); }); afterEach(async function () { - // withdraw principal - await expectEvent.inTransaction(this.tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: valueWithoutFees, - }); - - // redirect fees - await expectEvent.inTransaction(this.tx, this.token, 'Transfer', { - from: this.vault.address, - to: other, - value: fees, - }); - - // mint shares - await expectEvent.inTransaction(this.tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: valueWithFees, - }); - - // withdraw event - await expectEvent.inTransaction(this.tx, this.vault, 'Withdraw', { - sender: holder, - receiver: recipient, - owner: holder, - assets: valueWithoutFees, - shares: valueWithFees, - }); + await expect(this.tx).to.changeTokenBalances( + this.token, + [this.vault, this.recipient, this.other], + [-valueWithFees, valueWithoutFees, fees], + ); + await expect(this.tx).to.changeTokenBalance(this.vault, this.holder, -valueWithFees); + await expect(this.tx) + // withdraw principal + .to.emit(this.token, 'Transfer') + .withArgs(this.vault.target, this.recipient.address, valueWithoutFees) + // redirect fees + .to.emit(this.token, 'Transfer') + .withArgs(this.vault.target, this.other.address, fees) + // mint shares + .to.emit(this.vault, 'Transfer') + .withArgs(this.holder.address, ethers.ZeroAddress, valueWithFees) + // withdraw event + .to.emit(this.vault, 'Withdraw') + .withArgs(this.holder.address, this.recipient.address, this.holder.address, valueWithoutFees, valueWithFees); }); }); }); @@ -866,244 +730,161 @@ contract('ERC4626', function (accounts) { /// https://github.com/transmissions11/solmate/blob/main/src/test/ERC4626.t.sol it('multiple mint, deposit, redeem & withdrawal', async function () { // test designed with both asset using similar decimals - this.token = await ERC20Decimals.new(name, symbol, 18); - this.vault = await ERC4626.new(name + ' Vault', symbol + 'V', this.token.address); + const [alice, bruce] = this.accounts; + const token = await ethers.deployContract('$ERC20DecimalsMock', [name, symbol, 18n]); + const vault = await ethers.deployContract('$ERC4626', ['', '', token]); - await this.token.$_mint(user1, 4000); - await this.token.$_mint(user2, 7001); - await this.token.approve(this.vault.address, 4000, { from: user1 }); - await this.token.approve(this.vault.address, 7001, { from: user2 }); + await token.$_mint(alice, 4000n); + await token.$_mint(bruce, 7001n); + await token.connect(alice).approve(vault, 4000n); + await token.connect(bruce).approve(vault, 7001n); // 1. Alice mints 2000 shares (costs 2000 tokens) - { - const { tx } = await this.vault.mint(2000, user1, { from: user1 }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: user1, - to: this.vault.address, - value: '2000', - }); - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: user1, - value: '2000', - }); - - expect(await this.vault.previewDeposit(2000)).to.be.bignumber.equal('2000'); - expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000'); - expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('0'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2000'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0'); - expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( - '2000', - ); - expect(await this.vault.totalSupply()).to.be.bignumber.equal('2000'); - expect(await this.vault.totalAssets()).to.be.bignumber.equal('2000'); - } - - // 2. Bob deposits 4000 tokens (mints 4000 shares) - { - const { tx } = await this.vault.mint(4000, user2, { from: user2 }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: user2, - to: this.vault.address, - value: '4000', - }); - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: user2, - value: '4000', - }); - - expect(await this.vault.previewDeposit(4000)).to.be.bignumber.equal('4000'); - expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000'); - expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2000'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('4000'); - expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( - '6000', - ); - expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000'); - expect(await this.vault.totalAssets()).to.be.bignumber.equal('6000'); - } + await expect(vault.connect(alice).mint(2000n, alice)) + .to.emit(token, 'Transfer') + .withArgs(alice.address, vault.target, 2000n) + .to.emit(vault, 'Transfer') + .withArgs(ethers.ZeroAddress, alice.address, 2000n); + + expect(await vault.previewDeposit(2000n)).to.equal(2000n); + expect(await vault.balanceOf(alice)).to.equal(2000n); + expect(await vault.balanceOf(bruce)).to.equal(0n); + expect(await vault.convertToAssets(await vault.balanceOf(alice))).to.equal(2000n); + expect(await vault.convertToAssets(await vault.balanceOf(bruce))).to.equal(0n); + expect(await vault.convertToShares(await token.balanceOf(vault))).to.equal(2000n); + expect(await vault.totalSupply()).to.equal(2000n); + expect(await vault.totalAssets()).to.equal(2000n); + + // 2. Bruce deposits 4000 tokens (mints 4000 shares) + await expect(vault.connect(bruce).mint(4000n, bruce)) + .to.emit(token, 'Transfer') + .withArgs(bruce.address, vault.target, 4000n) + .to.emit(vault, 'Transfer') + .withArgs(ethers.ZeroAddress, bruce.address, 4000n); + + expect(await vault.previewDeposit(4000n)).to.equal(4000n); + expect(await vault.balanceOf(alice)).to.equal(2000n); + expect(await vault.balanceOf(bruce)).to.equal(4000n); + expect(await vault.convertToAssets(await vault.balanceOf(alice))).to.equal(2000n); + expect(await vault.convertToAssets(await vault.balanceOf(bruce))).to.equal(4000n); + expect(await vault.convertToShares(await token.balanceOf(vault))).to.equal(6000n); + expect(await vault.totalSupply()).to.equal(6000n); + expect(await vault.totalAssets()).to.equal(6000n); // 3. Vault mutates by +3000 tokens (simulated yield returned from strategy) - await this.token.$_mint(this.vault.address, 3000); - - expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000'); - expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2999'); // used to be 3000, but virtual assets/shares captures part of the yield - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('5999'); // used to be 6000, but virtual assets/shares captures part of the yield - expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( - '6000', - ); - expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000'); - expect(await this.vault.totalAssets()).to.be.bignumber.equal('9000'); + await token.$_mint(vault, 3000n); - // 4. Alice deposits 2000 tokens (mints 1333 shares) - { - const { tx } = await this.vault.deposit(2000, user1, { from: user1 }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: user1, - to: this.vault.address, - value: '2000', - }); - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: user1, - value: '1333', - }); + expect(await vault.balanceOf(alice)).to.equal(2000n); + expect(await vault.balanceOf(bruce)).to.equal(4000n); + expect(await vault.convertToAssets(await vault.balanceOf(alice))).to.equal(2999n); // used to be 3000, but virtual assets/shares captures part of the yield + expect(await vault.convertToAssets(await vault.balanceOf(bruce))).to.equal(5999n); // used to be 6000, but virtual assets/shares captures part of the yield + expect(await vault.convertToShares(await token.balanceOf(vault))).to.equal(6000n); + expect(await vault.totalSupply()).to.equal(6000n); + expect(await vault.totalAssets()).to.equal(9000n); - expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333'); - expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('4999'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('6000'); - expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( - '7333', - ); - expect(await this.vault.totalSupply()).to.be.bignumber.equal('7333'); - expect(await this.vault.totalAssets()).to.be.bignumber.equal('11000'); - } - - // 5. Bob mints 2000 shares (costs 3001 assets) - // NOTE: Bob's assets spent got rounded towards infinity + // 4. Alice deposits 2000 tokens (mints 1333 shares) + await expect(vault.connect(alice).deposit(2000n, alice)) + .to.emit(token, 'Transfer') + .withArgs(alice.address, vault.target, 2000n) + .to.emit(vault, 'Transfer') + .withArgs(ethers.ZeroAddress, alice.address, 1333n); + + expect(await vault.balanceOf(alice)).to.equal(3333n); + expect(await vault.balanceOf(bruce)).to.equal(4000n); + expect(await vault.convertToAssets(await vault.balanceOf(alice))).to.equal(4999n); + expect(await vault.convertToAssets(await vault.balanceOf(bruce))).to.equal(6000n); + expect(await vault.convertToShares(await token.balanceOf(vault))).to.equal(7333n); + expect(await vault.totalSupply()).to.equal(7333n); + expect(await vault.totalAssets()).to.equal(11000n); + + // 5. Bruce mints 2000 shares (costs 3001 assets) + // NOTE: Bruce's assets spent got rounded towards infinity // NOTE: Alices's vault assets got rounded towards infinity - { - const { tx } = await this.vault.mint(2000, user2, { from: user2 }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: user2, - to: this.vault.address, - value: '3000', // used to be 3001 - }); - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: user2, - value: '2000', - }); - - expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333'); - expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('4999'); // used to be 5000 - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('9000'); - expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( - '9333', - ); - expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333'); - expect(await this.vault.totalAssets()).to.be.bignumber.equal('14000'); // used to be 14001 - } + await expect(vault.connect(bruce).mint(2000n, bruce)) + .to.emit(token, 'Transfer') + .withArgs(bruce.address, vault.target, 3000n) + .to.emit(vault, 'Transfer') + .withArgs(ethers.ZeroAddress, bruce.address, 2000n); + + expect(await vault.balanceOf(alice)).to.equal(3333n); + expect(await vault.balanceOf(bruce)).to.equal(6000n); + expect(await vault.convertToAssets(await vault.balanceOf(alice))).to.equal(4999n); // used to be 5000 + expect(await vault.convertToAssets(await vault.balanceOf(bruce))).to.equal(9000n); + expect(await vault.convertToShares(await token.balanceOf(vault))).to.equal(9333n); + expect(await vault.totalSupply()).to.equal(9333n); + expect(await vault.totalAssets()).to.equal(14000n); // used to be 14001 // 6. Vault mutates by +3000 tokens // NOTE: Vault holds 17001 tokens, but sum of assetsOf() is 17000. - await this.token.$_mint(this.vault.address, 3000); - - expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333'); - expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('6070'); // used to be 6071 - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10928'); // used to be 10929 - expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( - '9333', - ); - expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333'); - expect(await this.vault.totalAssets()).to.be.bignumber.equal('17000'); // used to be 17001 + await token.$_mint(vault, 3000n); - // 7. Alice redeem 1333 shares (2428 assets) - { - const { tx } = await this.vault.redeem(1333, user1, user1, { from: user1 }); - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: user1, - to: constants.ZERO_ADDRESS, - value: '1333', - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: user1, - value: '2427', // used to be 2428 - }); - - expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000'); - expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3643'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10929'); - expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( - '8000', - ); - expect(await this.vault.totalSupply()).to.be.bignumber.equal('8000'); - expect(await this.vault.totalAssets()).to.be.bignumber.equal('14573'); - } - - // 8. Bob withdraws 2929 assets (1608 shares) - { - const { tx } = await this.vault.withdraw(2929, user2, user2, { from: user2 }); - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: user2, - to: constants.ZERO_ADDRESS, - value: '1608', - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: user2, - value: '2929', - }); + expect(await vault.balanceOf(alice)).to.equal(3333n); + expect(await vault.balanceOf(bruce)).to.equal(6000n); + expect(await vault.convertToAssets(await vault.balanceOf(alice))).to.equal(6070n); // used to be 6071 + expect(await vault.convertToAssets(await vault.balanceOf(bruce))).to.equal(10928n); // used to be 10929 + expect(await vault.convertToShares(await token.balanceOf(vault))).to.equal(9333n); + expect(await vault.totalSupply()).to.equal(9333n); + expect(await vault.totalAssets()).to.equal(17000n); // used to be 17001 - expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000'); - expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3643'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8000'); - expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( - '6392', - ); - expect(await this.vault.totalSupply()).to.be.bignumber.equal('6392'); - expect(await this.vault.totalAssets()).to.be.bignumber.equal('11644'); - } + // 7. Alice redeem 1333 shares (2428 assets) + await expect(vault.connect(alice).redeem(1333n, alice, alice)) + .to.emit(vault, 'Transfer') + .withArgs(alice.address, ethers.ZeroAddress, 1333n) + .to.emit(token, 'Transfer') + .withArgs(vault.target, alice.address, 2427n); // used to be 2428 + + expect(await vault.balanceOf(alice)).to.equal(2000n); + expect(await vault.balanceOf(bruce)).to.equal(6000n); + expect(await vault.convertToAssets(await vault.balanceOf(alice))).to.equal(3643n); + expect(await vault.convertToAssets(await vault.balanceOf(bruce))).to.equal(10929n); + expect(await vault.convertToShares(await token.balanceOf(vault))).to.equal(8000n); + expect(await vault.totalSupply()).to.equal(8000n); + expect(await vault.totalAssets()).to.equal(14573n); + + // 8. Bruce withdraws 2929 assets (1608 shares) + await expect(vault.connect(bruce).withdraw(2929n, bruce, bruce)) + .to.emit(vault, 'Transfer') + .withArgs(bruce.address, ethers.ZeroAddress, 1608n) + .to.emit(token, 'Transfer') + .withArgs(vault.target, bruce.address, 2929n); + + expect(await vault.balanceOf(alice)).to.equal(2000n); + expect(await vault.balanceOf(bruce)).to.equal(4392n); + expect(await vault.convertToAssets(await vault.balanceOf(alice))).to.equal(3643n); + expect(await vault.convertToAssets(await vault.balanceOf(bruce))).to.equal(8000n); + expect(await vault.convertToShares(await token.balanceOf(vault))).to.equal(6392n); + expect(await vault.totalSupply()).to.equal(6392n); + expect(await vault.totalAssets()).to.equal(11644n); // 9. Alice withdraws 3643 assets (2000 shares) - // NOTE: Bob's assets have been rounded back towards infinity - { - const { tx } = await this.vault.withdraw(3643, user1, user1, { from: user1 }); - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: user1, - to: constants.ZERO_ADDRESS, - value: '2000', - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: user1, - value: '3643', - }); - - expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('0'); - expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8000'); // used to be 8001 - expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( - '4392', - ); - expect(await this.vault.totalSupply()).to.be.bignumber.equal('4392'); - expect(await this.vault.totalAssets()).to.be.bignumber.equal('8001'); - } - - // 10. Bob redeem 4392 shares (8001 tokens) - { - const { tx } = await this.vault.redeem(4392, user2, user2, { from: user2 }); - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: user2, - to: constants.ZERO_ADDRESS, - value: '4392', - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: user2, - value: '8000', // used to be 8001 - }); - - expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('0'); - expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('0'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0'); - expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( - '0', - ); - expect(await this.vault.totalSupply()).to.be.bignumber.equal('0'); - expect(await this.vault.totalAssets()).to.be.bignumber.equal('1'); // used to be 0 - } + // NOTE: Bruce's assets have been rounded back towards infinity + await expect(vault.connect(alice).withdraw(3643n, alice, alice)) + .to.emit(vault, 'Transfer') + .withArgs(alice.address, ethers.ZeroAddress, 2000n) + .to.emit(token, 'Transfer') + .withArgs(vault.target, alice.address, 3643n); + + expect(await vault.balanceOf(alice)).to.equal(0n); + expect(await vault.balanceOf(bruce)).to.equal(4392n); + expect(await vault.convertToAssets(await vault.balanceOf(alice))).to.equal(0n); + expect(await vault.convertToAssets(await vault.balanceOf(bruce))).to.equal(8000n); // used to be 8001 + expect(await vault.convertToShares(await token.balanceOf(vault))).to.equal(4392n); + expect(await vault.totalSupply()).to.equal(4392n); + expect(await vault.totalAssets()).to.equal(8001n); + + // 10. Bruce redeem 4392 shares (8001 tokens) + await expect(vault.connect(bruce).redeem(4392n, bruce, bruce)) + .to.emit(vault, 'Transfer') + .withArgs(bruce.address, ethers.ZeroAddress, 4392n) + .to.emit(token, 'Transfer') + .withArgs(vault.target, bruce.address, 8000n); // used to be 8001 + + expect(await vault.balanceOf(alice)).to.equal(0n); + expect(await vault.balanceOf(bruce)).to.equal(0n); + expect(await vault.convertToAssets(await vault.balanceOf(alice))).to.equal(0n); + expect(await vault.convertToAssets(await vault.balanceOf(bruce))).to.equal(0n); + expect(await vault.convertToShares(await token.balanceOf(vault))).to.equal(0n); + expect(await vault.totalSupply()).to.equal(0n); + expect(await vault.totalAssets()).to.equal(1n); // used to be 0 }); }); diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js index 2b88f5f8edc..03a5b7cca83 100644 --- a/test/utils/cryptography/EIP712.test.js +++ b/test/utils/cryptography/EIP712.test.js @@ -3,6 +3,7 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { getDomain, domainSeparator, hashTypedData } = require('../../helpers/eip712'); +const { formatType } = require('../../helpers/eip712-types'); const { getChainId } = require('../../helpers/chainid'); const LENGTHS = { @@ -77,10 +78,10 @@ describe('EIP712', function () { it('digest', async function () { const types = { - Mail: [ - { name: 'to', type: 'address' }, - { name: 'contents', type: 'string' }, - ], + Mail: formatType({ + to: 'address', + contents: 'string', + }), }; const message = { From 88211e8fbad2e690e62c0dbfc4b14b2d2b6ab566 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 14 Dec 2023 10:07:55 +0100 Subject: [PATCH 34/44] Migrate ERC721 tests (#4793) Co-authored-by: ernestognw --- test/token/ERC721/ERC721.behavior.js | 850 +++++++++--------- test/token/ERC721/ERC721.test.js | 22 +- test/token/ERC721/ERC721Enumerable.test.js | 24 +- .../ERC721/extensions/ERC721Burnable.test.js | 79 +- .../extensions/ERC721Consecutive.test.js | 252 +++--- .../ERC721/extensions/ERC721Pausable.test.js | 75 +- .../ERC721/extensions/ERC721Royalty.test.js | 60 +- .../extensions/ERC721URIStorage.test.js | 105 ++- .../ERC721/extensions/ERC721Wrapper.test.js | 324 +++---- test/token/ERC721/utils/ERC721Holder.test.js | 24 +- test/token/common/ERC2981.behavior.js | 163 ++-- 11 files changed, 955 insertions(+), 1023 deletions(-) diff --git a/test/token/ERC721/ERC721.behavior.js b/test/token/ERC721/ERC721.behavior.js index 10f84826547..32d67d90d98 100644 --- a/test/token/ERC721/ERC721.behavior.js +++ b/test/token/ERC721/ERC721.behavior.js @@ -1,68 +1,76 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { ZERO_ADDRESS } = constants; +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); +const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const { Enum } = require('../../helpers/enums'); - -const ERC721ReceiverMock = artifacts.require('ERC721ReceiverMock'); -const NonERC721ReceiverMock = artifacts.require('CallReceiverMock'); +const { + bigint: { Enum }, +} = require('../../helpers/enums'); const RevertType = Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'); -const firstTokenId = new BN('5042'); -const secondTokenId = new BN('79217'); -const nonExistentTokenId = new BN('13'); -const fourthTokenId = new BN(4); +const firstTokenId = 5042n; +const secondTokenId = 79217n; +const nonExistentTokenId = 13n; +const fourthTokenId = 4n; const baseURI = 'https://api.example.com/v1/'; const RECEIVER_MAGIC_VALUE = '0x150b7a02'; -function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, operator, other) { +function shouldBehaveLikeERC721() { + beforeEach(async function () { + const [owner, newOwner, approved, operator, other] = this.accounts; + Object.assign(this, { owner, newOwner, approved, operator, other }); + }); + shouldSupportInterfaces(['ERC165', 'ERC721']); - context('with minted tokens', function () { + describe('with minted tokens', function () { beforeEach(async function () { - await this.token.$_mint(owner, firstTokenId); - await this.token.$_mint(owner, secondTokenId); - this.toWhom = other; // default to other for toWhom in context-dependent tests + await this.token.$_mint(this.owner, firstTokenId); + await this.token.$_mint(this.owner, secondTokenId); + this.to = this.other; }); describe('balanceOf', function () { - context('when the given address owns some tokens', function () { + describe('when the given address owns some tokens', function () { it('returns the amount of tokens owned by the given address', async function () { - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('2'); + expect(await this.token.balanceOf(this.owner)).to.equal(2n); }); }); - context('when the given address does not own any tokens', function () { + describe('when the given address does not own any tokens', function () { it('returns 0', async function () { - expect(await this.token.balanceOf(other)).to.be.bignumber.equal('0'); + expect(await this.token.balanceOf(this.other)).to.equal(0n); }); }); - context('when querying the zero address', function () { + describe('when querying the zero address', function () { it('throws', async function () { - await expectRevertCustomError(this.token.balanceOf(ZERO_ADDRESS), 'ERC721InvalidOwner', [ZERO_ADDRESS]); + await expect(this.token.balanceOf(ethers.ZeroAddress)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidOwner') + .withArgs(ethers.ZeroAddress); }); }); }); describe('ownerOf', function () { - context('when the given token ID was tracked by this token', function () { + describe('when the given token ID was tracked by this token', function () { const tokenId = firstTokenId; it('returns the owner of the given token ID', async function () { - expect(await this.token.ownerOf(tokenId)).to.be.equal(owner); + expect(await this.token.ownerOf(tokenId)).to.equal(this.owner.address); }); }); - context('when the given token ID was not tracked by this token', function () { + describe('when the given token ID was not tracked by this token', function () { const tokenId = nonExistentTokenId; it('reverts', async function () { - await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); + await expect(this.token.ownerOf(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); }); }); }); @@ -71,194 +79,204 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper const tokenId = firstTokenId; const data = '0x42'; - let receipt = null; - beforeEach(async function () { - await this.token.approve(approved, tokenId, { from: owner }); - await this.token.setApprovalForAll(operator, true, { from: owner }); + await this.token.connect(this.owner).approve(this.approved, tokenId); + await this.token.connect(this.owner).setApprovalForAll(this.operator, true); }); - const transferWasSuccessful = function ({ owner, tokenId }) { + const transferWasSuccessful = () => { it('transfers the ownership of the given token ID to the given address', async function () { - expect(await this.token.ownerOf(tokenId)).to.be.equal(this.toWhom); + await this.tx(); + expect(await this.token.ownerOf(tokenId)).to.equal(this.to.address ?? this.to.target); }); it('emits a Transfer event', async function () { - expectEvent(receipt, 'Transfer', { from: owner, to: this.toWhom, tokenId: tokenId }); + await expect(this.tx()) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, this.to.address ?? this.to.target, tokenId); }); it('clears the approval for the token ID with no event', async function () { - expect(await this.token.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS); - expectEvent.notEmitted(receipt, 'Approval'); + await expect(this.tx()).to.not.emit(this.token, 'Approval'); + + expect(await this.token.getApproved(tokenId)).to.equal(ethers.ZeroAddress); }); it('adjusts owners balances', async function () { - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1'); + const balanceBefore = await this.token.balanceOf(this.owner); + await this.tx(); + expect(await this.token.balanceOf(this.owner)).to.equal(balanceBefore - 1n); }); it('adjusts owners tokens by index', async function () { if (!this.token.tokenOfOwnerByIndex) return; - expect(await this.token.tokenOfOwnerByIndex(this.toWhom, 0)).to.be.bignumber.equal(tokenId); - - expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.not.equal(tokenId); + await this.tx(); + expect(await this.token.tokenOfOwnerByIndex(this.to, 0n)).to.equal(tokenId); + expect(await this.token.tokenOfOwnerByIndex(this.owner, 0n)).to.not.equal(tokenId); }); }; - const shouldTransferTokensByUsers = function (transferFunction, opts = {}) { - context('when called by the owner', function () { + const shouldTransferTokensByUsers = function (fragment, opts = {}) { + describe('when called by the owner', function () { beforeEach(async function () { - receipt = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: owner }); + this.tx = () => + this.token.connect(this.owner)[fragment](this.owner, this.to, tokenId, ...(opts.extra ?? [])); }); - transferWasSuccessful({ owner, tokenId, approved }); + transferWasSuccessful(); }); - context('when called by the approved individual', function () { + describe('when called by the approved individual', function () { beforeEach(async function () { - receipt = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: approved }); + this.tx = () => + this.token.connect(this.approved)[fragment](this.owner, this.to, tokenId, ...(opts.extra ?? [])); }); - transferWasSuccessful({ owner, tokenId, approved }); + transferWasSuccessful(); }); - context('when called by the operator', function () { + describe('when called by the operator', function () { beforeEach(async function () { - receipt = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: operator }); + this.tx = () => + this.token.connect(this.operator)[fragment](this.owner, this.to, tokenId, ...(opts.extra ?? [])); }); - transferWasSuccessful({ owner, tokenId, approved }); + transferWasSuccessful(); }); - context('when called by the owner without an approved user', function () { + describe('when called by the owner without an approved user', function () { beforeEach(async function () { - await this.token.approve(ZERO_ADDRESS, tokenId, { from: owner }); - receipt = await transferFunction.call(this, owner, this.toWhom, tokenId, { from: operator }); + await this.token.connect(this.owner).approve(ethers.ZeroAddress, tokenId); + this.tx = () => + this.token.connect(this.operator)[fragment](this.owner, this.to, tokenId, ...(opts.extra ?? [])); }); - transferWasSuccessful({ owner, tokenId, approved: null }); + transferWasSuccessful(); }); - context('when sent to the owner', function () { + describe('when sent to the owner', function () { beforeEach(async function () { - receipt = await transferFunction.call(this, owner, owner, tokenId, { from: owner }); + this.tx = () => + this.token.connect(this.owner)[fragment](this.owner, this.owner, tokenId, ...(opts.extra ?? [])); }); it('keeps ownership of the token', async function () { - expect(await this.token.ownerOf(tokenId)).to.be.equal(owner); + await this.tx(); + expect(await this.token.ownerOf(tokenId)).to.equal(this.owner.address); }); it('clears the approval for the token ID', async function () { - expect(await this.token.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS); + await this.tx(); + expect(await this.token.getApproved(tokenId)).to.equal(ethers.ZeroAddress); }); it('emits only a transfer event', async function () { - expectEvent(receipt, 'Transfer', { - from: owner, - to: owner, - tokenId: tokenId, - }); + await expect(this.tx()) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, this.owner.address, tokenId); }); it('keeps the owner balance', async function () { - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('2'); + const balanceBefore = await this.token.balanceOf(this.owner); + await this.tx(); + expect(await this.token.balanceOf(this.owner)).to.equal(balanceBefore); }); it('keeps same tokens by index', async function () { if (!this.token.tokenOfOwnerByIndex) return; - const tokensListed = await Promise.all([0, 1].map(i => this.token.tokenOfOwnerByIndex(owner, i))); - expect(tokensListed.map(t => t.toNumber())).to.have.members([ - firstTokenId.toNumber(), - secondTokenId.toNumber(), - ]); + + expect(await Promise.all([0n, 1n].map(i => this.token.tokenOfOwnerByIndex(this.owner, i)))).to.have.members( + [firstTokenId, secondTokenId], + ); }); }); - context('when the address of the previous owner is incorrect', function () { + describe('when the address of the previous owner is incorrect', function () { it('reverts', async function () { - await expectRevertCustomError( - transferFunction.call(this, other, other, tokenId, { from: owner }), - 'ERC721IncorrectOwner', - [other, tokenId, owner], - ); + await expect( + this.token.connect(this.owner)[fragment](this.other, this.other, tokenId, ...(opts.extra ?? [])), + ) + .to.be.revertedWithCustomError(this.token, 'ERC721IncorrectOwner') + .withArgs(this.other.address, tokenId, this.owner.address); }); }); - context('when the sender is not authorized for the token id', function () { + describe('when the sender is not authorized for the token id', function () { if (opts.unrestricted) { it('does not revert', async function () { - await transferFunction.call(this, owner, other, tokenId, { from: other }); + await this.token.connect(this.other)[fragment](this.owner, this.other, tokenId, ...(opts.extra ?? [])); }); } else { it('reverts', async function () { - await expectRevertCustomError( - transferFunction.call(this, owner, other, tokenId, { from: other }), - 'ERC721InsufficientApproval', - [other, tokenId], - ); + await expect( + this.token.connect(this.other)[fragment](this.owner, this.other, tokenId, ...(opts.extra ?? [])), + ) + .to.be.revertedWithCustomError(this.token, 'ERC721InsufficientApproval') + .withArgs(this.other.address, tokenId); }); } }); - context('when the given token ID does not exist', function () { + describe('when the given token ID does not exist', function () { it('reverts', async function () { - await expectRevertCustomError( - transferFunction.call(this, owner, other, nonExistentTokenId, { from: owner }), - 'ERC721NonexistentToken', - [nonExistentTokenId], - ); + await expect( + this.token + .connect(this.owner) + [fragment](this.owner, this.other, nonExistentTokenId, ...(opts.extra ?? [])), + ) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(nonExistentTokenId); }); }); - context('when the address to transfer the token to is the zero address', function () { + describe('when the address to transfer the token to is the zero address', function () { it('reverts', async function () { - await expectRevertCustomError( - transferFunction.call(this, owner, ZERO_ADDRESS, tokenId, { from: owner }), - 'ERC721InvalidReceiver', - [ZERO_ADDRESS], - ); + await expect( + this.token.connect(this.owner)[fragment](this.owner, ethers.ZeroAddress, tokenId, ...(opts.extra ?? [])), + ) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(ethers.ZeroAddress); }); }); }; - const shouldTransferSafely = function (transferFun, data, opts = {}) { + const shouldTransferSafely = function (fragment, data, opts = {}) { + // sanity + it('function exists', async function () { + expect(this.token.interface.hasFunction(fragment)).to.be.true; + }); + describe('to a user account', function () { - shouldTransferTokensByUsers(transferFun, opts); + shouldTransferTokensByUsers(fragment, opts); }); describe('to a valid receiver contract', function () { beforeEach(async function () { - this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.None); - this.toWhom = this.receiver.address; + this.to = await ethers.deployContract('ERC721ReceiverMock', [RECEIVER_MAGIC_VALUE, RevertType.None]); }); - shouldTransferTokensByUsers(transferFun, opts); + shouldTransferTokensByUsers(fragment, opts); it('calls onERC721Received', async function () { - const receipt = await transferFun.call(this, owner, this.receiver.address, tokenId, { from: owner }); - - await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', { - operator: owner, - from: owner, - tokenId: tokenId, - data: data, - }); + await expect(this.token.connect(this.owner)[fragment](this.owner, this.to, tokenId, ...(opts.extra ?? []))) + .to.emit(this.to, 'Received') + .withArgs(this.owner.address, this.owner.address, tokenId, data, anyValue); }); it('calls onERC721Received from approved', async function () { - const receipt = await transferFun.call(this, owner, this.receiver.address, tokenId, { from: approved }); - - await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', { - operator: approved, - from: owner, - tokenId: tokenId, - data: data, - }); + await expect( + this.token.connect(this.approved)[fragment](this.owner, this.to, tokenId, ...(opts.extra ?? [])), + ) + .to.emit(this.to, 'Received') + .withArgs(this.approved.address, this.owner.address, tokenId, data, anyValue); }); describe('with an invalid token id', function () { it('reverts', async function () { - await expectRevertCustomError( - transferFun.call(this, owner, this.receiver.address, nonExistentTokenId, { from: owner }), - 'ERC721NonexistentToken', - [nonExistentTokenId], - ); + await expect( + this.token + .connect(this.approved) + [fragment](this.owner, this.to, nonExistentTokenId, ...(opts.extra ?? [])), + ) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(nonExistentTokenId); }); }); }); @@ -269,9 +287,7 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper { fnName: '$_transfer', opts: { unrestricted: true } }, ]) { describe(`via ${fnName}`, function () { - shouldTransferTokensByUsers(function (from, to, tokenId, opts) { - return this.token[fnName](from, to, tokenId, opts); - }, opts); + shouldTransferTokensByUsers(fnName, opts); }); } @@ -280,103 +296,86 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper { fnName: '$_safeTransfer', opts: { unrestricted: true } }, ]) { describe(`via ${fnName}`, function () { - const safeTransferFromWithData = function (from, to, tokenId, opts) { - return this.token.methods[fnName + '(address,address,uint256,bytes)'](from, to, tokenId, data, opts); - }; - - const safeTransferFromWithoutData = function (from, to, tokenId, opts) { - return this.token.methods[fnName + '(address,address,uint256)'](from, to, tokenId, opts); - }; - describe('with data', function () { - shouldTransferSafely(safeTransferFromWithData, data, opts); + shouldTransferSafely(fnName, data, { ...opts, extra: [ethers.Typed.bytes(data)] }); }); describe('without data', function () { - shouldTransferSafely(safeTransferFromWithoutData, null, opts); + shouldTransferSafely(fnName, '0x', opts); }); describe('to a receiver contract returning unexpected value', function () { it('reverts', async function () { - const invalidReceiver = await ERC721ReceiverMock.new('0x42', RevertType.None); - await expectRevertCustomError( - this.token.methods[fnName + '(address,address,uint256)'](owner, invalidReceiver.address, tokenId, { - from: owner, - }), - 'ERC721InvalidReceiver', - [invalidReceiver.address], - ); + const invalidReceiver = await ethers.deployContract('ERC721ReceiverMock', [ + '0xdeadbeef', + RevertType.None, + ]); + + await expect(this.token.connect(this.owner)[fnName](this.owner, invalidReceiver, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(invalidReceiver.target); }); }); describe('to a receiver contract that reverts with message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new( + const revertingReceiver = await ethers.deployContract('ERC721ReceiverMock', [ RECEIVER_MAGIC_VALUE, RevertType.RevertWithMessage, - ); - await expectRevert( - this.token.methods[fnName + '(address,address,uint256)'](owner, revertingReceiver.address, tokenId, { - from: owner, - }), - 'ERC721ReceiverMock: reverting', - ); + ]); + + await expect( + this.token.connect(this.owner)[fnName](this.owner, revertingReceiver, tokenId), + ).to.be.revertedWith('ERC721ReceiverMock: reverting'); }); }); describe('to a receiver contract that reverts without message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new( + const revertingReceiver = await ethers.deployContract('ERC721ReceiverMock', [ RECEIVER_MAGIC_VALUE, RevertType.RevertWithoutMessage, - ); - await expectRevertCustomError( - this.token.methods[fnName + '(address,address,uint256)'](owner, revertingReceiver.address, tokenId, { - from: owner, - }), - 'ERC721InvalidReceiver', - [revertingReceiver.address], - ); + ]); + + await expect(this.token.connect(this.owner)[fnName](this.owner, revertingReceiver, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(revertingReceiver.target); }); }); describe('to a receiver contract that reverts with custom error', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new( + const revertingReceiver = await ethers.deployContract('ERC721ReceiverMock', [ RECEIVER_MAGIC_VALUE, RevertType.RevertWithCustomError, - ); - await expectRevertCustomError( - this.token.methods[fnName + '(address,address,uint256)'](owner, revertingReceiver.address, tokenId, { - from: owner, - }), - 'CustomError', - [RECEIVER_MAGIC_VALUE], - ); + ]); + + await expect(this.token.connect(this.owner)[fnName](this.owner, revertingReceiver, tokenId)) + .to.be.revertedWithCustomError(revertingReceiver, 'CustomError') + .withArgs(RECEIVER_MAGIC_VALUE); }); }); describe('to a receiver contract that panics', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.Panic); - await expectRevert.unspecified( - this.token.methods[fnName + '(address,address,uint256)'](owner, revertingReceiver.address, tokenId, { - from: owner, - }), - ); + const revertingReceiver = await ethers.deployContract('ERC721ReceiverMock', [ + RECEIVER_MAGIC_VALUE, + RevertType.Panic, + ]); + + await expect( + this.token.connect(this.owner)[fnName](this.owner, revertingReceiver, tokenId), + ).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO); }); }); describe('to a contract that does not implement the required function', function () { it('reverts', async function () { - const nonReceiver = await NonERC721ReceiverMock.new(); - await expectRevertCustomError( - this.token.methods[fnName + '(address,address,uint256)'](owner, nonReceiver.address, tokenId, { - from: owner, - }), - 'ERC721InvalidReceiver', - [nonReceiver.address], - ); + const nonReceiver = await ethers.deployContract('CallReceiverMock'); + + await expect(this.token.connect(this.owner)[fnName](this.owner, nonReceiver, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(nonReceiver.target); }); }); }); @@ -390,88 +389,90 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper describe('via safeMint', function () { // regular minting is tested in ERC721Mintable.test.js and others it('calls onERC721Received — with data', async function () { - this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.None); - const receipt = await this.token.$_safeMint(this.receiver.address, tokenId, data); + const receiver = await ethers.deployContract('ERC721ReceiverMock', [RECEIVER_MAGIC_VALUE, RevertType.None]); - await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', { - from: ZERO_ADDRESS, - tokenId: tokenId, - data: data, - }); + await expect(await this.token.$_safeMint(receiver, tokenId, ethers.Typed.bytes(data))) + .to.emit(receiver, 'Received') + .withArgs(anyValue, ethers.ZeroAddress, tokenId, data, anyValue); }); it('calls onERC721Received — without data', async function () { - this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.None); - const receipt = await this.token.$_safeMint(this.receiver.address, tokenId); + const receiver = await ethers.deployContract('ERC721ReceiverMock', [RECEIVER_MAGIC_VALUE, RevertType.None]); - await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', { - from: ZERO_ADDRESS, - tokenId: tokenId, - }); + await expect(await this.token.$_safeMint(receiver, tokenId)) + .to.emit(receiver, 'Received') + .withArgs(anyValue, ethers.ZeroAddress, tokenId, '0x', anyValue); }); - context('to a receiver contract returning unexpected value', function () { + describe('to a receiver contract returning unexpected value', function () { it('reverts', async function () { - const invalidReceiver = await ERC721ReceiverMock.new('0x42', RevertType.None); - await expectRevertCustomError( - this.token.$_safeMint(invalidReceiver.address, tokenId), - 'ERC721InvalidReceiver', - [invalidReceiver.address], - ); + const invalidReceiver = await ethers.deployContract('ERC721ReceiverMock', ['0xdeadbeef', RevertType.None]); + + await expect(this.token.$_safeMint(invalidReceiver, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(invalidReceiver.target); }); }); - context('to a receiver contract that reverts with message', function () { + describe('to a receiver contract that reverts with message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithMessage); - await expectRevert( - this.token.$_safeMint(revertingReceiver.address, tokenId), + const revertingReceiver = await ethers.deployContract('ERC721ReceiverMock', [ + RECEIVER_MAGIC_VALUE, + RevertType.RevertWithMessage, + ]); + + await expect(this.token.$_safeMint(revertingReceiver, tokenId)).to.be.revertedWith( 'ERC721ReceiverMock: reverting', ); }); }); - context('to a receiver contract that reverts without message', function () { + describe('to a receiver contract that reverts without message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new( + const revertingReceiver = await ethers.deployContract('ERC721ReceiverMock', [ RECEIVER_MAGIC_VALUE, RevertType.RevertWithoutMessage, - ); - await expectRevertCustomError( - this.token.$_safeMint(revertingReceiver.address, tokenId), - 'ERC721InvalidReceiver', - [revertingReceiver.address], - ); + ]); + + await expect(this.token.$_safeMint(revertingReceiver, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(revertingReceiver.target); }); }); - context('to a receiver contract that reverts with custom error', function () { + describe('to a receiver contract that reverts with custom error', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new( + const revertingReceiver = await ethers.deployContract('ERC721ReceiverMock', [ RECEIVER_MAGIC_VALUE, RevertType.RevertWithCustomError, - ); - await expectRevertCustomError(this.token.$_safeMint(revertingReceiver.address, tokenId), 'CustomError', [ - RECEIVER_MAGIC_VALUE, ]); + + await expect(this.token.$_safeMint(revertingReceiver, tokenId)) + .to.be.revertedWithCustomError(revertingReceiver, 'CustomError') + .withArgs(RECEIVER_MAGIC_VALUE); }); }); - context('to a receiver contract that panics', function () { + describe('to a receiver contract that panics', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.Panic); - await expectRevert.unspecified(this.token.$_safeMint(revertingReceiver.address, tokenId)); + const revertingReceiver = await ethers.deployContract('ERC721ReceiverMock', [ + RECEIVER_MAGIC_VALUE, + RevertType.Panic, + ]); + + await expect(this.token.$_safeMint(revertingReceiver, tokenId)).to.be.revertedWithPanic( + PANIC_CODES.DIVISION_BY_ZERO, + ); }); }); - context('to a contract that does not implement the required function', function () { + describe('to a contract that does not implement the required function', function () { it('reverts', async function () { - const nonReceiver = await NonERC721ReceiverMock.new(); - await expectRevertCustomError( - this.token.$_safeMint(nonReceiver.address, tokenId), - 'ERC721InvalidReceiver', - [nonReceiver.address], - ); + const nonReceiver = await ethers.deployContract('CallReceiverMock'); + + await expect(this.token.$_safeMint(nonReceiver, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(nonReceiver.target); }); }); }); @@ -480,227 +481,207 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper describe('approve', function () { const tokenId = firstTokenId; - let receipt = null; - const itClearsApproval = function () { it('clears approval for the token', async function () { - expect(await this.token.getApproved(tokenId)).to.be.equal(ZERO_ADDRESS); + expect(await this.token.getApproved(tokenId)).to.equal(ethers.ZeroAddress); }); }; - const itApproves = function (address) { + const itApproves = function () { it('sets the approval for the target address', async function () { - expect(await this.token.getApproved(tokenId)).to.be.equal(address); + expect(await this.token.getApproved(tokenId)).to.equal(this.approved.address ?? this.approved); }); }; - const itEmitsApprovalEvent = function (address) { + const itEmitsApprovalEvent = function () { it('emits an approval event', async function () { - expectEvent(receipt, 'Approval', { - owner: owner, - approved: address, - tokenId: tokenId, - }); + await expect(this.tx) + .to.emit(this.token, 'Approval') + .withArgs(this.owner.address, this.approved.address ?? this.approved, tokenId); }); }; - context('when clearing approval', function () { - context('when there was no prior approval', function () { + describe('when clearing approval', function () { + describe('when there was no prior approval', function () { beforeEach(async function () { - receipt = await this.token.approve(ZERO_ADDRESS, tokenId, { from: owner }); + this.approved = ethers.ZeroAddress; + this.tx = await this.token.connect(this.owner).approve(this.approved, tokenId); }); itClearsApproval(); - itEmitsApprovalEvent(ZERO_ADDRESS); + itEmitsApprovalEvent(); }); - context('when there was a prior approval', function () { + describe('when there was a prior approval', function () { beforeEach(async function () { - await this.token.approve(approved, tokenId, { from: owner }); - receipt = await this.token.approve(ZERO_ADDRESS, tokenId, { from: owner }); + await this.token.connect(this.owner).approve(this.other, tokenId); + this.approved = ethers.ZeroAddress; + this.tx = await this.token.connect(this.owner).approve(this.approved, tokenId); }); itClearsApproval(); - itEmitsApprovalEvent(ZERO_ADDRESS); + itEmitsApprovalEvent(); }); }); - context('when approving a non-zero address', function () { - context('when there was no prior approval', function () { + describe('when approving a non-zero address', function () { + describe('when there was no prior approval', function () { beforeEach(async function () { - receipt = await this.token.approve(approved, tokenId, { from: owner }); + this.tx = await this.token.connect(this.owner).approve(this.approved, tokenId); }); - itApproves(approved); - itEmitsApprovalEvent(approved); + itApproves(); + itEmitsApprovalEvent(); }); - context('when there was a prior approval to the same address', function () { + describe('when there was a prior approval to the same address', function () { beforeEach(async function () { - await this.token.approve(approved, tokenId, { from: owner }); - receipt = await this.token.approve(approved, tokenId, { from: owner }); + await this.token.connect(this.owner).approve(this.approved, tokenId); + this.tx = await this.token.connect(this.owner).approve(this.approved, tokenId); }); - itApproves(approved); - itEmitsApprovalEvent(approved); + itApproves(); + itEmitsApprovalEvent(); }); - context('when there was a prior approval to a different address', function () { + describe('when there was a prior approval to a different address', function () { beforeEach(async function () { - await this.token.approve(anotherApproved, tokenId, { from: owner }); - receipt = await this.token.approve(anotherApproved, tokenId, { from: owner }); + await this.token.connect(this.owner).approve(this.other, tokenId); + this.tx = await this.token.connect(this.owner).approve(this.approved, tokenId); }); - itApproves(anotherApproved); - itEmitsApprovalEvent(anotherApproved); + itApproves(); + itEmitsApprovalEvent(); }); }); - context('when the sender does not own the given token ID', function () { + describe('when the sender does not own the given token ID', function () { it('reverts', async function () { - await expectRevertCustomError( - this.token.approve(approved, tokenId, { from: other }), - 'ERC721InvalidApprover', - [other], - ); + await expect(this.token.connect(this.other).approve(this.approved, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidApprover') + .withArgs(this.other.address); }); }); - context('when the sender is approved for the given token ID', function () { + describe('when the sender is approved for the given token ID', function () { it('reverts', async function () { - await this.token.approve(approved, tokenId, { from: owner }); - await expectRevertCustomError( - this.token.approve(anotherApproved, tokenId, { from: approved }), - 'ERC721InvalidApprover', - [approved], - ); + await this.token.connect(this.owner).approve(this.approved, tokenId); + + await expect(this.token.connect(this.approved).approve(this.other, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidApprover') + .withArgs(this.approved.address); }); }); - context('when the sender is an operator', function () { + describe('when the sender is an operator', function () { beforeEach(async function () { - await this.token.setApprovalForAll(operator, true, { from: owner }); - receipt = await this.token.approve(approved, tokenId, { from: operator }); + await this.token.connect(this.owner).setApprovalForAll(this.operator, true); + + this.tx = await this.token.connect(this.operator).approve(this.approved, tokenId); }); - itApproves(approved); - itEmitsApprovalEvent(approved); + itApproves(); + itEmitsApprovalEvent(); }); - context('when the given token ID does not exist', function () { + describe('when the given token ID does not exist', function () { it('reverts', async function () { - await expectRevertCustomError( - this.token.approve(approved, nonExistentTokenId, { from: operator }), - 'ERC721NonexistentToken', - [nonExistentTokenId], - ); + await expect(this.token.connect(this.operator).approve(this.approved, nonExistentTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(nonExistentTokenId); }); }); }); describe('setApprovalForAll', function () { - context('when the operator willing to approve is not the owner', function () { - context('when there is no operator approval set by the sender', function () { + describe('when the operator willing to approve is not the owner', function () { + describe('when there is no operator approval set by the sender', function () { it('approves the operator', async function () { - await this.token.setApprovalForAll(operator, true, { from: owner }); + await this.token.connect(this.owner).setApprovalForAll(this.operator, true); - expect(await this.token.isApprovedForAll(owner, operator)).to.equal(true); + expect(await this.token.isApprovedForAll(this.owner, this.operator)).to.be.true; }); it('emits an approval event', async function () { - const receipt = await this.token.setApprovalForAll(operator, true, { from: owner }); - - expectEvent(receipt, 'ApprovalForAll', { - owner: owner, - operator: operator, - approved: true, - }); + await expect(this.token.connect(this.owner).setApprovalForAll(this.operator, true)) + .to.emit(this.token, 'ApprovalForAll') + .withArgs(this.owner.address, this.operator.address, true); }); }); - context('when the operator was set as not approved', function () { + describe('when the operator was set as not approved', function () { beforeEach(async function () { - await this.token.setApprovalForAll(operator, false, { from: owner }); + await this.token.connect(this.owner).setApprovalForAll(this.operator, false); }); it('approves the operator', async function () { - await this.token.setApprovalForAll(operator, true, { from: owner }); + await this.token.connect(this.owner).setApprovalForAll(this.operator, true); - expect(await this.token.isApprovedForAll(owner, operator)).to.equal(true); + expect(await this.token.isApprovedForAll(this.owner, this.operator)).to.be.true; }); it('emits an approval event', async function () { - const receipt = await this.token.setApprovalForAll(operator, true, { from: owner }); - - expectEvent(receipt, 'ApprovalForAll', { - owner: owner, - operator: operator, - approved: true, - }); + await expect(this.token.connect(this.owner).setApprovalForAll(this.operator, true)) + .to.emit(this.token, 'ApprovalForAll') + .withArgs(this.owner.address, this.operator.address, true); }); it('can unset the operator approval', async function () { - await this.token.setApprovalForAll(operator, false, { from: owner }); + await this.token.connect(this.owner).setApprovalForAll(this.operator, false); - expect(await this.token.isApprovedForAll(owner, operator)).to.equal(false); + expect(await this.token.isApprovedForAll(this.owner, this.operator)).to.be.false; }); }); - context('when the operator was already approved', function () { + describe('when the operator was already approved', function () { beforeEach(async function () { - await this.token.setApprovalForAll(operator, true, { from: owner }); + await this.token.connect(this.owner).setApprovalForAll(this.operator, true); }); it('keeps the approval to the given address', async function () { - await this.token.setApprovalForAll(operator, true, { from: owner }); + await this.token.connect(this.owner).setApprovalForAll(this.operator, true); - expect(await this.token.isApprovedForAll(owner, operator)).to.equal(true); + expect(await this.token.isApprovedForAll(this.owner, this.operator)).to.be.true; }); it('emits an approval event', async function () { - const receipt = await this.token.setApprovalForAll(operator, true, { from: owner }); - - expectEvent(receipt, 'ApprovalForAll', { - owner: owner, - operator: operator, - approved: true, - }); + await expect(this.token.connect(this.owner).setApprovalForAll(this.operator, true)) + .to.emit(this.token, 'ApprovalForAll') + .withArgs(this.owner.address, this.operator.address, true); }); }); }); - context('when the operator is address zero', function () { + describe('when the operator is address zero', function () { it('reverts', async function () { - await expectRevertCustomError( - this.token.setApprovalForAll(constants.ZERO_ADDRESS, true, { from: owner }), - 'ERC721InvalidOperator', - [constants.ZERO_ADDRESS], - ); + await expect(this.token.connect(this.owner).setApprovalForAll(ethers.ZeroAddress, true)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidOperator') + .withArgs(ethers.ZeroAddress); }); }); }); describe('getApproved', async function () { - context('when token is not minted', async function () { + describe('when token is not minted', async function () { it('reverts', async function () { - await expectRevertCustomError(this.token.getApproved(nonExistentTokenId), 'ERC721NonexistentToken', [ - nonExistentTokenId, - ]); + await expect(this.token.getApproved(nonExistentTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(nonExistentTokenId); }); }); - context('when token has been minted ', async function () { + describe('when token has been minted ', async function () { it('should return the zero address', async function () { - expect(await this.token.getApproved(firstTokenId)).to.be.equal(ZERO_ADDRESS); + expect(await this.token.getApproved(firstTokenId)).to.equal(ethers.ZeroAddress); }); - context('when account has been approved', async function () { + describe('when account has been approved', async function () { beforeEach(async function () { - await this.token.approve(approved, firstTokenId, { from: owner }); + await this.token.connect(this.owner).approve(this.approved, firstTokenId); }); it('returns approved account', async function () { - expect(await this.token.getApproved(firstTokenId)).to.be.equal(approved); + expect(await this.token.getApproved(firstTokenId)).to.equal(this.approved.address); }); }); }); @@ -709,243 +690,268 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper describe('_mint(address, uint256)', function () { it('reverts with a null destination address', async function () { - await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721InvalidReceiver', [ - ZERO_ADDRESS, - ]); + await expect(this.token.$_mint(ethers.ZeroAddress, firstTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(ethers.ZeroAddress); }); - context('with minted token', async function () { + describe('with minted token', async function () { beforeEach(async function () { - this.receipt = await this.token.$_mint(owner, firstTokenId); + this.tx = await this.token.$_mint(this.owner, firstTokenId); }); - it('emits a Transfer event', function () { - expectEvent(this.receipt, 'Transfer', { from: ZERO_ADDRESS, to: owner, tokenId: firstTokenId }); + it('emits a Transfer event', async function () { + await expect(this.tx) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.owner.address, firstTokenId); }); it('creates the token', async function () { - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1'); - expect(await this.token.ownerOf(firstTokenId)).to.equal(owner); + expect(await this.token.balanceOf(this.owner)).to.equal(1n); + expect(await this.token.ownerOf(firstTokenId)).to.equal(this.owner.address); }); it('reverts when adding a token id that already exists', async function () { - await expectRevertCustomError(this.token.$_mint(owner, firstTokenId), 'ERC721InvalidSender', [ZERO_ADDRESS]); + await expect(this.token.$_mint(this.owner, firstTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidSender') + .withArgs(ethers.ZeroAddress); }); }); }); describe('_burn', function () { it('reverts when burning a non-existent token id', async function () { - await expectRevertCustomError(this.token.$_burn(nonExistentTokenId), 'ERC721NonexistentToken', [ - nonExistentTokenId, - ]); + await expect(this.token.$_burn(nonExistentTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(nonExistentTokenId); }); - context('with minted tokens', function () { + describe('with minted tokens', function () { beforeEach(async function () { - await this.token.$_mint(owner, firstTokenId); - await this.token.$_mint(owner, secondTokenId); + await this.token.$_mint(this.owner, firstTokenId); + await this.token.$_mint(this.owner, secondTokenId); }); - context('with burnt token', function () { + describe('with burnt token', function () { beforeEach(async function () { - this.receipt = await this.token.$_burn(firstTokenId); + this.tx = await this.token.$_burn(firstTokenId); }); - it('emits a Transfer event', function () { - expectEvent(this.receipt, 'Transfer', { from: owner, to: ZERO_ADDRESS, tokenId: firstTokenId }); + it('emits a Transfer event', async function () { + await expect(this.tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, firstTokenId); }); it('deletes the token', async function () { - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1'); - await expectRevertCustomError(this.token.ownerOf(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); + expect(await this.token.balanceOf(this.owner)).to.equal(1n); + await expect(this.token.ownerOf(firstTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(firstTokenId); }); it('reverts when burning a token id that has been deleted', async function () { - await expectRevertCustomError(this.token.$_burn(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); + await expect(this.token.$_burn(firstTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(firstTokenId); }); }); }); }); } -function shouldBehaveLikeERC721Enumerable(owner, newOwner, approved, anotherApproved, operator, other) { +function shouldBehaveLikeERC721Enumerable() { + beforeEach(async function () { + const [owner, newOwner, approved, operator, other] = this.accounts; + Object.assign(this, { owner, newOwner, approved, operator, other }); + }); + shouldSupportInterfaces(['ERC721Enumerable']); - context('with minted tokens', function () { + describe('with minted tokens', function () { beforeEach(async function () { - await this.token.$_mint(owner, firstTokenId); - await this.token.$_mint(owner, secondTokenId); - this.toWhom = other; // default to other for toWhom in context-dependent tests + await this.token.$_mint(this.owner, firstTokenId); + await this.token.$_mint(this.owner, secondTokenId); + this.to = this.other; }); describe('totalSupply', function () { it('returns total token supply', async function () { - expect(await this.token.totalSupply()).to.be.bignumber.equal('2'); + expect(await this.token.totalSupply()).to.equal(2n); }); }); describe('tokenOfOwnerByIndex', function () { describe('when the given index is lower than the amount of tokens owned by the given address', function () { it('returns the token ID placed at the given index', async function () { - expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.equal(firstTokenId); + expect(await this.token.tokenOfOwnerByIndex(this.owner, 0n)).to.equal(firstTokenId); }); }); describe('when the index is greater than or equal to the total tokens owned by the given address', function () { it('reverts', async function () { - await expectRevertCustomError(this.token.tokenOfOwnerByIndex(owner, 2), 'ERC721OutOfBoundsIndex', [owner, 2]); + await expect(this.token.tokenOfOwnerByIndex(this.owner, 2n)) + .to.be.revertedWithCustomError(this.token, 'ERC721OutOfBoundsIndex') + .withArgs(this.owner.address, 2n); }); }); describe('when the given address does not own any token', function () { it('reverts', async function () { - await expectRevertCustomError(this.token.tokenOfOwnerByIndex(other, 0), 'ERC721OutOfBoundsIndex', [other, 0]); + await expect(this.token.tokenOfOwnerByIndex(this.other, 0n)) + .to.be.revertedWithCustomError(this.token, 'ERC721OutOfBoundsIndex') + .withArgs(this.other.address, 0n); }); }); describe('after transferring all tokens to another user', function () { beforeEach(async function () { - await this.token.transferFrom(owner, other, firstTokenId, { from: owner }); - await this.token.transferFrom(owner, other, secondTokenId, { from: owner }); + await this.token.connect(this.owner).transferFrom(this.owner, this.other, firstTokenId); + await this.token.connect(this.owner).transferFrom(this.owner, this.other, secondTokenId); }); it('returns correct token IDs for target', async function () { - expect(await this.token.balanceOf(other)).to.be.bignumber.equal('2'); - const tokensListed = await Promise.all([0, 1].map(i => this.token.tokenOfOwnerByIndex(other, i))); - expect(tokensListed.map(t => t.toNumber())).to.have.members([ - firstTokenId.toNumber(), - secondTokenId.toNumber(), + expect(await this.token.balanceOf(this.other)).to.equal(2n); + + expect(await Promise.all([0n, 1n].map(i => this.token.tokenOfOwnerByIndex(this.other, i)))).to.have.members([ + firstTokenId, + secondTokenId, ]); }); it('returns empty collection for original owner', async function () { - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('0'); - await expectRevertCustomError(this.token.tokenOfOwnerByIndex(owner, 0), 'ERC721OutOfBoundsIndex', [owner, 0]); + expect(await this.token.balanceOf(this.owner)).to.equal(0n); + await expect(this.token.tokenOfOwnerByIndex(this.owner, 0n)) + .to.be.revertedWithCustomError(this.token, 'ERC721OutOfBoundsIndex') + .withArgs(this.owner.address, 0n); }); }); }); describe('tokenByIndex', function () { it('returns all tokens', async function () { - const tokensListed = await Promise.all([0, 1].map(i => this.token.tokenByIndex(i))); - expect(tokensListed.map(t => t.toNumber())).to.have.members([ - firstTokenId.toNumber(), - secondTokenId.toNumber(), + expect(await Promise.all([0n, 1n].map(i => this.token.tokenByIndex(i)))).to.have.members([ + firstTokenId, + secondTokenId, ]); }); it('reverts if index is greater than supply', async function () { - await expectRevertCustomError(this.token.tokenByIndex(2), 'ERC721OutOfBoundsIndex', [ZERO_ADDRESS, 2]); + await expect(this.token.tokenByIndex(2n)) + .to.be.revertedWithCustomError(this.token, 'ERC721OutOfBoundsIndex') + .withArgs(ethers.ZeroAddress, 2n); }); - [firstTokenId, secondTokenId].forEach(function (tokenId) { + for (const tokenId of [firstTokenId, secondTokenId]) { it(`returns all tokens after burning token ${tokenId} and minting new tokens`, async function () { - const newTokenId = new BN(300); - const anotherNewTokenId = new BN(400); + const newTokenId = 300n; + const anotherNewTokenId = 400n; await this.token.$_burn(tokenId); - await this.token.$_mint(newOwner, newTokenId); - await this.token.$_mint(newOwner, anotherNewTokenId); + await this.token.$_mint(this.newOwner, newTokenId); + await this.token.$_mint(this.newOwner, anotherNewTokenId); - expect(await this.token.totalSupply()).to.be.bignumber.equal('3'); + expect(await this.token.totalSupply()).to.equal(3n); - const tokensListed = await Promise.all([0, 1, 2].map(i => this.token.tokenByIndex(i))); - const expectedTokens = [firstTokenId, secondTokenId, newTokenId, anotherNewTokenId].filter( - x => x !== tokenId, - ); - expect(tokensListed.map(t => t.toNumber())).to.have.members(expectedTokens.map(t => t.toNumber())); + expect(await Promise.all([0n, 1n, 2n].map(i => this.token.tokenByIndex(i)))) + .to.have.members([firstTokenId, secondTokenId, newTokenId, anotherNewTokenId].filter(x => x !== tokenId)) + .to.not.include(tokenId); }); - }); + } }); }); describe('_mint(address, uint256)', function () { it('reverts with a null destination address', async function () { - await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721InvalidReceiver', [ - ZERO_ADDRESS, - ]); + await expect(this.token.$_mint(ethers.ZeroAddress, firstTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(ethers.ZeroAddress); }); - context('with minted token', async function () { + describe('with minted token', async function () { beforeEach(async function () { - this.receipt = await this.token.$_mint(owner, firstTokenId); + await this.token.$_mint(this.owner, firstTokenId); }); it('adjusts owner tokens by index', async function () { - expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.equal(firstTokenId); + expect(await this.token.tokenOfOwnerByIndex(this.owner, 0n)).to.equal(firstTokenId); }); it('adjusts all tokens list', async function () { - expect(await this.token.tokenByIndex(0)).to.be.bignumber.equal(firstTokenId); + expect(await this.token.tokenByIndex(0n)).to.equal(firstTokenId); }); }); }); describe('_burn', function () { it('reverts when burning a non-existent token id', async function () { - await expectRevertCustomError(this.token.$_burn(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); + await expect(this.token.$_burn(firstTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(firstTokenId); }); - context('with minted tokens', function () { + describe('with minted tokens', function () { beforeEach(async function () { - await this.token.$_mint(owner, firstTokenId); - await this.token.$_mint(owner, secondTokenId); + await this.token.$_mint(this.owner, firstTokenId); + await this.token.$_mint(this.owner, secondTokenId); }); - context('with burnt token', function () { + describe('with burnt token', function () { beforeEach(async function () { - this.receipt = await this.token.$_burn(firstTokenId); + await this.token.$_burn(firstTokenId); }); it('removes that token from the token list of the owner', async function () { - expect(await this.token.tokenOfOwnerByIndex(owner, 0)).to.be.bignumber.equal(secondTokenId); + expect(await this.token.tokenOfOwnerByIndex(this.owner, 0n)).to.equal(secondTokenId); }); it('adjusts all tokens list', async function () { - expect(await this.token.tokenByIndex(0)).to.be.bignumber.equal(secondTokenId); + expect(await this.token.tokenByIndex(0n)).to.equal(secondTokenId); }); it('burns all tokens', async function () { - await this.token.$_burn(secondTokenId, { from: owner }); - expect(await this.token.totalSupply()).to.be.bignumber.equal('0'); - await expectRevertCustomError(this.token.tokenByIndex(0), 'ERC721OutOfBoundsIndex', [ZERO_ADDRESS, 0]); + await this.token.$_burn(secondTokenId); + expect(await this.token.totalSupply()).to.equal(0n); + + await expect(this.token.tokenByIndex(0n)) + .to.be.revertedWithCustomError(this.token, 'ERC721OutOfBoundsIndex') + .withArgs(ethers.ZeroAddress, 0n); }); }); }); }); } -function shouldBehaveLikeERC721Metadata(name, symbol, owner) { +function shouldBehaveLikeERC721Metadata(name, symbol) { shouldSupportInterfaces(['ERC721Metadata']); describe('metadata', function () { it('has a name', async function () { - expect(await this.token.name()).to.be.equal(name); + expect(await this.token.name()).to.equal(name); }); it('has a symbol', async function () { - expect(await this.token.symbol()).to.be.equal(symbol); + expect(await this.token.symbol()).to.equal(symbol); }); describe('token URI', function () { beforeEach(async function () { - await this.token.$_mint(owner, firstTokenId); + await this.token.$_mint(this.owner, firstTokenId); }); it('return empty string by default', async function () { - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(''); + expect(await this.token.tokenURI(firstTokenId)).to.equal(''); }); it('reverts when queried for non existent token id', async function () { - await expectRevertCustomError(this.token.tokenURI(nonExistentTokenId), 'ERC721NonexistentToken', [ - nonExistentTokenId, - ]); + await expect(this.token.tokenURI(nonExistentTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(nonExistentTokenId); }); describe('base URI', function () { beforeEach(function () { - if (this.token.setBaseURI === undefined) { + if (!this.token.interface.hasFunction('setBaseURI')) { this.skip(); } }); @@ -957,14 +963,14 @@ function shouldBehaveLikeERC721Metadata(name, symbol, owner) { it('base URI is added as a prefix to the token URI', async function () { await this.token.setBaseURI(baseURI); - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(baseURI + firstTokenId.toString()); + expect(await this.token.tokenURI(firstTokenId)).to.equal(baseURI + firstTokenId.toString()); }); it('token URI can be changed by changing the base URI', async function () { await this.token.setBaseURI(baseURI); const newBaseURI = 'https://api.example.com/v2/'; await this.token.setBaseURI(newBaseURI); - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(newBaseURI + firstTokenId.toString()); + expect(await this.token.tokenURI(firstTokenId)).to.equal(newBaseURI + firstTokenId.toString()); }); }); }); diff --git a/test/token/ERC721/ERC721.test.js b/test/token/ERC721/ERC721.test.js index 372dd5069d0..1454cb057c6 100644 --- a/test/token/ERC721/ERC721.test.js +++ b/test/token/ERC721/ERC721.test.js @@ -1,15 +1,23 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + const { shouldBehaveLikeERC721, shouldBehaveLikeERC721Metadata } = require('./ERC721.behavior'); -const ERC721 = artifacts.require('$ERC721'); +const name = 'Non Fungible Token'; +const symbol = 'NFT'; -contract('ERC721', function (accounts) { - const name = 'Non Fungible Token'; - const symbol = 'NFT'; +async function fixture() { + return { + accounts: await ethers.getSigners(), + token: await ethers.deployContract('$ERC721', [name, symbol]), + }; +} +describe('ERC721', function () { beforeEach(async function () { - this.token = await ERC721.new(name, symbol); + Object.assign(this, await loadFixture(fixture)); }); - shouldBehaveLikeERC721(...accounts); - shouldBehaveLikeERC721Metadata(name, symbol, ...accounts); + shouldBehaveLikeERC721(); + shouldBehaveLikeERC721Metadata(name, symbol); }); diff --git a/test/token/ERC721/ERC721Enumerable.test.js b/test/token/ERC721/ERC721Enumerable.test.js index 31c28d177b5..a3bdea73f5a 100644 --- a/test/token/ERC721/ERC721Enumerable.test.js +++ b/test/token/ERC721/ERC721Enumerable.test.js @@ -1,20 +1,28 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + const { shouldBehaveLikeERC721, shouldBehaveLikeERC721Metadata, shouldBehaveLikeERC721Enumerable, } = require('./ERC721.behavior'); -const ERC721Enumerable = artifacts.require('$ERC721Enumerable'); +const name = 'Non Fungible Token'; +const symbol = 'NFT'; -contract('ERC721Enumerable', function (accounts) { - const name = 'Non Fungible Token'; - const symbol = 'NFT'; +async function fixture() { + return { + accounts: await ethers.getSigners(), + token: await ethers.deployContract('$ERC721Enumerable', [name, symbol]), + }; +} +describe('ERC721', function () { beforeEach(async function () { - this.token = await ERC721Enumerable.new(name, symbol); + Object.assign(this, await loadFixture(fixture)); }); - shouldBehaveLikeERC721(...accounts); - shouldBehaveLikeERC721Metadata(name, symbol, ...accounts); - shouldBehaveLikeERC721Enumerable(...accounts); + shouldBehaveLikeERC721(); + shouldBehaveLikeERC721Metadata(name, symbol); + shouldBehaveLikeERC721Enumerable(); }); diff --git a/test/token/ERC721/extensions/ERC721Burnable.test.js b/test/token/ERC721/extensions/ERC721Burnable.test.js index df059e09078..0a5e838fec8 100644 --- a/test/token/ERC721/extensions/ERC721Burnable.test.js +++ b/test/token/ERC721/extensions/ERC721Burnable.test.js @@ -1,80 +1,75 @@ -const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { expectRevertCustomError } = require('../../../helpers/customError'); - -const ERC721Burnable = artifacts.require('$ERC721Burnable'); - -contract('ERC721Burnable', function (accounts) { - const [owner, approved, another] = accounts; +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); - const firstTokenId = new BN(1); - const secondTokenId = new BN(2); - const unknownTokenId = new BN(3); +const name = 'Non Fungible Token'; +const symbol = 'NFT'; +const tokenId = 1n; +const otherTokenId = 2n; +const unknownTokenId = 3n; - const name = 'Non Fungible Token'; - const symbol = 'NFT'; +async function fixture() { + const [owner, approved, another] = await ethers.getSigners(); + const token = await ethers.deployContract('$ERC721Burnable', [name, symbol]); + return { owner, approved, another, token }; +} +describe('ERC721Burnable', function () { beforeEach(async function () { - this.token = await ERC721Burnable.new(name, symbol); + Object.assign(this, await loadFixture(fixture)); }); describe('like a burnable ERC721', function () { beforeEach(async function () { - await this.token.$_mint(owner, firstTokenId); - await this.token.$_mint(owner, secondTokenId); + await this.token.$_mint(this.owner, tokenId); + await this.token.$_mint(this.owner, otherTokenId); }); describe('burn', function () { - const tokenId = firstTokenId; - let receipt = null; - describe('when successful', function () { - beforeEach(async function () { - receipt = await this.token.burn(tokenId, { from: owner }); - }); + it('emits a burn event, burns the given token ID and adjusts the balance of the owner', async function () { + const balanceBefore = await this.token.balanceOf(this.owner); - it('burns the given token ID and adjusts the balance of the owner', async function () { - await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); - expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1'); - }); + await expect(this.token.connect(this.owner).burn(tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, tokenId); - it('emits a burn event', async function () { - expectEvent(receipt, 'Transfer', { - from: owner, - to: constants.ZERO_ADDRESS, - tokenId: tokenId, - }); + await expect(this.token.ownerOf(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); + + expect(await this.token.balanceOf(this.owner)).to.equal(balanceBefore - 1n); }); }); describe('when there is a previous approval burned', function () { beforeEach(async function () { - await this.token.approve(approved, tokenId, { from: owner }); - receipt = await this.token.burn(tokenId, { from: owner }); + await this.token.connect(this.owner).approve(this.approved, tokenId); + await this.token.connect(this.owner).burn(tokenId); }); context('getApproved', function () { it('reverts', async function () { - await expectRevertCustomError(this.token.getApproved(tokenId), 'ERC721NonexistentToken', [tokenId]); + await expect(this.token.getApproved(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); }); }); }); describe('when there is no previous approval burned', function () { it('reverts', async function () { - await expectRevertCustomError(this.token.burn(tokenId, { from: another }), 'ERC721InsufficientApproval', [ - another, - tokenId, - ]); + await expect(this.token.connect(this.another).burn(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InsufficientApproval') + .withArgs(this.another.address, tokenId); }); }); describe('when the given token ID was not tracked by this contract', function () { it('reverts', async function () { - await expectRevertCustomError(this.token.burn(unknownTokenId, { from: owner }), 'ERC721NonexistentToken', [ - unknownTokenId, - ]); + await expect(this.token.connect(this.owner).burn(unknownTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(unknownTokenId); }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Consecutive.test.js b/test/token/ERC721/extensions/ERC721Consecutive.test.js index e4ee3196d44..b83e2feb4cf 100644 --- a/test/token/ERC721/extensions/ERC721Consecutive.test.js +++ b/test/token/ERC721/extensions/ERC721Consecutive.test.js @@ -1,55 +1,60 @@ -const { constants, expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + const { sum } = require('../../../helpers/math'); -const { expectRevertCustomError } = require('../../../helpers/customError'); -const { ZERO_ADDRESS } = require('@openzeppelin/test-helpers/src/constants'); - -const ERC721ConsecutiveMock = artifacts.require('$ERC721ConsecutiveMock'); -const ERC721ConsecutiveEnumerableMock = artifacts.require('$ERC721ConsecutiveEnumerableMock'); -const ERC721ConsecutiveNoConstructorMintMock = artifacts.require('$ERC721ConsecutiveNoConstructorMintMock'); - -contract('ERC721Consecutive', function (accounts) { - const [user1, user2, user3, receiver] = accounts; - - const name = 'Non Fungible Token'; - const symbol = 'NFT'; - const batches = [ - { receiver: user1, amount: 0 }, - { receiver: user1, amount: 1 }, - { receiver: user1, amount: 2 }, - { receiver: user2, amount: 5 }, - { receiver: user3, amount: 0 }, - { receiver: user1, amount: 7 }, - ]; - const delegates = [user1, user3]; - - for (const offset of [0, 1, 42]) { + +const name = 'Non Fungible Token'; +const symbol = 'NFT'; + +describe('ERC721Consecutive', function () { + for (const offset of [0n, 1n, 42n]) { describe(`with offset ${offset}`, function () { - beforeEach(async function () { - this.token = await ERC721ConsecutiveMock.new( + async function fixture() { + const accounts = await ethers.getSigners(); + const [alice, bruce, chris, receiver] = accounts; + + const batches = [ + { receiver: alice, amount: 0n }, + { receiver: alice, amount: 1n }, + { receiver: alice, amount: 2n }, + { receiver: bruce, amount: 5n }, + { receiver: chris, amount: 0n }, + { receiver: alice, amount: 7n }, + ]; + const delegates = [alice, chris]; + + const token = await ethers.deployContract('$ERC721ConsecutiveMock', [ name, symbol, offset, delegates, batches.map(({ receiver }) => receiver), batches.map(({ amount }) => amount), - ); + ]); + + return { accounts, alice, bruce, chris, receiver, batches, delegates, token }; + } + + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); }); describe('minting during construction', function () { it('events are emitted at construction', async function () { let first = offset; - - for (const batch of batches) { + for (const batch of this.batches) { if (batch.amount > 0) { - await expectEvent.inConstruction(this.token, 'ConsecutiveTransfer', { - fromTokenId: web3.utils.toBN(first), - toTokenId: web3.utils.toBN(first + batch.amount - 1), - fromAddress: constants.ZERO_ADDRESS, - toAddress: batch.receiver, - }); + await expect(this.token.deploymentTransaction()) + .to.emit(this.token, 'ConsecutiveTransfer') + .withArgs( + first /* fromTokenId */, + first + batch.amount - 1n /* toTokenId */, + ethers.ZeroAddress /* fromAddress */, + batch.receiver.address /* toAddress */, + ); } else { - // expectEvent.notEmitted.inConstruction only looks at event name, and doesn't check the parameters + // ".to.not.emit" only looks at event name, and doesn't check the parameters } first += batch.amount; } @@ -57,166 +62,175 @@ contract('ERC721Consecutive', function (accounts) { it('ownership is set', async function () { const owners = [ - ...Array(offset).fill(constants.ZERO_ADDRESS), - ...batches.flatMap(({ receiver, amount }) => Array(amount).fill(receiver)), + ...Array(Number(offset)).fill(ethers.ZeroAddress), + ...this.batches.flatMap(({ receiver, amount }) => Array(Number(amount)).fill(receiver.address)), ]; for (const tokenId in owners) { - if (owners[tokenId] != constants.ZERO_ADDRESS) { - expect(await this.token.ownerOf(tokenId)).to.be.equal(owners[tokenId]); + if (owners[tokenId] != ethers.ZeroAddress) { + expect(await this.token.ownerOf(tokenId)).to.equal(owners[tokenId]); } } }); it('balance & voting power are set', async function () { - for (const account of accounts) { + for (const account of this.accounts) { const balance = - sum(...batches.filter(({ receiver }) => receiver === account).map(({ amount }) => amount)) ?? 0; + sum(...this.batches.filter(({ receiver }) => receiver === account).map(({ amount }) => amount)) ?? 0n; - expect(await this.token.balanceOf(account)).to.be.bignumber.equal(web3.utils.toBN(balance)); + expect(await this.token.balanceOf(account)).to.equal(balance); // If not delegated at construction, check before + do delegation - if (!delegates.includes(account)) { - expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(0)); + if (!this.delegates.includes(account)) { + expect(await this.token.getVotes(account)).to.equal(0n); - await this.token.delegate(account, { from: account }); + await this.token.connect(account).delegate(account); } // At this point all accounts should have delegated - expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(balance)); + expect(await this.token.getVotes(account)).to.equal(balance); } }); it('reverts on consecutive minting to the zero address', async function () { - await expectRevertCustomError( - ERC721ConsecutiveMock.new(name, symbol, offset, delegates, [ZERO_ADDRESS], [10]), - 'ERC721InvalidReceiver', - [ZERO_ADDRESS], - ); + await expect( + ethers.deployContract('$ERC721ConsecutiveMock', [ + name, + symbol, + offset, + this.delegates, + [ethers.ZeroAddress], + [10], + ]), + ) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidReceiver') + .withArgs(ethers.ZeroAddress); }); }); describe('minting after construction', function () { it('consecutive minting is not possible after construction', async function () { - await expectRevertCustomError(this.token.$_mintConsecutive(user1, 10), 'ERC721ForbiddenBatchMint', []); + await expect(this.token.$_mintConsecutive(this.alice, 10)).to.be.revertedWithCustomError( + this.token, + 'ERC721ForbiddenBatchMint', + ); }); it('simple minting is possible after construction', async function () { - const tokenId = sum(...batches.map(b => b.amount)) + offset; + const tokenId = sum(...this.batches.map(b => b.amount)) + offset; - await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); + await expect(this.token.ownerOf(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); - expectEvent(await this.token.$_mint(user1, tokenId), 'Transfer', { - from: constants.ZERO_ADDRESS, - to: user1, - tokenId: tokenId.toString(), - }); + await expect(this.token.$_mint(this.alice, tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.alice.address, tokenId); }); it('cannot mint a token that has been batched minted', async function () { - const tokenId = sum(...batches.map(b => b.amount)) + offset - 1; + const tokenId = sum(...this.batches.map(b => b.amount)) + offset - 1n; - expect(await this.token.ownerOf(tokenId)).to.be.not.equal(constants.ZERO_ADDRESS); + expect(await this.token.ownerOf(tokenId)).to.not.equal(ethers.ZeroAddress); - await expectRevertCustomError(this.token.$_mint(user1, tokenId), 'ERC721InvalidSender', [ZERO_ADDRESS]); + await expect(this.token.$_mint(this.alice, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721InvalidSender') + .withArgs(ethers.ZeroAddress); }); }); describe('ERC721 behavior', function () { - const tokenId = web3.utils.toBN(offset + 1); + const tokenId = offset + 1n; it('core takes over ownership on transfer', async function () { - await this.token.transferFrom(user1, receiver, tokenId, { from: user1 }); + await this.token.connect(this.alice).transferFrom(this.alice, this.receiver, tokenId); - expect(await this.token.ownerOf(tokenId)).to.be.equal(receiver); + expect(await this.token.ownerOf(tokenId)).to.equal(this.receiver.address); }); it('tokens can be burned and re-minted #1', async function () { - expectEvent(await this.token.$_burn(tokenId, { from: user1 }), 'Transfer', { - from: user1, - to: constants.ZERO_ADDRESS, - tokenId, - }); + await expect(this.token.connect(this.alice).$_burn(tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(this.alice.address, ethers.ZeroAddress, tokenId); - await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); + await expect(this.token.ownerOf(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); - expectEvent(await this.token.$_mint(user2, tokenId), 'Transfer', { - from: constants.ZERO_ADDRESS, - to: user2, - tokenId, - }); + await expect(this.token.$_mint(this.bruce, tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.bruce.address, tokenId); - expect(await this.token.ownerOf(tokenId)).to.be.equal(user2); + expect(await this.token.ownerOf(tokenId)).to.equal(this.bruce.address); }); it('tokens can be burned and re-minted #2', async function () { - const tokenId = web3.utils.toBN(sum(...batches.map(({ amount }) => amount)) + offset); + const tokenId = sum(...this.batches.map(({ amount }) => amount)) + offset; - await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); + await expect(this.token.ownerOf(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); // mint - await this.token.$_mint(user1, tokenId); + await expect(this.token.$_mint(this.alice, tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.alice.address, tokenId); - expect(await this.token.ownerOf(tokenId), user1); + expect(await this.token.ownerOf(tokenId)).to.equal(this.alice.address); // burn - expectEvent(await this.token.$_burn(tokenId, { from: user1 }), 'Transfer', { - from: user1, - to: constants.ZERO_ADDRESS, - tokenId, - }); + await expect(await this.token.$_burn(tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(this.alice.address, ethers.ZeroAddress, tokenId); - await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); + await expect(this.token.ownerOf(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); // re-mint - expectEvent(await this.token.$_mint(user2, tokenId), 'Transfer', { - from: constants.ZERO_ADDRESS, - to: user2, - tokenId, - }); + await expect(this.token.$_mint(this.bruce, tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.bruce.address, tokenId); - expect(await this.token.ownerOf(tokenId), user2); + expect(await this.token.ownerOf(tokenId)).to.equal(this.bruce.address); }); }); }); } describe('invalid use', function () { + const receiver = ethers.Wallet.createRandom(); + it('cannot mint a batch larger than 5000', async function () { - await expectRevertCustomError( - ERC721ConsecutiveMock.new(name, symbol, 0, [], [user1], ['5001']), - 'ERC721ExceededMaxBatchMint', - [5000, 5001], - ); + const { interface } = await ethers.getContractFactory('$ERC721ConsecutiveMock'); + + await expect(ethers.deployContract('$ERC721ConsecutiveMock', [name, symbol, 0, [], [receiver], [5001n]])) + .to.be.revertedWithCustomError({ interface }, 'ERC721ExceededMaxBatchMint') + .withArgs(5001n, 5000n); }); it('cannot use single minting during construction', async function () { - await expectRevertCustomError( - ERC721ConsecutiveNoConstructorMintMock.new(name, symbol), - 'ERC721ForbiddenMint', - [], - ); + const { interface } = await ethers.getContractFactory('$ERC721ConsecutiveNoConstructorMintMock'); + + await expect( + ethers.deployContract('$ERC721ConsecutiveNoConstructorMintMock', [name, symbol]), + ).to.be.revertedWithCustomError({ interface }, 'ERC721ForbiddenMint'); }); it('cannot use single minting during construction', async function () { - await expectRevertCustomError( - ERC721ConsecutiveNoConstructorMintMock.new(name, symbol), - 'ERC721ForbiddenMint', - [], - ); + const { interface } = await ethers.getContractFactory('$ERC721ConsecutiveNoConstructorMintMock'); + + await expect( + ethers.deployContract('$ERC721ConsecutiveNoConstructorMintMock', [name, symbol]), + ).to.be.revertedWithCustomError({ interface }, 'ERC721ForbiddenMint'); }); it('consecutive mint not compatible with enumerability', async function () { - await expectRevertCustomError( - ERC721ConsecutiveEnumerableMock.new( - name, - symbol, - batches.map(({ receiver }) => receiver), - batches.map(({ amount }) => amount), - ), - 'ERC721EnumerableForbiddenBatchMint', - [], - ); + const { interface } = await ethers.getContractFactory('$ERC721ConsecutiveEnumerableMock'); + + await expect( + ethers.deployContract('$ERC721ConsecutiveEnumerableMock', [name, symbol, [receiver], [100n]]), + ).to.be.revertedWithCustomError({ interface }, 'ERC721EnumerableForbiddenBatchMint'); }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Pausable.test.js b/test/token/ERC721/extensions/ERC721Pausable.test.js index 5d77149f2b3..2be0c902dfe 100644 --- a/test/token/ERC721/extensions/ERC721Pausable.test.js +++ b/test/token/ERC721/extensions/ERC721Pausable.test.js @@ -1,89 +1,80 @@ -const { BN, constants } = require('@openzeppelin/test-helpers'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { expectRevertCustomError } = require('../../../helpers/customError'); - -const ERC721Pausable = artifacts.require('$ERC721Pausable'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -contract('ERC721Pausable', function (accounts) { - const [owner, receiver, operator] = accounts; +const name = 'Non Fungible Token'; +const symbol = 'NFT'; +const tokenId = 1n; +const otherTokenId = 2n; +const data = ethers.Typed.bytes('0x42'); - const name = 'Non Fungible Token'; - const symbol = 'NFT'; +async function fixture() { + const [owner, receiver, operator] = await ethers.getSigners(); + const token = await ethers.deployContract('$ERC721Pausable', [name, symbol]); + return { owner, receiver, operator, token }; +} +describe('ERC721Pausable', function () { beforeEach(async function () { - this.token = await ERC721Pausable.new(name, symbol); + Object.assign(this, await loadFixture(fixture)); }); context('when token is paused', function () { - const firstTokenId = new BN(1); - const secondTokenId = new BN(1337); - - const mockData = '0x42'; - beforeEach(async function () { - await this.token.$_mint(owner, firstTokenId, { from: owner }); + await this.token.$_mint(this.owner, tokenId); await this.token.$_pause(); }); it('reverts when trying to transferFrom', async function () { - await expectRevertCustomError( - this.token.transferFrom(owner, receiver, firstTokenId, { from: owner }), - 'EnforcedPause', - [], - ); + await expect( + this.token.connect(this.owner).transferFrom(this.owner, this.receiver, tokenId), + ).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); it('reverts when trying to safeTransferFrom', async function () { - await expectRevertCustomError( - this.token.safeTransferFrom(owner, receiver, firstTokenId, { from: owner }), - 'EnforcedPause', - [], - ); + await expect( + this.token.connect(this.owner).safeTransferFrom(this.owner, this.receiver, tokenId), + ).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); it('reverts when trying to safeTransferFrom with data', async function () { - await expectRevertCustomError( - this.token.methods['safeTransferFrom(address,address,uint256,bytes)'](owner, receiver, firstTokenId, mockData, { - from: owner, - }), - 'EnforcedPause', - [], - ); + await expect( + this.token.connect(this.owner).safeTransferFrom(this.owner, this.receiver, tokenId, data), + ).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); it('reverts when trying to mint', async function () { - await expectRevertCustomError(this.token.$_mint(receiver, secondTokenId), 'EnforcedPause', []); + await expect(this.token.$_mint(this.receiver, otherTokenId)).to.be.revertedWithCustomError( + this.token, + 'EnforcedPause', + ); }); it('reverts when trying to burn', async function () { - await expectRevertCustomError(this.token.$_burn(firstTokenId), 'EnforcedPause', []); + await expect(this.token.$_burn(tokenId)).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); describe('getApproved', function () { it('returns approved address', async function () { - const approvedAccount = await this.token.getApproved(firstTokenId); - expect(approvedAccount).to.equal(constants.ZERO_ADDRESS); + expect(await this.token.getApproved(tokenId)).to.equal(ethers.ZeroAddress); }); }); describe('balanceOf', function () { it('returns the amount of tokens owned by the given address', async function () { - const balance = await this.token.balanceOf(owner); - expect(balance).to.be.bignumber.equal('1'); + expect(await this.token.balanceOf(this.owner)).to.equal(1n); }); }); describe('ownerOf', function () { it('returns the amount of tokens owned by the given address', async function () { - const ownerOfToken = await this.token.ownerOf(firstTokenId); - expect(ownerOfToken).to.equal(owner); + expect(await this.token.ownerOf(tokenId)).to.equal(this.owner.address); }); }); describe('isApprovedForAll', function () { it('returns the approval of the operator', async function () { - expect(await this.token.isApprovedForAll(owner, operator)).to.equal(false); + expect(await this.token.isApprovedForAll(this.owner, this.operator)).to.be.false; }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Royalty.test.js b/test/token/ERC721/extensions/ERC721Royalty.test.js index 78cba9858c6..e11954ae7a3 100644 --- a/test/token/ERC721/extensions/ERC721Royalty.test.js +++ b/test/token/ERC721/extensions/ERC721Royalty.test.js @@ -1,45 +1,55 @@ -require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldBehaveLikeERC2981 } = require('../../common/ERC2981.behavior'); -const ERC721Royalty = artifacts.require('$ERC721Royalty'); +const name = 'Non Fungible Token'; +const symbol = 'NFT'; -contract('ERC721Royalty', function (accounts) { - const [account1, account2, recipient] = accounts; - const tokenId1 = web3.utils.toBN('1'); - const tokenId2 = web3.utils.toBN('2'); - const royalty = web3.utils.toBN('200'); - const salePrice = web3.utils.toBN('1000'); +const tokenId1 = 1n; +const tokenId2 = 2n; +const royalty = 200n; +const salePrice = 1000n; +async function fixture() { + const [account1, account2, recipient] = await ethers.getSigners(); + + const token = await ethers.deployContract('$ERC721Royalty', [name, symbol]); + await token.$_mint(account1, tokenId1); + await token.$_mint(account1, tokenId2); + + return { account1, account2, recipient, token }; +} + +describe('ERC721Royalty', function () { beforeEach(async function () { - this.token = await ERC721Royalty.new('My Token', 'TKN'); - - await this.token.$_mint(account1, tokenId1); - await this.token.$_mint(account1, tokenId2); - this.account1 = account1; - this.account2 = account2; - this.tokenId1 = tokenId1; - this.tokenId2 = tokenId2; - this.salePrice = salePrice; + Object.assign( + this, + await loadFixture(fixture), + { tokenId1, tokenId2, royalty, salePrice }, // set for behavior tests + ); }); describe('token specific functions', function () { beforeEach(async function () { - await this.token.$_setTokenRoyalty(tokenId1, recipient, royalty); + await this.token.$_setTokenRoyalty(tokenId1, this.recipient, royalty); }); it('royalty information are kept during burn and re-mint', async function () { await this.token.$_burn(tokenId1); - const tokenInfoA = await this.token.royaltyInfo(tokenId1, salePrice); - expect(tokenInfoA[0]).to.be.equal(recipient); - expect(tokenInfoA[1]).to.be.bignumber.equal(salePrice.mul(royalty).divn(1e4)); + expect(await this.token.royaltyInfo(tokenId1, salePrice)).to.deep.equal([ + this.recipient.address, + (salePrice * royalty) / 10000n, + ]); - await this.token.$_mint(account2, tokenId1); + await this.token.$_mint(this.account2, tokenId1); - const tokenInfoB = await this.token.royaltyInfo(tokenId1, salePrice); - expect(tokenInfoB[0]).to.be.equal(recipient); - expect(tokenInfoB[1]).to.be.bignumber.equal(salePrice.mul(royalty).divn(1e4)); + expect(await this.token.royaltyInfo(tokenId1, salePrice)).to.deep.equal([ + this.recipient.address, + (salePrice * royalty) / 10000n, + ]); }); }); diff --git a/test/token/ERC721/extensions/ERC721URIStorage.test.js b/test/token/ERC721/extensions/ERC721URIStorage.test.js index 8c882fab0a6..3a74f55ca4f 100644 --- a/test/token/ERC721/extensions/ERC721URIStorage.test.js +++ b/test/token/ERC721/extensions/ERC721URIStorage.test.js @@ -1,63 +1,64 @@ -const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior'); -const { expectRevertCustomError } = require('../../../helpers/customError'); - -const ERC721URIStorageMock = artifacts.require('$ERC721URIStorageMock'); - -contract('ERC721URIStorage', function (accounts) { - const [owner] = accounts; - - const name = 'Non Fungible Token'; - const symbol = 'NFT'; - - const firstTokenId = new BN('5042'); - const nonExistentTokenId = new BN('13'); +const name = 'Non Fungible Token'; +const symbol = 'NFT'; +const baseURI = 'https://api.example.com/v1/'; +const otherBaseURI = 'https://api.example.com/v2/'; +const sampleUri = 'mock://mytoken'; +const tokenId = 1n; +const nonExistentTokenId = 2n; + +async function fixture() { + const [owner] = await ethers.getSigners(); + const token = await ethers.deployContract('$ERC721URIStorageMock', [name, symbol]); + return { owner, token }; +} + +contract('ERC721URIStorage', function () { beforeEach(async function () { - this.token = await ERC721URIStorageMock.new(name, symbol); + Object.assign(this, await loadFixture(fixture)); }); shouldSupportInterfaces(['0x49064906']); describe('token URI', function () { beforeEach(async function () { - await this.token.$_mint(owner, firstTokenId); + await this.token.$_mint(this.owner, tokenId); }); - const baseURI = 'https://api.example.com/v1/'; - const sampleUri = 'mock://mytoken'; - it('it is empty by default', async function () { - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(''); + expect(await this.token.tokenURI(tokenId)).to.equal(''); }); it('reverts when queried for non existent token id', async function () { - await expectRevertCustomError(this.token.tokenURI(nonExistentTokenId), 'ERC721NonexistentToken', [ - nonExistentTokenId, - ]); + await expect(this.token.tokenURI(nonExistentTokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(nonExistentTokenId); }); it('can be set for a token id', async function () { - await this.token.$_setTokenURI(firstTokenId, sampleUri); - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(sampleUri); + await this.token.$_setTokenURI(tokenId, sampleUri); + expect(await this.token.tokenURI(tokenId)).to.equal(sampleUri); }); it('setting the uri emits an event', async function () { - expectEvent(await this.token.$_setTokenURI(firstTokenId, sampleUri), 'MetadataUpdate', { - _tokenId: firstTokenId, - }); + await expect(this.token.$_setTokenURI(tokenId, sampleUri)) + .to.emit(this.token, 'MetadataUpdate') + .withArgs(tokenId); }); it('setting the uri for non existent token id is allowed', async function () { - expectEvent(await this.token.$_setTokenURI(nonExistentTokenId, sampleUri), 'MetadataUpdate', { - _tokenId: nonExistentTokenId, - }); + await expect(await this.token.$_setTokenURI(nonExistentTokenId, sampleUri)) + .to.emit(this.token, 'MetadataUpdate') + .withArgs(nonExistentTokenId); // value will be accessible after mint - await this.token.$_mint(owner, nonExistentTokenId); - expect(await this.token.tokenURI(nonExistentTokenId)).to.be.equal(sampleUri); + await this.token.$_mint(this.owner, nonExistentTokenId); + expect(await this.token.tokenURI(nonExistentTokenId)).to.equal(sampleUri); }); it('base URI can be set', async function () { @@ -67,48 +68,54 @@ contract('ERC721URIStorage', function (accounts) { it('base URI is added as a prefix to the token URI', async function () { await this.token.setBaseURI(baseURI); - await this.token.$_setTokenURI(firstTokenId, sampleUri); + await this.token.$_setTokenURI(tokenId, sampleUri); - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(baseURI + sampleUri); + expect(await this.token.tokenURI(tokenId)).to.equal(baseURI + sampleUri); }); it('token URI can be changed by changing the base URI', async function () { await this.token.setBaseURI(baseURI); - await this.token.$_setTokenURI(firstTokenId, sampleUri); + await this.token.$_setTokenURI(tokenId, sampleUri); - const newBaseURI = 'https://api.example.com/v2/'; - await this.token.setBaseURI(newBaseURI); - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(newBaseURI + sampleUri); + await this.token.setBaseURI(otherBaseURI); + expect(await this.token.tokenURI(tokenId)).to.equal(otherBaseURI + sampleUri); }); it('tokenId is appended to base URI for tokens with no URI', async function () { await this.token.setBaseURI(baseURI); - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(baseURI + firstTokenId); + expect(await this.token.tokenURI(tokenId)).to.equal(baseURI + tokenId); }); it('tokens without URI can be burnt ', async function () { - await this.token.$_burn(firstTokenId); + await this.token.$_burn(tokenId); - await expectRevertCustomError(this.token.tokenURI(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); + await expect(this.token.tokenURI(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); }); it('tokens with URI can be burnt ', async function () { - await this.token.$_setTokenURI(firstTokenId, sampleUri); + await this.token.$_setTokenURI(tokenId, sampleUri); - await this.token.$_burn(firstTokenId); + await this.token.$_burn(tokenId); - await expectRevertCustomError(this.token.tokenURI(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); + await expect(this.token.tokenURI(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); }); it('tokens URI is kept if token is burnt and reminted ', async function () { - await this.token.$_setTokenURI(firstTokenId, sampleUri); + await this.token.$_setTokenURI(tokenId, sampleUri); + + await this.token.$_burn(tokenId); - await this.token.$_burn(firstTokenId); - await expectRevertCustomError(this.token.tokenURI(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); + await expect(this.token.tokenURI(tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') + .withArgs(tokenId); - await this.token.$_mint(owner, firstTokenId); - expect(await this.token.tokenURI(firstTokenId)).to.be.equal(sampleUri); + await this.token.$_mint(this.owner, tokenId); + expect(await this.token.tokenURI(tokenId)).to.equal(sampleUri); }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Wrapper.test.js b/test/token/ERC721/extensions/ERC721Wrapper.test.js index 6839977449d..2c093a08723 100644 --- a/test/token/ERC721/extensions/ERC721Wrapper.test.js +++ b/test/token/ERC721/extensions/ERC721Wrapper.test.js @@ -1,26 +1,29 @@ -const { BN, expectEvent, constants } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldBehaveLikeERC721 } = require('../ERC721.behavior'); -const { expectRevertCustomError } = require('../../../helpers/customError'); -const ERC721 = artifacts.require('$ERC721'); -const ERC721Wrapper = artifacts.require('$ERC721Wrapper'); +const name = 'Non Fungible Token'; +const symbol = 'NFT'; +const tokenId = 1n; +const otherTokenId = 2n; -contract('ERC721Wrapper', function (accounts) { - const [initialHolder, anotherAccount, approvedAccount] = accounts; +async function fixture() { + const accounts = await ethers.getSigners(); + const [owner, approved, other] = accounts; - const name = 'My Token'; - const symbol = 'MTKN'; - const firstTokenId = new BN(1); - const secondTokenId = new BN(2); + const underlying = await ethers.deployContract('$ERC721', [name, symbol]); + await underlying.$_safeMint(owner, tokenId); + await underlying.$_safeMint(owner, otherTokenId); + const token = await ethers.deployContract('$ERC721Wrapper', [`Wrapped ${name}`, `W${symbol}`, underlying]); - beforeEach(async function () { - this.underlying = await ERC721.new(name, symbol); - this.token = await ERC721Wrapper.new(`Wrapped ${name}`, `W${symbol}`, this.underlying.address); + return { accounts, owner, approved, other, underlying, token }; +} - await this.underlying.$_safeMint(initialHolder, firstTokenId); - await this.underlying.$_safeMint(initialHolder, secondTokenId); +describe('ERC721Wrapper', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); }); it('has a name', async function () { @@ -32,258 +35,167 @@ contract('ERC721Wrapper', function (accounts) { }); it('has underlying', async function () { - expect(await this.token.underlying()).to.be.bignumber.equal(this.underlying.address); + expect(await this.token.underlying()).to.equal(this.underlying.target); }); describe('depositFor', function () { it('works with token approval', async function () { - await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder }); - - const { tx } = await this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: initialHolder, - to: this.token.address, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: initialHolder, - tokenId: firstTokenId, - }); + await this.underlying.connect(this.owner).approve(this.token, tokenId); + + await expect(this.token.connect(this.owner).depositFor(this.owner, [tokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.owner.address, this.token.target, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.owner.address, tokenId); }); it('works with approval for all', async function () { - await this.underlying.setApprovalForAll(this.token.address, true, { from: initialHolder }); - - const { tx } = await this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: initialHolder, - to: this.token.address, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: initialHolder, - tokenId: firstTokenId, - }); + await this.underlying.connect(this.owner).setApprovalForAll(this.token, true); + + await expect(this.token.connect(this.owner).depositFor(this.owner, [tokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.owner.address, this.token.target, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.owner.address, tokenId); }); it('works sending to another account', async function () { - await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder }); - - const { tx } = await this.token.depositFor(anotherAccount, [firstTokenId], { from: initialHolder }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: initialHolder, - to: this.token.address, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: anotherAccount, - tokenId: firstTokenId, - }); + await this.underlying.connect(this.owner).approve(this.token, tokenId); + + await expect(this.token.connect(this.owner).depositFor(this.other, [tokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.owner.address, this.token.target, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.other.address, tokenId); }); it('works with multiple tokens', async function () { - await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder }); - await this.underlying.approve(this.token.address, secondTokenId, { from: initialHolder }); - - const { tx } = await this.token.depositFor(initialHolder, [firstTokenId, secondTokenId], { from: initialHolder }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: initialHolder, - to: this.token.address, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: initialHolder, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: initialHolder, - to: this.token.address, - tokenId: secondTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: initialHolder, - tokenId: secondTokenId, - }); + await this.underlying.connect(this.owner).approve(this.token, tokenId); + await this.underlying.connect(this.owner).approve(this.token, otherTokenId); + + await expect(this.token.connect(this.owner).depositFor(this.owner, [tokenId, otherTokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.owner.address, this.token.target, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.owner.address, tokenId) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.owner.address, this.token.target, otherTokenId) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.owner.address, otherTokenId); }); it('reverts with missing approval', async function () { - await expectRevertCustomError( - this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }), - 'ERC721InsufficientApproval', - [this.token.address, firstTokenId], - ); + await expect(this.token.connect(this.owner).depositFor(this.owner, [tokenId])) + .to.be.revertedWithCustomError(this.token, 'ERC721InsufficientApproval') + .withArgs(this.token.target, tokenId); }); }); describe('withdrawTo', function () { beforeEach(async function () { - await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder }); - await this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }); + await this.underlying.connect(this.owner).approve(this.token, tokenId); + await this.token.connect(this.owner).depositFor(this.owner, [tokenId]); }); it('works for an owner', async function () { - const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId], { from: initialHolder }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: this.token.address, - to: initialHolder, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: initialHolder, - to: constants.ZERO_ADDRESS, - tokenId: firstTokenId, - }); + await expect(this.token.connect(this.owner).withdrawTo(this.owner, [tokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.token.target, this.owner.address, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, tokenId); }); it('works for an approved', async function () { - await this.token.approve(approvedAccount, firstTokenId, { from: initialHolder }); - - const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId], { from: approvedAccount }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: this.token.address, - to: initialHolder, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: initialHolder, - to: constants.ZERO_ADDRESS, - tokenId: firstTokenId, - }); + await this.token.connect(this.owner).approve(this.approved, tokenId); + + await expect(this.token.connect(this.approved).withdrawTo(this.owner, [tokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.token.target, this.owner.address, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, tokenId); }); it('works for an approved for all', async function () { - await this.token.setApprovalForAll(approvedAccount, true, { from: initialHolder }); - - const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId], { from: approvedAccount }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: this.token.address, - to: initialHolder, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: initialHolder, - to: constants.ZERO_ADDRESS, - tokenId: firstTokenId, - }); + await this.token.connect(this.owner).setApprovalForAll(this.approved, true); + + await expect(this.token.connect(this.approved).withdrawTo(this.owner, [tokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.token.target, this.owner.address, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, tokenId); }); it("doesn't work for a non-owner nor approved", async function () { - await expectRevertCustomError( - this.token.withdrawTo(initialHolder, [firstTokenId], { from: anotherAccount }), - 'ERC721InsufficientApproval', - [anotherAccount, firstTokenId], - ); + await expect(this.token.connect(this.other).withdrawTo(this.owner, [tokenId])) + .to.be.revertedWithCustomError(this.token, 'ERC721InsufficientApproval') + .withArgs(this.other.address, tokenId); }); it('works with multiple tokens', async function () { - await this.underlying.approve(this.token.address, secondTokenId, { from: initialHolder }); - await this.token.depositFor(initialHolder, [secondTokenId], { from: initialHolder }); - - const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId, secondTokenId], { from: initialHolder }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: this.token.address, - to: initialHolder, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: this.token.address, - to: initialHolder, - tokenId: secondTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: initialHolder, - to: constants.ZERO_ADDRESS, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: initialHolder, - to: constants.ZERO_ADDRESS, - tokenId: secondTokenId, - }); + await this.underlying.connect(this.owner).approve(this.token, otherTokenId); + await this.token.connect(this.owner).depositFor(this.owner, [otherTokenId]); + + await expect(this.token.connect(this.owner).withdrawTo(this.owner, [tokenId, otherTokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.token.target, this.owner.address, tokenId) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.token.target, this.owner.address, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, tokenId); }); it('works to another account', async function () { - const { tx } = await this.token.withdrawTo(anotherAccount, [firstTokenId], { from: initialHolder }); - - await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { - from: this.token.address, - to: anotherAccount, - tokenId: firstTokenId, - }); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: initialHolder, - to: constants.ZERO_ADDRESS, - tokenId: firstTokenId, - }); + await expect(this.token.connect(this.owner).withdrawTo(this.other, [tokenId])) + .to.emit(this.underlying, 'Transfer') + .withArgs(this.token.target, this.other.address, tokenId) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, ethers.ZeroAddress, tokenId); }); }); describe('onERC721Received', function () { it('only allows calls from underlying', async function () { - await expectRevertCustomError( - this.token.onERC721Received( - initialHolder, - this.token.address, - firstTokenId, - anotherAccount, // Correct data - { from: anotherAccount }, + await expect( + this.token.connect(this.other).onERC721Received( + this.owner, + this.token, + tokenId, + this.other.address, // Correct data ), - 'ERC721UnsupportedToken', - [anotherAccount], - ); + ) + .to.be.revertedWithCustomError(this.token, 'ERC721UnsupportedToken') + .withArgs(this.other.address); }); it('mints a token to from', async function () { - const { tx } = await this.underlying.safeTransferFrom(initialHolder, this.token.address, firstTokenId, { - from: initialHolder, - }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: initialHolder, - tokenId: firstTokenId, - }); + await expect(this.underlying.connect(this.owner).safeTransferFrom(this.owner, this.token, tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.owner.address, tokenId); }); }); describe('_recover', function () { it('works if there is something to recover', async function () { // Should use `transferFrom` to avoid `onERC721Received` minting - await this.underlying.transferFrom(initialHolder, this.token.address, firstTokenId, { from: initialHolder }); + await this.underlying.connect(this.owner).transferFrom(this.owner, this.token, tokenId); - const { tx } = await this.token.$_recover(anotherAccount, firstTokenId); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: anotherAccount, - tokenId: firstTokenId, - }); + await expect(this.token.$_recover(this.other, tokenId)) + .to.emit(this.token, 'Transfer') + .withArgs(ethers.ZeroAddress, this.other.address, tokenId); }); it('reverts if there is nothing to recover', async function () { - const owner = await this.underlying.ownerOf(firstTokenId); - await expectRevertCustomError(this.token.$_recover(initialHolder, firstTokenId), 'ERC721IncorrectOwner', [ - this.token.address, - firstTokenId, - owner, - ]); + const holder = await this.underlying.ownerOf(tokenId); + + await expect(this.token.$_recover(holder, tokenId)) + .to.be.revertedWithCustomError(this.token, 'ERC721IncorrectOwner') + .withArgs(this.token.target, tokenId, holder); }); }); describe('ERC712 behavior', function () { - shouldBehaveLikeERC721(...accounts); + shouldBehaveLikeERC721(); }); }); diff --git a/test/token/ERC721/utils/ERC721Holder.test.js b/test/token/ERC721/utils/ERC721Holder.test.js index 4aa2b79484b..01b774930ab 100644 --- a/test/token/ERC721/utils/ERC721Holder.test.js +++ b/test/token/ERC721/utils/ERC721Holder.test.js @@ -1,22 +1,20 @@ +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const ERC721Holder = artifacts.require('$ERC721Holder'); -const ERC721 = artifacts.require('$ERC721'); - -contract('ERC721Holder', function (accounts) { - const [owner] = accounts; - - const name = 'Non Fungible Token'; - const symbol = 'NFT'; - const tokenId = web3.utils.toBN(1); +const name = 'Non Fungible Token'; +const symbol = 'NFT'; +const tokenId = 1n; +describe('ERC721Holder', function () { it('receives an ERC721 token', async function () { - const token = await ERC721.new(name, symbol); + const [owner] = await ethers.getSigners(); + + const token = await ethers.deployContract('$ERC721', [name, symbol]); await token.$_mint(owner, tokenId); - const receiver = await ERC721Holder.new(); - await token.safeTransferFrom(owner, receiver.address, tokenId, { from: owner }); + const receiver = await ethers.deployContract('$ERC721Holder'); + await token.connect(owner).safeTransferFrom(owner, receiver, tokenId); - expect(await token.ownerOf(tokenId)).to.be.equal(receiver.address); + expect(await token.ownerOf(tokenId)).to.equal(receiver.target); }); }); diff --git a/test/token/common/ERC2981.behavior.js b/test/token/common/ERC2981.behavior.js index 1c062b0524c..ae6abccaedb 100644 --- a/test/token/common/ERC2981.behavior.js +++ b/test/token/common/ERC2981.behavior.js @@ -1,12 +1,10 @@ -const { BN, constants } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { ZERO_ADDRESS } = constants; const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); -const { expectRevertCustomError } = require('../../helpers/customError'); function shouldBehaveLikeERC2981() { - const royaltyFraction = new BN('10'); + const royaltyFraction = 10n; shouldSupportInterfaces(['ERC2981']); @@ -16,64 +14,54 @@ function shouldBehaveLikeERC2981() { }); it('checks royalty is set', async function () { - const royalty = new BN((this.salePrice * royaltyFraction) / 10000); - - const initInfo = await this.token.royaltyInfo(this.tokenId1, this.salePrice); - - expect(initInfo[0]).to.be.equal(this.account1); - expect(initInfo[1]).to.be.bignumber.equal(royalty); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.deep.equal([ + this.account1.address, + (this.salePrice * royaltyFraction) / 10_000n, + ]); }); it('updates royalty amount', async function () { - const newPercentage = new BN('25'); + const newFraction = 25n; - // Updated royalty check - await this.token.$_setDefaultRoyalty(this.account1, newPercentage); - const royalty = new BN((this.salePrice * newPercentage) / 10000); - const newInfo = await this.token.royaltyInfo(this.tokenId1, this.salePrice); + await this.token.$_setDefaultRoyalty(this.account1, newFraction); - expect(newInfo[0]).to.be.equal(this.account1); - expect(newInfo[1]).to.be.bignumber.equal(royalty); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.deep.equal([ + this.account1.address, + (this.salePrice * newFraction) / 10_000n, + ]); }); it('holds same royalty value for different tokens', async function () { - const newPercentage = new BN('20'); - await this.token.$_setDefaultRoyalty(this.account1, newPercentage); + const newFraction = 20n; - const token1Info = await this.token.royaltyInfo(this.tokenId1, this.salePrice); - const token2Info = await this.token.royaltyInfo(this.tokenId2, this.salePrice); + await this.token.$_setDefaultRoyalty(this.account1, newFraction); - expect(token1Info[1]).to.be.bignumber.equal(token2Info[1]); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.deep.equal( + await this.token.royaltyInfo(this.tokenId2, this.salePrice), + ); }); it('Remove royalty information', async function () { - const newValue = new BN('0'); + const newValue = 0n; await this.token.$_deleteDefaultRoyalty(); - const token1Info = await this.token.royaltyInfo(this.tokenId1, this.salePrice); - const token2Info = await this.token.royaltyInfo(this.tokenId2, this.salePrice); - // Test royalty info is still persistent across all tokens - expect(token1Info[0]).to.be.bignumber.equal(token2Info[0]); - expect(token1Info[1]).to.be.bignumber.equal(token2Info[1]); - // Test information was deleted - expect(token1Info[0]).to.be.equal(ZERO_ADDRESS); - expect(token1Info[1]).to.be.bignumber.equal(newValue); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.deep.equal([ethers.ZeroAddress, newValue]); + + expect(await this.token.royaltyInfo(this.tokenId2, this.salePrice)).to.deep.equal([ethers.ZeroAddress, newValue]); }); it('reverts if invalid parameters', async function () { const royaltyDenominator = await this.token.$_feeDenominator(); - await expectRevertCustomError( - this.token.$_setDefaultRoyalty(ZERO_ADDRESS, royaltyFraction), - 'ERC2981InvalidDefaultRoyaltyReceiver', - [ZERO_ADDRESS], - ); - const anotherRoyaltyFraction = new BN('11000'); - await expectRevertCustomError( - this.token.$_setDefaultRoyalty(this.account1, anotherRoyaltyFraction), - 'ERC2981InvalidDefaultRoyalty', - [anotherRoyaltyFraction, royaltyDenominator], - ); + await expect(this.token.$_setDefaultRoyalty(ethers.ZeroAddress, royaltyFraction)) + .to.be.revertedWithCustomError(this.token, 'ERC2981InvalidDefaultRoyaltyReceiver') + .withArgs(ethers.ZeroAddress); + + const anotherRoyaltyFraction = 11000n; + + await expect(this.token.$_setDefaultRoyalty(this.account1, anotherRoyaltyFraction)) + .to.be.revertedWithCustomError(this.token, 'ERC2981InvalidDefaultRoyalty') + .withArgs(anotherRoyaltyFraction, royaltyDenominator); }); }); @@ -83,83 +71,78 @@ function shouldBehaveLikeERC2981() { }); it('updates royalty amount', async function () { - const newPercentage = new BN('25'); - let royalty = new BN((this.salePrice * royaltyFraction) / 10000); - // Initial royalty check - const initInfo = await this.token.royaltyInfo(this.tokenId1, this.salePrice); + const newFraction = 25n; - expect(initInfo[0]).to.be.equal(this.account1); - expect(initInfo[1]).to.be.bignumber.equal(royalty); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.deep.equal([ + this.account1.address, + (this.salePrice * royaltyFraction) / 10_000n, + ]); - // Updated royalty check - await this.token.$_setTokenRoyalty(this.tokenId1, this.account1, newPercentage); - royalty = new BN((this.salePrice * newPercentage) / 10000); - const newInfo = await this.token.royaltyInfo(this.tokenId1, this.salePrice); + await this.token.$_setTokenRoyalty(this.tokenId1, this.account1, newFraction); - expect(newInfo[0]).to.be.equal(this.account1); - expect(newInfo[1]).to.be.bignumber.equal(royalty); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.deep.equal([ + this.account1.address, + (this.salePrice * newFraction) / 10_000n, + ]); }); it('holds different values for different tokens', async function () { - const newPercentage = new BN('20'); - await this.token.$_setTokenRoyalty(this.tokenId2, this.account1, newPercentage); + const newFraction = 20n; - const token1Info = await this.token.royaltyInfo(this.tokenId1, this.salePrice); - const token2Info = await this.token.royaltyInfo(this.tokenId2, this.salePrice); + await this.token.$_setTokenRoyalty(this.tokenId2, this.account1, newFraction); - // must be different even at the same this.salePrice - expect(token1Info[1]).to.not.be.bignumber.equal(token2Info[1]); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.not.deep.equal( + await this.token.royaltyInfo(this.tokenId2, this.salePrice), + ); }); it('reverts if invalid parameters', async function () { const royaltyDenominator = await this.token.$_feeDenominator(); - await expectRevertCustomError( - this.token.$_setTokenRoyalty(this.tokenId1, ZERO_ADDRESS, royaltyFraction), - 'ERC2981InvalidTokenRoyaltyReceiver', - [this.tokenId1.toString(), ZERO_ADDRESS], - ); - const anotherRoyaltyFraction = new BN('11000'); - await expectRevertCustomError( - this.token.$_setTokenRoyalty(this.tokenId1, this.account1, anotherRoyaltyFraction), - 'ERC2981InvalidTokenRoyalty', - [this.tokenId1.toString(), anotherRoyaltyFraction, royaltyDenominator], - ); + await expect(this.token.$_setTokenRoyalty(this.tokenId1, ethers.ZeroAddress, royaltyFraction)) + .to.be.revertedWithCustomError(this.token, 'ERC2981InvalidTokenRoyaltyReceiver') + .withArgs(this.tokenId1, ethers.ZeroAddress); + + const anotherRoyaltyFraction = 11000n; + + await expect(this.token.$_setTokenRoyalty(this.tokenId1, this.account1, anotherRoyaltyFraction)) + .to.be.revertedWithCustomError(this.token, 'ERC2981InvalidTokenRoyalty') + .withArgs(this.tokenId1, anotherRoyaltyFraction, royaltyDenominator); }); it('can reset token after setting royalty', async function () { - const newPercentage = new BN('30'); - const royalty = new BN((this.salePrice * newPercentage) / 10000); - await this.token.$_setTokenRoyalty(this.tokenId1, this.account2, newPercentage); + const newFraction = 30n; - const tokenInfo = await this.token.royaltyInfo(this.tokenId1, this.salePrice); + await this.token.$_setTokenRoyalty(this.tokenId1, this.account2, newFraction); // Tokens must have own information - expect(tokenInfo[1]).to.be.bignumber.equal(royalty); - expect(tokenInfo[0]).to.be.equal(this.account2); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.deep.equal([ + this.account2.address, + (this.salePrice * newFraction) / 10_000n, + ]); + + await this.token.$_setTokenRoyalty(this.tokenId2, this.account1, 0n); - await this.token.$_setTokenRoyalty(this.tokenId2, this.account1, new BN('0')); - const result = await this.token.royaltyInfo(this.tokenId2, this.salePrice); // Token must not share default information - expect(result[0]).to.be.equal(this.account1); - expect(result[1]).to.be.bignumber.equal(new BN('0')); + expect(await this.token.royaltyInfo(this.tokenId2, this.salePrice)).to.deep.equal([this.account1.address, 0n]); }); it('can hold default and token royalty information', async function () { - const newPercentage = new BN('30'); - const royalty = new BN((this.salePrice * newPercentage) / 10000); + const newFraction = 30n; - await this.token.$_setTokenRoyalty(this.tokenId2, this.account2, newPercentage); + await this.token.$_setTokenRoyalty(this.tokenId2, this.account2, newFraction); - const token1Info = await this.token.royaltyInfo(this.tokenId1, this.salePrice); - const token2Info = await this.token.royaltyInfo(this.tokenId2, this.salePrice); // Tokens must not have same values - expect(token1Info[1]).to.not.be.bignumber.equal(token2Info[1]); - expect(token1Info[0]).to.not.be.equal(token2Info[0]); + expect(await this.token.royaltyInfo(this.tokenId1, this.salePrice)).to.not.deep.equal([ + this.account2.address, + (this.salePrice * newFraction) / 10_000n, + ]); // Updated token must have new values - expect(token2Info[0]).to.be.equal(this.account2); - expect(token2Info[1]).to.be.bignumber.equal(royalty); + expect(await this.token.royaltyInfo(this.tokenId2, this.salePrice)).to.deep.equal([ + this.account2.address, + (this.salePrice * newFraction) / 10_000n, + ]); }); }); } From d155600d554d28b583a8ab36dee0849215d48a20 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 15 Dec 2023 17:50:46 +0100 Subject: [PATCH 35/44] Migrate `utils/types/time` tests to ethers.js (#4778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- test/utils/types/Time.test.js | 98 +++++++++++++++++------------------ 1 file changed, 47 insertions(+), 51 deletions(-) diff --git a/test/utils/types/Time.test.js b/test/utils/types/Time.test.js index d30daffc2c4..c55a769f985 100644 --- a/test/utils/types/Time.test.js +++ b/test/utils/types/Time.test.js @@ -1,24 +1,22 @@ -require('@openzeppelin/test-helpers'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { clock } = require('../../helpers/time'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { + bigint: { clock }, +} = require('../../helpers/time'); + const { product } = require('../../helpers/iterate'); const { max } = require('../../helpers/math'); -const Time = artifacts.require('$Time'); - const MAX_UINT32 = 1n << (32n - 1n); const MAX_UINT48 = 1n << (48n - 1n); const SOME_VALUES = [0n, 1n, 2n, 15n, 16n, 17n, 42n]; const asUint = (value, size) => { - if (typeof value != 'bigint') { - value = BigInt(value); - } - // chai does not support bigint :/ - if (value < 0 || value >= 1n << BigInt(size)) { - throw new Error(`value is not a valid uint${size}`); - } + value = ethers.toBigInt(value); + size = ethers.toBigInt(size); + expect(value).to.be.greaterThanOrEqual(0n, `value is not a valid uint${size}`); + expect(value).to.be.lessThan(1n << size, `value is not a valid uint${size}`); return value; }; @@ -40,18 +38,23 @@ const effectSamplesForTimepoint = timepoint => [ MAX_UINT48, ]; +async function fixture() { + const mock = await ethers.deployContract('$Time'); + return { mock }; +} + contract('Time', function () { beforeEach(async function () { - this.mock = await Time.new(); + Object.assign(this, await loadFixture(fixture)); }); describe('clocks', function () { it('timestamp', async function () { - expect(await this.mock.$timestamp()).to.be.bignumber.equal(web3.utils.toBN(await clock.timestamp())); + expect(await this.mock.$timestamp()).to.equal(await clock.timestamp()); }); it('block number', async function () { - expect(await this.mock.$blockNumber()).to.be.bignumber.equal(web3.utils.toBN(await clock.blocknumber())); + expect(await this.mock.$blockNumber()).to.equal(await clock.blocknumber()); }); }); @@ -63,28 +66,28 @@ contract('Time', function () { const delay = 1272825341158973505578n; it('pack', async function () { - const packed = await this.mock.$pack(valueBefore, valueAfter, effect); - expect(packed).to.be.bignumber.equal(delay.toString()); - - const packed2 = packDelay({ valueBefore, valueAfter, effect }); - expect(packed2).to.be.equal(delay); + expect(await this.mock.$pack(valueBefore, valueAfter, effect)).to.equal(delay); + expect(packDelay({ valueBefore, valueAfter, effect })).to.equal(delay); }); it('unpack', async function () { - const unpacked = await this.mock.$unpack(delay); - expect(unpacked[0]).to.be.bignumber.equal(valueBefore.toString()); - expect(unpacked[1]).to.be.bignumber.equal(valueAfter.toString()); - expect(unpacked[2]).to.be.bignumber.equal(effect.toString()); + expect(await this.mock.$unpack(delay)).to.deep.equal([valueBefore, valueAfter, effect]); - const unpacked2 = unpackDelay(delay); - expect(unpacked2).to.be.deep.equal({ valueBefore, valueAfter, effect }); + expect(unpackDelay(delay)).to.deep.equal({ + valueBefore, + valueAfter, + effect, + }); }); }); it('toDelay', async function () { for (const value of [...SOME_VALUES, MAX_UINT32]) { - const delay = await this.mock.$toDelay(value).then(unpackDelay); - expect(delay).to.be.deep.equal({ valueBefore: 0n, valueAfter: value, effect: 0n }); + expect(await this.mock.$toDelay(value).then(unpackDelay)).to.deep.equal({ + valueBefore: 0n, + valueAfter: value, + effect: 0n, + }); } }); @@ -95,15 +98,14 @@ contract('Time', function () { for (const effect of effectSamplesForTimepoint(timepoint)) { const isPast = effect <= timepoint; - const delay = packDelay({ valueBefore, valueAfter, effect }); - expect(await this.mock.$get(delay)).to.be.bignumber.equal(String(isPast ? valueAfter : valueBefore)); - - const result = await this.mock.$getFull(delay); - expect(result[0]).to.be.bignumber.equal(String(isPast ? valueAfter : valueBefore)); - expect(result[1]).to.be.bignumber.equal(String(isPast ? 0n : valueAfter)); - expect(result[2]).to.be.bignumber.equal(String(isPast ? 0n : effect)); + expect(await this.mock.$get(delay)).to.equal(isPast ? valueAfter : valueBefore); + expect(await this.mock.$getFull(delay)).to.deep.equal([ + isPast ? valueAfter : valueBefore, + isPast ? 0n : valueAfter, + isPast ? 0n : effect, + ]); } }); @@ -119,22 +121,16 @@ contract('Time', function () { const expectedvalueBefore = isPast ? valueAfter : valueBefore; const expectedSetback = max(minSetback, expectedvalueBefore - newvalueAfter, 0n); - const result = await this.mock.$withUpdate( - packDelay({ valueBefore, valueAfter, effect }), - newvalueAfter, - minSetback, - ); - - expect(result[0]).to.be.bignumber.equal( - String( - packDelay({ - valueBefore: expectedvalueBefore, - valueAfter: newvalueAfter, - effect: timepoint + expectedSetback, - }), - ), - ); - expect(result[1]).to.be.bignumber.equal(String(timepoint + expectedSetback)); + expect( + await this.mock.$withUpdate(packDelay({ valueBefore, valueAfter, effect }), newvalueAfter, minSetback), + ).to.deep.equal([ + packDelay({ + valueBefore: expectedvalueBefore, + valueAfter: newvalueAfter, + effect: timepoint + expectedSetback, + }), + timepoint + expectedSetback, + ]); } }); }); From c3cd70811b0993aab26840034c47e63fb3a2c993 Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Mon, 18 Dec 2023 17:09:23 -0300 Subject: [PATCH 36/44] Migrate governance tests to ethers.js (#4728) Co-authored-by: ernestognw Co-authored-by: Hadrien Croubois --- package-lock.json | 20 +- package.json | 1 - test/governance/Governor.test.js | 994 +++++----- test/governance/TimelockController.test.js | 1653 ++++++++--------- .../extensions/GovernorERC721.test.js | 181 +- .../GovernorPreventLateQuorum.test.js | 246 ++- .../extensions/GovernorStorage.test.js | 209 ++- .../extensions/GovernorTimelockAccess.test.js | 858 +++++---- .../GovernorTimelockCompound.test.js | 447 ++--- .../GovernorTimelockControl.test.js | 519 +++--- .../GovernorVotesQuorumFraction.test.js | 164 +- .../extensions/GovernorWithParams.test.js | 327 ++-- test/governance/utils/ERC6372.behavior.js | 10 +- test/governance/utils/Votes.behavior.js | 467 +++-- test/governance/utils/Votes.test.js | 110 +- test/helpers/governance.js | 339 ++-- test/helpers/iterate.js | 7 + test/helpers/time.js | 11 +- test/helpers/txpool.js | 25 +- .../ERC20/extensions/ERC20Permit.test.js | 6 +- .../token/ERC20/extensions/ERC20Votes.test.js | 811 ++++---- .../ERC721/extensions/ERC721Votes.test.js | 261 +-- 22 files changed, 3734 insertions(+), 3932 deletions(-) diff --git a/package-lock.json b/package-lock.json index 922723c432f..fc8a1ca7f40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openzeppelin-solidity", - "version": "5.0.0", + "version": "5.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openzeppelin-solidity", - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "devDependencies": { "@changesets/changelog-github": "^0.5.0", @@ -25,7 +25,6 @@ "@openzeppelin/test-helpers": "^0.5.13", "@openzeppelin/upgrade-safe-transpiler": "^0.3.32", "@openzeppelin/upgrades-core": "^1.20.6", - "array.prototype.at": "^1.1.1", "chai": "^4.2.0", "eslint": "^8.30.0", "eslint-config-prettier": "^9.0.0", @@ -4908,21 +4907,6 @@ "node": ">=0.10.0" } }, - "node_modules/array.prototype.at": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.at/-/array.prototype.at-1.1.2.tgz", - "integrity": "sha512-TPj626jUZMc2Qbld8uXKZrXM/lSStx2KfbIyF70Ui9RgdgibpTWC6WGCuff6qQ7xYzqXtir60WAHrfmknkF3Vw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array.prototype.findlast": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.3.tgz", diff --git a/package.json b/package.json index d1005679ca7..644de03b9bd 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "@openzeppelin/test-helpers": "^0.5.13", "@openzeppelin/upgrade-safe-transpiler": "^0.3.32", "@openzeppelin/upgrades-core": "^1.20.6", - "array.prototype.at": "^1.1.1", "chai": "^4.2.0", "eslint": "^8.30.0", "eslint-config-prettier": "^9.0.0", diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js index b277d8c1241..f27e0d9f2c0 100644 --- a/test/governance/Governor.test.js +++ b/test/governance/Governor.test.js @@ -1,77 +1,94 @@ -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const Enums = require('../helpers/enums'); -const { getDomain, domainType, Ballot } = require('../helpers/eip712'); -const { GovernorHelper, proposalStatesToBitMap } = require('../helpers/governance'); -const { clockFromReceipt } = require('../helpers/time'); -const { expectRevertCustomError } = require('../helpers/customError'); +const { GovernorHelper } = require('../helpers/governance'); +const { getDomain, Ballot } = require('../helpers/eip712'); +const { bigint: Enums } = require('../helpers/enums'); +const { bigint: time } = require('../helpers/time'); const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); const { shouldBehaveLikeERC6372 } = require('./utils/ERC6372.behavior'); -const { ZERO_BYTES32 } = require('@openzeppelin/test-helpers/src/constants'); - -const Governor = artifacts.require('$GovernorMock'); -const CallReceiver = artifacts.require('CallReceiverMock'); -const ERC721 = artifacts.require('$ERC721'); -const ERC1155 = artifacts.require('$ERC1155'); -const ERC1271WalletMock = artifacts.require('ERC1271WalletMock'); const TOKENS = [ - { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, - { Token: artifacts.require('$ERC20VotesLegacyMock'), mode: 'blocknumber' }, + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, + { Token: '$ERC20VotesLegacyMock', mode: 'blocknumber' }, ]; -contract('Governor', function (accounts) { - const [owner, proposer, voter1, voter2, voter3, voter4] = accounts; - - const name = 'OZ-Governor'; - const version = '1'; - const tokenName = 'MockToken'; - const tokenSymbol = 'MTKN'; - const tokenSupply = web3.utils.toWei('100'); - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); + +const signBallot = account => (contract, message) => + getDomain(contract).then(domain => account.signTypedData(domain, { Ballot }, message)); + +async function deployToken(contractName) { + try { + return await ethers.deployContract(contractName, [tokenName, tokenSymbol, tokenName, version]); + } catch (error) { + if (error.message == 'incorrect number of arguments to constructor') { + // ERC20VotesLegacyMock has a different construction that uses version='1' by default. + return ethers.deployContract(contractName, [tokenName, tokenSymbol, tokenName]); + } + throw error; + } +} + +describe('Governor', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [owner, proposer, voter1, voter2, voter3, voter4, userEOA] = await ethers.getSigners(); + const receiver = await ethers.deployContract('CallReceiverMock'); + + const token = await deployToken(Token, [tokenName, tokenSymbol, version]); + const mock = await ethers.deployContract('$GovernorMock', [ + name, // name + votingDelay, // initialVotingDelay + votingPeriod, // initialVotingPeriod + 0n, // initialProposalThreshold + token, // tokenAddress + 10n, // quorumNumeratorValue + ]); + + await owner.sendTransaction({ to: mock, value }); + await token.$_mint(owner, tokenSupply); + + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token: token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(owner).delegate({ token: token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(owner).delegate({ token: token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(owner).delegate({ token: token, to: voter4, value: ethers.parseEther('2') }); + + return { + owner, + proposer, + voter1, + voter2, + voter3, + voter4, + userEOA, + receiver, + token, + mock, + helper, + }; + }; + + describe(`using ${Token}`, function () { beforeEach(async function () { - this.chainId = await web3.eth.getChainId(); - try { - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - } catch { - // ERC20VotesLegacyMock has a different construction that uses version='1' by default. - this.token = await Token.new(tokenName, tokenSymbol, tokenName); - } - this.mock = await Governor.new( - name, // name - votingDelay, // initialVotingDelay - votingPeriod, // initialVotingPeriod - 0, // initialProposalThreshold - this.token.address, // tokenAddress - 10, // quorumNumeratorValue - ); - this.receiver = await CallReceiver.new(); - - this.helper = new GovernorHelper(this.mock, mode); - - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); - - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - + Object.assign(this, await loadFixture(fixture)); + // initiate fresh proposal this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunction().encodeABI(), + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunction'), value, }, ], @@ -83,216 +100,163 @@ contract('Governor', function (accounts) { shouldBehaveLikeERC6372(mode); it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=for,abstain'); + expect(await this.mock.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token.target); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.equal(0n); + expect(await this.mock.COUNTING_MODE()).to.equal('support=bravo&quorum=for,abstain'); }); it('nominal workflow', async function () { // Before - expect(await this.mock.proposalProposer(this.proposal.id)).to.be.equal(constants.ZERO_ADDRESS); - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value); - expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0'); + expect(await this.mock.proposalProposer(this.proposal.id)).to.equal(ethers.ZeroAddress); + expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.false; + expect(await ethers.provider.getBalance(this.mock)).to.equal(value); + expect(await ethers.provider.getBalance(this.receiver)).to.equal(0n); - expect(await this.mock.proposalEta(this.proposal.id)).to.be.bignumber.equal('0'); - expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.equal(false); + expect(await this.mock.proposalEta(this.proposal.id)).to.equal(0n); + expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.false; // Run proposal - const txPropose = await this.helper.propose({ from: proposer }); - - expectEvent(txPropose, 'ProposalCreated', { - proposalId: this.proposal.id, - proposer, - targets: this.proposal.targets, - // values: this.proposal.values, - signatures: this.proposal.signatures, - calldatas: this.proposal.data, - voteStart: web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay), - voteEnd: web3.utils - .toBN(await clockFromReceipt[mode](txPropose.receipt)) - .add(votingDelay) - .add(votingPeriod), - description: this.proposal.description, - }); + const txPropose = await this.helper.connect(this.proposer).propose(); + const timepoint = await time.clockFromReceipt[mode](txPropose); + + await expect(txPropose) + .to.emit(this.mock, 'ProposalCreated') + .withArgs( + this.proposal.id, + this.proposer.address, + this.proposal.targets, + this.proposal.values, + this.proposal.signatures, + this.proposal.data, + timepoint + votingDelay, + timepoint + votingDelay + votingPeriod, + this.proposal.description, + ); await this.helper.waitForSnapshot(); - expectEvent( - await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }), - 'VoteCast', - { - voter: voter1, - support: Enums.VoteType.For, - reason: 'This is nice', - weight: web3.utils.toWei('10'), - }, - ); + await expect(this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For, reason: 'This is nice' })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter1.address, this.proposal.id, Enums.VoteType.For, ethers.parseEther('10'), 'This is nice'); - expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', { - voter: voter2, - support: Enums.VoteType.For, - weight: web3.utils.toWei('7'), - }); + await expect(this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter2.address, this.proposal.id, Enums.VoteType.For, ethers.parseEther('7'), ''); - expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', { - voter: voter3, - support: Enums.VoteType.Against, - weight: web3.utils.toWei('5'), - }); + await expect(this.helper.connect(this.voter3).vote({ support: Enums.VoteType.Against })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter3.address, this.proposal.id, Enums.VoteType.Against, ethers.parseEther('5'), ''); - expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', { - voter: voter4, - support: Enums.VoteType.Abstain, - weight: web3.utils.toWei('2'), - }); + await expect(this.helper.connect(this.voter4).vote({ support: Enums.VoteType.Abstain })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter4.address, this.proposal.id, Enums.VoteType.Abstain, ethers.parseEther('2'), ''); await this.helper.waitForDeadline(); const txExecute = await this.helper.execute(); - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); + await expect(txExecute).to.emit(this.mock, 'ProposalExecuted').withArgs(this.proposal.id); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + await expect(txExecute).to.emit(this.receiver, 'MockFunctionCalled'); // After - expect(await this.mock.proposalProposer(this.proposal.id)).to.be.equal(proposer); - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); - expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value); - - expect(await this.mock.proposalEta(this.proposal.id)).to.be.bignumber.equal('0'); - expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.equal(false); + expect(await this.mock.proposalProposer(this.proposal.id)).to.equal(this.proposer.address); + expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.true; + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.receiver)).to.equal(value); + + expect(await this.mock.proposalEta(this.proposal.id)).to.equal(0n); + expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.false; }); it('send ethers', async function () { - const empty = web3.utils.toChecksumAddress(web3.utils.randomHex(20)); - - this.proposal = this.helper.setProposal( + this.helper.setProposal( [ { - target: empty, + target: this.userEOA.address, value, }, ], '', ); - // Before - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value); - expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal('0'); - // Run proposal - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - - // After - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); - expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal(value); + await expect(async () => { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.waitForDeadline(); + return this.helper.execute(); + }).to.changeEtherBalances([this.mock, this.userEOA], [-value, value]); }); describe('vote with signature', function () { - const sign = privateKey => async (contract, message) => { - const domain = await getDomain(contract); - return ethSigUtil.signTypedMessage(privateKey, { - data: { - primaryType: 'Ballot', - types: { - EIP712Domain: domainType(domain), - Ballot, - }, - domain, - message, - }, - }); - }; - - afterEach('no other votes are cast for proposalId', async function () { - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); - }); - it('votes with an EOA signature', async function () { - const voterBySig = Wallet.generate(); - const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString()); - - await this.token.delegate(voterBySigAddress, { from: voter1 }); + await this.token.connect(this.voter1).delegate(this.userEOA); - const nonce = await this.mock.nonces(voterBySigAddress); + const nonce = await this.mock.nonces(this.userEOA); // Run proposal await this.helper.propose(); await this.helper.waitForSnapshot(); - expectEvent( - await this.helper.vote({ + await expect( + this.helper.vote({ support: Enums.VoteType.For, - voter: voterBySigAddress, + voter: this.userEOA.address, nonce, - signature: sign(voterBySig.getPrivateKey()), + signature: signBallot(this.userEOA), }), - 'VoteCast', - { - voter: voterBySigAddress, - support: Enums.VoteType.For, - }, - ); + ) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.userEOA.address, this.proposal.id, Enums.VoteType.For, ethers.parseEther('10'), ''); + await this.helper.waitForDeadline(); await this.helper.execute(); // After - expect(await this.mock.hasVoted(this.proposal.id, voterBySigAddress)).to.be.equal(true); - expect(await this.mock.nonces(voterBySigAddress)).to.be.bignumber.equal(nonce.addn(1)); + expect(await this.mock.hasVoted(this.proposal.id, this.userEOA)).to.be.true; + expect(await this.mock.nonces(this.userEOA)).to.equal(nonce + 1n); }); it('votes with a valid EIP-1271 signature', async function () { - const ERC1271WalletOwner = Wallet.generate(); - ERC1271WalletOwner.address = web3.utils.toChecksumAddress(ERC1271WalletOwner.getAddressString()); - - const wallet = await ERC1271WalletMock.new(ERC1271WalletOwner.address); + const wallet = await ethers.deployContract('ERC1271WalletMock', [this.userEOA]); - await this.token.delegate(wallet.address, { from: voter1 }); + await this.token.connect(this.voter1).delegate(wallet); - const nonce = await this.mock.nonces(wallet.address); + const nonce = await this.mock.nonces(this.userEOA); // Run proposal await this.helper.propose(); await this.helper.waitForSnapshot(); - expectEvent( - await this.helper.vote({ + await expect( + this.helper.vote({ support: Enums.VoteType.For, - voter: wallet.address, + voter: wallet.target, nonce, - signature: sign(ERC1271WalletOwner.getPrivateKey()), + signature: signBallot(this.userEOA), }), - 'VoteCast', - { - voter: wallet.address, - support: Enums.VoteType.For, - }, - ); + ) + .to.emit(this.mock, 'VoteCast') + .withArgs(wallet.target, this.proposal.id, Enums.VoteType.For, ethers.parseEther('10'), ''); await this.helper.waitForDeadline(); await this.helper.execute(); // After - expect(await this.mock.hasVoted(this.proposal.id, wallet.address)).to.be.equal(true); - expect(await this.mock.nonces(wallet.address)).to.be.bignumber.equal(nonce.addn(1)); + expect(await this.mock.hasVoted(this.proposal.id, wallet)).to.be.true; + expect(await this.mock.nonces(wallet)).to.equal(nonce + 1n); }); afterEach('no other votes are cast', async function () { - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.false; }); }); @@ -300,97 +264,73 @@ contract('Governor', function (accounts) { describe('on propose', function () { it('if proposal already exists', async function () { await this.helper.propose(); - await expectRevertCustomError(this.helper.propose(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Pending, - ZERO_BYTES32, - ]); + await expect(this.helper.propose()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs(this.proposal.id, Enums.ProposalState.Pending, ethers.ZeroHash); }); it('if proposer has below threshold votes', async function () { - const votes = web3.utils.toWei('10'); - const threshold = web3.utils.toWei('1000'); + const votes = ethers.parseEther('10'); + const threshold = ethers.parseEther('1000'); await this.mock.$_setProposalThreshold(threshold); - await expectRevertCustomError(this.helper.propose({ from: voter1 }), 'GovernorInsufficientProposerVotes', [ - voter1, - votes, - threshold, - ]); + await expect(this.helper.connect(this.voter1).propose()) + .to.be.revertedWithCustomError(this.mock, 'GovernorInsufficientProposerVotes') + .withArgs(this.voter1.address, votes, threshold); }); }); describe('on vote', function () { it('if proposal does not exist', async function () { - await expectRevertCustomError( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'GovernorNonexistentProposal', - [this.proposal.id], - ); + await expect(this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For })) + .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal') + .withArgs(this.proposal.id); }); it('if voting has not started', async function () { await this.helper.propose(); - await expectRevertCustomError( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'GovernorUnexpectedProposalState', - [this.proposal.id, Enums.ProposalState.Pending, proposalStatesToBitMap([Enums.ProposalState.Active])], - ); + await expect(this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For })) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Pending, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Active]), + ); }); it('if support value is invalid', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await expectRevertCustomError( - this.helper.vote({ support: web3.utils.toBN('255') }), + await expect(this.helper.vote({ support: 255 })).to.be.revertedWithCustomError( + this.mock, 'GovernorInvalidVoteType', - [], ); }); it('if vote was already casted', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await expectRevertCustomError( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'GovernorAlreadyCastVote', - [voter1], - ); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await expect(this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For })) + .to.be.revertedWithCustomError(this.mock, 'GovernorAlreadyCastVote') + .withArgs(this.voter1.address); }); it('if voting is over', async function () { await this.helper.propose(); await this.helper.waitForDeadline(); - await expectRevertCustomError( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'GovernorUnexpectedProposalState', - [this.proposal.id, Enums.ProposalState.Defeated, proposalStatesToBitMap([Enums.ProposalState.Active])], - ); + await expect(this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For })) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Defeated, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Active]), + ); }); }); describe('on vote by signature', function () { beforeEach(async function () { - this.voterBySig = Wallet.generate(); - this.voterBySig.address = web3.utils.toChecksumAddress(this.voterBySig.getAddressString()); - - this.data = (contract, message) => - getDomain(contract).then(domain => ({ - primaryType: 'Ballot', - types: { - EIP712Domain: domainType(domain), - Ballot, - }, - domain, - message, - })); - - this.signature = (contract, message) => - this.data(contract, message).then(data => - ethSigUtil.signTypedMessage(this.voterBySig.getPrivateKey(), { data }), - ); - - await this.token.delegate(this.voterBySig.address, { from: voter1 }); + await this.token.connect(this.voter1).delegate(this.userEOA); // Run proposal await this.helper.propose(); @@ -398,96 +338,104 @@ contract('Governor', function (accounts) { }); it('if signature does not match signer', async function () { - const nonce = await this.mock.nonces(this.voterBySig.address); + const nonce = await this.mock.nonces(this.userEOA); + + function tamper(str, index, mask) { + const arrayStr = ethers.toBeArray(BigInt(str)); + arrayStr[index] ^= mask; + return ethers.hexlify(arrayStr); + } const voteParams = { support: Enums.VoteType.For, - voter: this.voterBySig.address, + voter: this.userEOA.address, nonce, - signature: async (...params) => { - const sig = await this.signature(...params); - const tamperedSig = web3.utils.hexToBytes(sig); - tamperedSig[42] ^= 0xff; - return web3.utils.bytesToHex(tamperedSig); - }, + signature: (...args) => signBallot(this.userEOA)(...args).then(sig => tamper(sig, 42, 0xff)), }; - await expectRevertCustomError(this.helper.vote(voteParams), 'GovernorInvalidSignature', [voteParams.voter]); + await expect(this.helper.vote(voteParams)) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature') + .withArgs(voteParams.voter); }); it('if vote nonce is incorrect', async function () { - const nonce = await this.mock.nonces(this.voterBySig.address); + const nonce = await this.mock.nonces(this.userEOA); const voteParams = { support: Enums.VoteType.For, - voter: this.voterBySig.address, - nonce: nonce.addn(1), - signature: this.signature, + voter: this.userEOA.address, + nonce: nonce + 1n, + signature: signBallot(this.userEOA), }; - await expectRevertCustomError( - this.helper.vote(voteParams), - // The signature check implies the nonce can't be tampered without changing the signer - 'GovernorInvalidSignature', - [voteParams.voter], - ); + await expect(this.helper.vote(voteParams)) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature') + .withArgs(voteParams.voter); }); }); describe('on queue', function () { it('always', async function () { - await this.helper.propose({ from: proposer }); + await this.helper.connect(this.proposer).propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - await expectRevertCustomError(this.helper.queue(), 'GovernorQueueNotImplemented', []); + await expect(this.helper.queue()).to.be.revertedWithCustomError(this.mock, 'GovernorQueueNotImplemented'); }); }); describe('on execute', function () { it('if proposal does not exist', async function () { - await expectRevertCustomError(this.helper.execute(), 'GovernorNonexistentProposal', [this.proposal.id]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal') + .withArgs(this.proposal.id); }); it('if quorum is not reached', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter3 }); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Active, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await this.helper.connect(this.voter3).vote({ support: Enums.VoteType.For }); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Active, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('if score not reached', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 }); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Active, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.Against }); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Active, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('if voting is not over', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Active, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Active, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('if receiver revert without reason', async function () { this.helper.setProposal( [ { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(), + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunctionRevertsNoReason'), }, ], '', @@ -495,17 +443,17 @@ contract('Governor', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - await expectRevertCustomError(this.helper.execute(), 'FailedInnerCall', []); + await expect(this.helper.execute()).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); }); it('if receiver revert with reason', async function () { this.helper.setProposal( [ { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI(), + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunctionRevertsReason'), }, ], '', @@ -513,147 +461,157 @@ contract('Governor', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - await expectRevert(this.helper.execute(), 'CallReceiverMock: reverting'); + await expect(this.helper.execute()).to.be.revertedWith('CallReceiverMock: reverting'); }); it('if proposal was already executed', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.execute(); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Executed, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Executed, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); }); }); describe('state', function () { it('Unset', async function () { - await expectRevertCustomError(this.mock.state(this.proposal.id), 'GovernorNonexistentProposal', [ - this.proposal.id, - ]); + await expect(this.mock.state(this.proposal.id)) + .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal') + .withArgs(this.proposal.id); }); it('Pending & Active', async function () { await this.helper.propose(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Pending); await this.helper.waitForSnapshot(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending); - await this.helper.waitForSnapshot(+1); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Pending); + await this.helper.waitForSnapshot(1n); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Active); }); it('Defeated', async function () { await this.helper.propose(); await this.helper.waitForDeadline(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); - await this.helper.waitForDeadline(+1); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Active); + await this.helper.waitForDeadline(1n); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Defeated); }); it('Succeeded', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); - await this.helper.waitForDeadline(+1); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Active); + await this.helper.waitForDeadline(1n); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Succeeded); }); it('Executed', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.execute(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Executed); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Executed); }); }); describe('cancel', function () { describe('internal', function () { it('before proposal', async function () { - await expectRevertCustomError(this.helper.cancel('internal'), 'GovernorNonexistentProposal', [ - this.proposal.id, - ]); + await expect(this.helper.cancel('internal')) + .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal') + .withArgs(this.proposal.id); }); it('after proposal', async function () { await this.helper.propose(); await this.helper.cancel('internal'); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Canceled); await this.helper.waitForSnapshot(); - await expectRevertCustomError( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'GovernorUnexpectedProposalState', - [this.proposal.id, Enums.ProposalState.Canceled, proposalStatesToBitMap([Enums.ProposalState.Active])], - ); + await expect(this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For })) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Active]), + ); }); it('after vote', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.cancel('internal'); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Canceled); await this.helper.waitForDeadline(); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('after deadline', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.cancel('internal'); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Canceled); + + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('after execution', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.execute(); - await expectRevertCustomError(this.helper.cancel('internal'), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Executed, - proposalStatesToBitMap( - [Enums.ProposalState.Canceled, Enums.ProposalState.Expired, Enums.ProposalState.Executed], - { inverted: true }, - ), - ]); + await expect(this.helper.cancel('internal')) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Executed, + GovernorHelper.proposalStatesToBitMap( + [Enums.ProposalState.Canceled, Enums.ProposalState.Expired, Enums.ProposalState.Executed], + { inverted: true }, + ), + ); }); }); describe('public', function () { it('before proposal', async function () { - await expectRevertCustomError(this.helper.cancel('external'), 'GovernorNonexistentProposal', [ - this.proposal.id, - ]); + await expect(this.helper.cancel('external')) + .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal') + .withArgs(this.proposal.id); }); it('after proposal', async function () { @@ -663,61 +621,69 @@ contract('Governor', function (accounts) { }); it('after proposal - restricted to proposer', async function () { - await this.helper.propose(); + await this.helper.connect(this.proposer).propose(); - await expectRevertCustomError(this.helper.cancel('external', { from: owner }), 'GovernorOnlyProposer', [ - owner, - ]); + await expect(this.helper.connect(this.owner).cancel('external')) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyProposer') + .withArgs(this.owner.address); }); it('after vote started', async function () { await this.helper.propose(); - await this.helper.waitForSnapshot(1); // snapshot + 1 block - - await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Active, - proposalStatesToBitMap([Enums.ProposalState.Pending]), - ]); + await this.helper.waitForSnapshot(1n); // snapshot + 1 block + + await expect(this.helper.cancel('external')) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Active, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Pending]), + ); }); it('after vote', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - - await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Active, - proposalStatesToBitMap([Enums.ProposalState.Pending]), - ]); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + + await expect(this.helper.cancel('external')) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Active, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Pending]), + ); }); it('after deadline', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Succeeded, - proposalStatesToBitMap([Enums.ProposalState.Pending]), - ]); + await expect(this.helper.cancel('external')) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Succeeded, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Pending]), + ); }); it('after execution', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.execute(); - await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Executed, - proposalStatesToBitMap([Enums.ProposalState.Pending]), - ]); + await expect(this.helper.cancel('external')) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Executed, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Pending]), + ); }); }); }); @@ -725,88 +691,123 @@ contract('Governor', function (accounts) { describe('proposal length', function () { it('empty', async function () { this.helper.setProposal([], ''); - await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [0, 0, 0]); + + await expect(this.helper.propose()) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidProposalLength') + .withArgs(0, 0, 0); }); it('mismatch #1', async function () { this.helper.setProposal( { targets: [], - values: [web3.utils.toWei('0')], - data: [this.receiver.contract.methods.mockFunction().encodeABI()], + values: [0n], + data: [this.receiver.interface.encodeFunctionData('mockFunction')], }, '', ); - await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [0, 1, 1]); + await expect(this.helper.propose()) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidProposalLength') + .withArgs(0, 1, 1); }); it('mismatch #2', async function () { this.helper.setProposal( { - targets: [this.receiver.address], + targets: [this.receiver.target], values: [], - data: [this.receiver.contract.methods.mockFunction().encodeABI()], + data: [this.receiver.interface.encodeFunctionData('mockFunction')], }, '', ); - await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [1, 1, 0]); + await expect(this.helper.propose()) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidProposalLength') + .withArgs(1, 1, 0); }); it('mismatch #3', async function () { this.helper.setProposal( { - targets: [this.receiver.address], - values: [web3.utils.toWei('0')], + targets: [this.receiver.target], + values: [0n], data: [], }, '', ); - await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [1, 0, 1]); + await expect(this.helper.propose()) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidProposalLength') + .withArgs(1, 0, 1); }); }); describe('frontrun protection using description suffix', function () { + function shouldPropose() { + it('proposer can propose', async function () { + const txPropose = await this.helper.connect(this.proposer).propose(); + + await expect(txPropose) + .to.emit(this.mock, 'ProposalCreated') + .withArgs( + this.proposal.id, + this.proposer.address, + this.proposal.targets, + this.proposal.values, + this.proposal.signatures, + this.proposal.data, + (await time.clockFromReceipt[mode](txPropose)) + votingDelay, + (await time.clockFromReceipt[mode](txPropose)) + votingDelay + votingPeriod, + this.proposal.description, + ); + }); + + it('someone else can propose', async function () { + const txPropose = await this.helper.connect(this.voter1).propose(); + + await expect(txPropose) + .to.emit(this.mock, 'ProposalCreated') + .withArgs( + this.proposal.id, + this.voter1.address, + this.proposal.targets, + this.proposal.values, + this.proposal.signatures, + this.proposal.data, + (await time.clockFromReceipt[mode](txPropose)) + votingDelay, + (await time.clockFromReceipt[mode](txPropose)) + votingDelay + votingPeriod, + this.proposal.description, + ); + }); + } + describe('without protection', function () { describe('without suffix', function () { - it('proposer can propose', async function () { - expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated'); - }); - - it('someone else can propose', async function () { - expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated'); - }); + shouldPropose(); }); describe('with different suffix', function () { - beforeEach(async function () { + beforeEach(function () { this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunction().encodeABI(), + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunction'), value, }, ], - `#wrong-suffix=${proposer}`, + `#wrong-suffix=${this.proposer}`, ); }); - it('proposer can propose', async function () { - expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated'); - }); - - it('someone else can propose', async function () { - expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated'); - }); + shouldPropose(); }); describe('with proposer suffix but bad address part', function () { - beforeEach(async function () { + beforeEach(function () { this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunction().encodeABI(), + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunction'), value, }, ], @@ -814,69 +815,53 @@ contract('Governor', function (accounts) { ); }); - it('propose can propose', async function () { - expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated'); - }); - - it('someone else can propose', async function () { - expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated'); - }); + shouldPropose(); }); }); describe('with protection via proposer suffix', function () { - beforeEach(async function () { + beforeEach(function () { this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunction().encodeABI(), + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunction'), value, }, ], - `#proposer=${proposer}`, + `#proposer=${this.proposer}`, ); }); - it('proposer can propose', async function () { - expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated'); - }); - - it('someone else cannot propose', async function () { - await expectRevertCustomError(this.helper.propose({ from: voter1 }), 'GovernorRestrictedProposer', [ - voter1, - ]); - }); + shouldPropose(); }); }); describe('onlyGovernance updates', function () { it('setVotingDelay is protected', async function () { - await expectRevertCustomError(this.mock.setVotingDelay('0', { from: owner }), 'GovernorOnlyExecutor', [ - owner, - ]); + await expect(this.mock.connect(this.owner).setVotingDelay(0n)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); it('setVotingPeriod is protected', async function () { - await expectRevertCustomError(this.mock.setVotingPeriod('32', { from: owner }), 'GovernorOnlyExecutor', [ - owner, - ]); + await expect(this.mock.connect(this.owner).setVotingPeriod(32n)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); it('setProposalThreshold is protected', async function () { - await expectRevertCustomError( - this.mock.setProposalThreshold('1000000000000000000', { from: owner }), - 'GovernorOnlyExecutor', - [owner], - ); + await expect(this.mock.connect(this.owner).setProposalThreshold(1_000_000_000_000_000_000n)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); it('can setVotingDelay through governance', async function () { this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.setVotingDelay('0').encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setVotingDelay', [0n]), }, ], '', @@ -884,20 +869,20 @@ contract('Governor', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - expectEvent(await this.helper.execute(), 'VotingDelaySet', { oldVotingDelay: '4', newVotingDelay: '0' }); + await expect(this.helper.execute()).to.emit(this.mock, 'VotingDelaySet').withArgs(4n, 0n); - expect(await this.mock.votingDelay()).to.be.bignumber.equal('0'); + expect(await this.mock.votingDelay()).to.equal(0n); }); it('can setVotingPeriod through governance', async function () { this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.setVotingPeriod('32').encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setVotingPeriod', [32n]), }, ], '', @@ -905,21 +890,22 @@ contract('Governor', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - expectEvent(await this.helper.execute(), 'VotingPeriodSet', { oldVotingPeriod: '16', newVotingPeriod: '32' }); + await expect(this.helper.execute()).to.emit(this.mock, 'VotingPeriodSet').withArgs(16n, 32n); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32'); + expect(await this.mock.votingPeriod()).to.equal(32n); }); it('cannot setVotingPeriod to 0 through governance', async function () { - const votingPeriod = 0; + const votingPeriod = 0n; + this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.setVotingPeriod(votingPeriod).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setVotingPeriod', [votingPeriod]), }, ], '', @@ -927,18 +913,20 @@ contract('Governor', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - await expectRevertCustomError(this.helper.execute(), 'GovernorInvalidVotingPeriod', [votingPeriod]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidVotingPeriod') + .withArgs(votingPeriod); }); it('can setProposalThreshold to 0 through governance', async function () { this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setProposalThreshold', [1_000_000_000_000_000_000n]), }, ], '', @@ -946,66 +934,62 @@ contract('Governor', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - expectEvent(await this.helper.execute(), 'ProposalThresholdSet', { - oldProposalThreshold: '0', - newProposalThreshold: '1000000000000000000', - }); + await expect(this.helper.execute()) + .to.emit(this.mock, 'ProposalThresholdSet') + .withArgs(0n, 1_000_000_000_000_000_000n); - expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000'); + expect(await this.mock.proposalThreshold()).to.equal(1_000_000_000_000_000_000n); }); }); describe('safe receive', function () { describe('ERC721', function () { - const name = 'Non Fungible Token'; - const symbol = 'NFT'; - const tokenId = web3.utils.toBN(1); + const tokenId = 1n; beforeEach(async function () { - this.token = await ERC721.new(name, symbol); - await this.token.$_mint(owner, tokenId); + this.token = await ethers.deployContract('$ERC721', ['Non Fungible Token', 'NFT']); + await this.token.$_mint(this.owner, tokenId); }); it('can receive an ERC721 safeTransfer', async function () { - await this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner }); + await this.token.connect(this.owner).safeTransferFrom(this.owner, this.mock.target, tokenId); }); }); describe('ERC1155', function () { - const uri = 'https://token-cdn-domain/{id}.json'; const tokenIds = { - 1: web3.utils.toBN(1000), - 2: web3.utils.toBN(2000), - 3: web3.utils.toBN(3000), + 1: 1000n, + 2: 2000n, + 3: 3000n, }; beforeEach(async function () { - this.token = await ERC1155.new(uri); - await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); + this.token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']); + await this.token.$_mintBatch(this.owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); }); it('can receive ERC1155 safeTransfer', async function () { - await this.token.safeTransferFrom( - owner, - this.mock.address, + await this.token.connect(this.owner).safeTransferFrom( + this.owner, + this.mock.target, ...Object.entries(tokenIds)[0], // id + amount '0x', - { from: owner }, ); }); it('can receive ERC1155 safeBatchTransfer', async function () { - await this.token.safeBatchTransferFrom( - owner, - this.mock.address, - Object.keys(tokenIds), - Object.values(tokenIds), - '0x', - { from: owner }, - ); + await this.token + .connect(this.owner) + .safeBatchTransferFrom( + this.owner, + this.mock.target, + Object.keys(tokenIds), + Object.values(tokenIds), + '0x', + ); }); }); }); diff --git a/test/governance/TimelockController.test.js b/test/governance/TimelockController.test.js index ce051e7870a..9d3f5188b41 100644 --- a/test/governance/TimelockController.test.js +++ b/test/governance/TimelockController.test.js @@ -1,93 +1,112 @@ -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS, ZERO_BYTES32 } = constants; -const { proposalStatesToBitMap } = require('../helpers/governance'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); + +const { GovernorHelper } = require('../helpers/governance'); +const { bigint: time } = require('../helpers/time'); +const { + bigint: { OperationState }, +} = require('../helpers/enums'); const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); -const { expectRevertCustomError } = require('../helpers/customError'); -const { OperationState } = require('../helpers/enums'); -const TimelockController = artifacts.require('TimelockController'); -const CallReceiverMock = artifacts.require('CallReceiverMock'); -const Implementation2 = artifacts.require('Implementation2'); -const ERC721 = artifacts.require('$ERC721'); -const ERC1155 = artifacts.require('$ERC1155'); -const TimelockReentrant = artifacts.require('$TimelockReentrant'); +const salt = '0x025e7b0be353a74631ad648c667493c0e1cd31caa4cc2d3520fdc171ea0cc726'; // a random value const MINDELAY = time.duration.days(1); +const DEFAULT_ADMIN_ROLE = ethers.ZeroHash; +const PROPOSER_ROLE = ethers.id('PROPOSER_ROLE'); +const EXECUTOR_ROLE = ethers.id('EXECUTOR_ROLE'); +const CANCELLER_ROLE = ethers.id('CANCELLER_ROLE'); -const salt = '0x025e7b0be353a74631ad648c667493c0e1cd31caa4cc2d3520fdc171ea0cc726'; // a random value +const getAddress = obj => obj.address ?? obj.target ?? obj; function genOperation(target, value, data, predecessor, salt) { - const id = web3.utils.keccak256( - web3.eth.abi.encodeParameters( + const id = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( ['address', 'uint256', 'bytes', 'uint256', 'bytes32'], - [target, value, data, predecessor, salt], + [getAddress(target), value, data, predecessor, salt], ), ); return { id, target, value, data, predecessor, salt }; } function genOperationBatch(targets, values, payloads, predecessor, salt) { - const id = web3.utils.keccak256( - web3.eth.abi.encodeParameters( + const id = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( ['address[]', 'uint256[]', 'bytes[]', 'uint256', 'bytes32'], - [targets, values, payloads, predecessor, salt], + [targets.map(getAddress), values, payloads, predecessor, salt], ), ); return { id, targets, values, payloads, predecessor, salt }; } -contract('TimelockController', function (accounts) { - const [, admin, proposer, canceller, executor, other] = accounts; - - const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000'; - const PROPOSER_ROLE = web3.utils.soliditySha3('PROPOSER_ROLE'); - const EXECUTOR_ROLE = web3.utils.soliditySha3('EXECUTOR_ROLE'); - const CANCELLER_ROLE = web3.utils.soliditySha3('CANCELLER_ROLE'); +async function fixture() { + const [admin, proposer, canceller, executor, other] = await ethers.getSigners(); + + const mock = await ethers.deployContract('TimelockController', [MINDELAY, [proposer], [executor], admin]); + const callreceivermock = await ethers.deployContract('CallReceiverMock'); + const implementation2 = await ethers.deployContract('Implementation2'); + + expect(await mock.hasRole(CANCELLER_ROLE, proposer)).to.be.true; + await mock.connect(admin).revokeRole(CANCELLER_ROLE, proposer); + await mock.connect(admin).grantRole(CANCELLER_ROLE, canceller); + + return { + admin, + proposer, + canceller, + executor, + other, + mock, + callreceivermock, + implementation2, + }; +} +describe('TimelockController', function () { beforeEach(async function () { - // Deploy new timelock - this.mock = await TimelockController.new(MINDELAY, [proposer], [executor], admin); - - expect(await this.mock.hasRole(CANCELLER_ROLE, proposer)).to.be.equal(true); - await this.mock.revokeRole(CANCELLER_ROLE, proposer, { from: admin }); - await this.mock.grantRole(CANCELLER_ROLE, canceller, { from: admin }); - - // Mocks - this.callreceivermock = await CallReceiverMock.new({ from: admin }); - this.implementation2 = await Implementation2.new({ from: admin }); + Object.assign(this, await loadFixture(fixture)); }); shouldSupportInterfaces(['ERC1155Receiver']); it('initial state', async function () { - expect(await this.mock.getMinDelay()).to.be.bignumber.equal(MINDELAY); + expect(await this.mock.getMinDelay()).to.equal(MINDELAY); - expect(await this.mock.DEFAULT_ADMIN_ROLE()).to.be.equal(DEFAULT_ADMIN_ROLE); - expect(await this.mock.PROPOSER_ROLE()).to.be.equal(PROPOSER_ROLE); - expect(await this.mock.EXECUTOR_ROLE()).to.be.equal(EXECUTOR_ROLE); - expect(await this.mock.CANCELLER_ROLE()).to.be.equal(CANCELLER_ROLE); + expect(await this.mock.DEFAULT_ADMIN_ROLE()).to.equal(DEFAULT_ADMIN_ROLE); + expect(await this.mock.PROPOSER_ROLE()).to.equal(PROPOSER_ROLE); + expect(await this.mock.EXECUTOR_ROLE()).to.equal(EXECUTOR_ROLE); + expect(await this.mock.CANCELLER_ROLE()).to.equal(CANCELLER_ROLE); expect( - await Promise.all([PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, proposer))), - ).to.be.deep.equal([true, false, false]); + await Promise.all( + [PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, this.proposer)), + ), + ).to.deep.equal([true, false, false]); expect( - await Promise.all([PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, canceller))), - ).to.be.deep.equal([false, true, false]); + await Promise.all( + [PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, this.canceller)), + ), + ).to.deep.equal([false, true, false]); expect( - await Promise.all([PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, executor))), - ).to.be.deep.equal([false, false, true]); + await Promise.all( + [PROPOSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE].map(role => this.mock.hasRole(role, this.executor)), + ), + ).to.deep.equal([false, false, true]); }); it('optional admin', async function () { - const mock = await TimelockController.new(MINDELAY, [proposer], [executor], ZERO_ADDRESS, { from: other }); - - expect(await mock.hasRole(DEFAULT_ADMIN_ROLE, admin)).to.be.equal(false); - expect(await mock.hasRole(DEFAULT_ADMIN_ROLE, mock.address)).to.be.equal(true); + const mock = await ethers.deployContract('TimelockController', [ + MINDELAY, + [this.proposer], + [this.executor], + ethers.ZeroAddress, + ]); + expect(await mock.hasRole(DEFAULT_ADMIN_ROLE, this.admin)).to.be.false; + expect(await mock.hasRole(DEFAULT_ADMIN_ROLE, mock.target)).to.be.true; }); describe('methods', function () { @@ -108,7 +127,7 @@ contract('TimelockController', function (accounts) { this.operation.predecessor, this.operation.salt, ), - ).to.be.equal(this.operation.id); + ).to.equal(this.operation.id); }); it('hashOperationBatch', async function () { @@ -127,7 +146,7 @@ contract('TimelockController', function (accounts) { this.operation.predecessor, this.operation.salt, ), - ).to.be.equal(this.operation.id); + ).to.equal(this.operation.id); }); }); describe('simple', function () { @@ -135,114 +154,119 @@ contract('TimelockController', function (accounts) { beforeEach(async function () { this.operation = genOperation( '0x31754f590B97fD975Eb86938f18Cc304E264D2F2', - 0, + 0n, '0x3bf92ccc', - ZERO_BYTES32, + ethers.ZeroHash, salt, ); }); it('proposer can schedule', async function () { - const receipt = await this.mock.schedule( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - ); - expectEvent(receipt, 'CallScheduled', { - id: this.operation.id, - index: web3.utils.toBN(0), - target: this.operation.target, - value: web3.utils.toBN(this.operation.value), - data: this.operation.data, - predecessor: this.operation.predecessor, - delay: MINDELAY, - }); - - expectEvent(receipt, 'CallSalt', { - id: this.operation.id, - salt: this.operation.salt, - }); - - const block = await web3.eth.getBlock(receipt.receipt.blockHash); - - expect(await this.mock.getTimestamp(this.operation.id)).to.be.bignumber.equal( - web3.utils.toBN(block.timestamp).add(MINDELAY), - ); - }); - - it('prevent overwriting active operation', async function () { - await this.mock.schedule( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - ); - - await expectRevertCustomError( - this.mock.schedule( + const tx = await this.mock + .connect(this.proposer) + .schedule( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, MINDELAY, - { from: proposer }, - ), - 'TimelockUnexpectedOperationState', - [this.operation.id, proposalStatesToBitMap(OperationState.Unset)], - ); - }); + ); - it('prevent non-proposer from committing', async function () { - await expectRevertCustomError( - this.mock.schedule( + expect(tx) + .to.emit(this.mock, 'CallScheduled') + .withArgs( + this.operation.id, + 0n, this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, - this.operation.salt, MINDELAY, - { from: other }, - ), - `AccessControlUnauthorizedAccount`, - [other, PROPOSER_ROLE], + ) + .to.emit(this.mock, 'CallSalt') + .withArgs(this.operation.id, this.operation.salt); + + expect(await this.mock.getTimestamp(this.operation.id)).to.equal( + (await time.clockFromReceipt.timestamp(tx)) + MINDELAY, ); }); - it('enforce minimum delay', async function () { - await expectRevertCustomError( - this.mock.schedule( + it('prevent overwriting active operation', async function () { + await this.mock + .connect(this.proposer) + .schedule( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, - MINDELAY - 1, - { from: proposer }, - ), - 'TimelockInsufficientDelay', - [MINDELAY, MINDELAY - 1], - ); + MINDELAY, + ); + + await expect( + this.mock + .connect(this.proposer) + .schedule( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + MINDELAY, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Unset)); + }); + + it('prevent non-proposer from committing', async function () { + await expect( + this.mock + .connect(this.other) + .schedule( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + MINDELAY, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount') + .withArgs(this.other.address, PROPOSER_ROLE); + }); + + it('enforce minimum delay', async function () { + await expect( + this.mock + .connect(this.proposer) + .schedule( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + MINDELAY - 1n, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockInsufficientDelay') + .withArgs(MINDELAY - 1n, MINDELAY); }); it('schedule operation with salt zero', async function () { - const { receipt } = await this.mock.schedule( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - ZERO_BYTES32, - MINDELAY, - { from: proposer }, - ); - expectEvent.notEmitted(receipt, 'CallSalt'); + await expect( + this.mock + .connect(this.proposer) + .schedule( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + ethers.ZeroHash, + MINDELAY, + ), + ).to.not.emit(this.mock, 'CallSalt'); }); }); @@ -250,188 +274,193 @@ contract('TimelockController', function (accounts) { beforeEach(async function () { this.operation = genOperation( '0xAe22104DCD970750610E6FE15E623468A98b15f7', - 0, + 0n, '0x13e414de', - ZERO_BYTES32, + ethers.ZeroHash, '0xc1059ed2dc130227aa1d1d539ac94c641306905c020436c636e19e3fab56fc7f', ); }); it('revert if operation is not scheduled', async function () { - await expectRevertCustomError( - this.mock.execute( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - this.operation.salt, - { from: executor }, - ), - 'TimelockUnexpectedOperationState', - [this.operation.id, proposalStatesToBitMap(OperationState.Ready)], - ); + await expect( + this.mock + .connect(this.executor) + .execute( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready)); }); describe('with scheduled operation', function () { beforeEach(async function () { - ({ receipt: this.receipt, logs: this.logs } = await this.mock.schedule( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - )); - }); - - it('revert if execution comes too early 1/2', async function () { - await expectRevertCustomError( - this.mock.execute( + await this.mock + .connect(this.proposer) + .schedule( this.operation.target, this.operation.value, this.operation.data, this.operation.predecessor, this.operation.salt, - { from: executor }, - ), - 'TimelockUnexpectedOperationState', - [this.operation.id, proposalStatesToBitMap(OperationState.Ready)], - ); + MINDELAY, + ); + }); + + it('revert if execution comes too early 1/2', async function () { + await expect( + this.mock + .connect(this.executor) + .execute( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready)); }); it('revert if execution comes too early 2/2', async function () { - const timestamp = await this.mock.getTimestamp(this.operation.id); - await time.increaseTo(timestamp - 5); // -1 is too tight, test sometime fails + // -1 is too tight, test sometime fails + await this.mock.getTimestamp(this.operation.id).then(clock => time.forward.timestamp(clock - 5n)); - await expectRevertCustomError( - this.mock.execute( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - this.operation.salt, - { from: executor }, - ), - 'TimelockUnexpectedOperationState', - [this.operation.id, proposalStatesToBitMap(OperationState.Ready)], - ); + await expect( + this.mock + .connect(this.executor) + .execute( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready)); }); describe('on time', function () { beforeEach(async function () { - const timestamp = await this.mock.getTimestamp(this.operation.id); - await time.increaseTo(timestamp); + await this.mock.getTimestamp(this.operation.id).then(clock => time.forward.timestamp(clock)); }); it('executor can reveal', async function () { - const receipt = await this.mock.execute( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - this.operation.salt, - { from: executor }, - ); - expectEvent(receipt, 'CallExecuted', { - id: this.operation.id, - index: web3.utils.toBN(0), - target: this.operation.target, - value: web3.utils.toBN(this.operation.value), - data: this.operation.data, - }); + await expect( + this.mock + .connect(this.executor) + .execute( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.emit(this.mock, 'CallExecuted') + .withArgs(this.operation.id, 0n, this.operation.target, this.operation.value, this.operation.data); }); it('prevent non-executor from revealing', async function () { - await expectRevertCustomError( - this.mock.execute( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - this.operation.salt, - { from: other }, - ), - `AccessControlUnauthorizedAccount`, - [other, EXECUTOR_ROLE], - ); + await expect( + this.mock + .connect(this.other) + .execute( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount') + .withArgs(this.other.address, EXECUTOR_ROLE); }); it('prevents reentrancy execution', async function () { // Create operation - const reentrant = await TimelockReentrant.new(); + const reentrant = await ethers.deployContract('$TimelockReentrant'); const reentrantOperation = genOperation( - reentrant.address, - 0, - reentrant.contract.methods.reenter().encodeABI(), - ZERO_BYTES32, + reentrant, + 0n, + reentrant.interface.encodeFunctionData('reenter'), + ethers.ZeroHash, salt, ); // Schedule so it can be executed - await this.mock.schedule( - reentrantOperation.target, - reentrantOperation.value, - reentrantOperation.data, - reentrantOperation.predecessor, - reentrantOperation.salt, - MINDELAY, - { from: proposer }, - ); + await this.mock + .connect(this.proposer) + .schedule( + reentrantOperation.target, + reentrantOperation.value, + reentrantOperation.data, + reentrantOperation.predecessor, + reentrantOperation.salt, + MINDELAY, + ); // Advance on time to make the operation executable - const timestamp = await this.mock.getTimestamp(reentrantOperation.id); - await time.increaseTo(timestamp); + await this.mock.getTimestamp(reentrantOperation.id).then(clock => time.forward.timestamp(clock)); // Grant executor role to the reentrant contract - await this.mock.grantRole(EXECUTOR_ROLE, reentrant.address, { from: admin }); + await this.mock.connect(this.admin).grantRole(EXECUTOR_ROLE, reentrant); // Prepare reenter - const data = this.mock.contract.methods - .execute( - reentrantOperation.target, - reentrantOperation.value, - reentrantOperation.data, - reentrantOperation.predecessor, - reentrantOperation.salt, - ) - .encodeABI(); - await reentrant.enableRentrancy(this.mock.address, data); + const data = this.mock.interface.encodeFunctionData('execute', [ + getAddress(reentrantOperation.target), + reentrantOperation.value, + reentrantOperation.data, + reentrantOperation.predecessor, + reentrantOperation.salt, + ]); + await reentrant.enableRentrancy(this.mock, data); // Expect to fail - await expectRevertCustomError( - this.mock.execute( - reentrantOperation.target, - reentrantOperation.value, - reentrantOperation.data, - reentrantOperation.predecessor, - reentrantOperation.salt, - { from: executor }, - ), - 'TimelockUnexpectedOperationState', - [reentrantOperation.id, proposalStatesToBitMap(OperationState.Ready)], - ); + await expect( + this.mock + .connect(this.executor) + .execute( + reentrantOperation.target, + reentrantOperation.value, + reentrantOperation.data, + reentrantOperation.predecessor, + reentrantOperation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(reentrantOperation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready)); // Disable reentrancy await reentrant.disableReentrancy(); const nonReentrantOperation = reentrantOperation; // Not anymore // Try again successfully - const receipt = await this.mock.execute( - nonReentrantOperation.target, - nonReentrantOperation.value, - nonReentrantOperation.data, - nonReentrantOperation.predecessor, - nonReentrantOperation.salt, - { from: executor }, - ); - expectEvent(receipt, 'CallExecuted', { - id: nonReentrantOperation.id, - index: web3.utils.toBN(0), - target: nonReentrantOperation.target, - value: web3.utils.toBN(nonReentrantOperation.value), - data: nonReentrantOperation.data, - }); + await expect( + this.mock + .connect(this.executor) + .execute( + nonReentrantOperation.target, + nonReentrantOperation.value, + nonReentrantOperation.data, + nonReentrantOperation.predecessor, + nonReentrantOperation.salt, + ), + ) + .to.emit(this.mock, 'CallExecuted') + .withArgs( + nonReentrantOperation.id, + 0n, + getAddress(nonReentrantOperation.target), + nonReentrantOperation.value, + nonReentrantOperation.data, + ); }); }); }); @@ -443,135 +472,139 @@ contract('TimelockController', function (accounts) { beforeEach(async function () { this.operation = genOperationBatch( Array(8).fill('0xEd912250835c812D4516BBD80BdaEA1bB63a293C'), - Array(8).fill(0), + Array(8).fill(0n), Array(8).fill('0x2fcb7a88'), - ZERO_BYTES32, + ethers.ZeroHash, '0x6cf9d042ade5de78bed9ffd075eb4b2a4f6b1736932c2dc8af517d6e066f51f5', ); }); it('proposer can schedule', async function () { - const receipt = await this.mock.scheduleBatch( - this.operation.targets, - this.operation.values, - this.operation.payloads, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - ); + const tx = this.mock + .connect(this.proposer) + .scheduleBatch( + this.operation.targets, + this.operation.values, + this.operation.payloads, + this.operation.predecessor, + this.operation.salt, + MINDELAY, + ); for (const i in this.operation.targets) { - expectEvent(receipt, 'CallScheduled', { - id: this.operation.id, - index: web3.utils.toBN(i), - target: this.operation.targets[i], - value: web3.utils.toBN(this.operation.values[i]), - data: this.operation.payloads[i], - predecessor: this.operation.predecessor, - delay: MINDELAY, - }); - - expectEvent(receipt, 'CallSalt', { - id: this.operation.id, - salt: this.operation.salt, - }); + await expect(tx) + .to.emit(this.mock, 'CallScheduled') + .withArgs( + this.operation.id, + i, + getAddress(this.operation.targets[i]), + this.operation.values[i], + this.operation.payloads[i], + this.operation.predecessor, + MINDELAY, + ) + .to.emit(this.mock, 'CallSalt') + .withArgs(this.operation.id, this.operation.salt); } - const block = await web3.eth.getBlock(receipt.receipt.blockHash); - - expect(await this.mock.getTimestamp(this.operation.id)).to.be.bignumber.equal( - web3.utils.toBN(block.timestamp).add(MINDELAY), + expect(await this.mock.getTimestamp(this.operation.id)).to.equal( + (await time.clockFromReceipt.timestamp(tx)) + MINDELAY, ); }); it('prevent overwriting active operation', async function () { - await this.mock.scheduleBatch( - this.operation.targets, - this.operation.values, - this.operation.payloads, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - ); - - await expectRevertCustomError( - this.mock.scheduleBatch( + await this.mock + .connect(this.proposer) + .scheduleBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, MINDELAY, - { from: proposer }, - ), - 'TimelockUnexpectedOperationState', - [this.operation.id, proposalStatesToBitMap(OperationState.Unset)], - ); + ); + + await expect( + this.mock + .connect(this.proposer) + .scheduleBatch( + this.operation.targets, + this.operation.values, + this.operation.payloads, + this.operation.predecessor, + this.operation.salt, + MINDELAY, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Unset)); }); it('length of batch parameter must match #1', async function () { - await expectRevertCustomError( - this.mock.scheduleBatch( - this.operation.targets, - [], - this.operation.payloads, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - ), - 'TimelockInvalidOperationLength', - [this.operation.targets.length, this.operation.payloads.length, 0], - ); + await expect( + this.mock + .connect(this.proposer) + .scheduleBatch( + this.operation.targets, + [], + this.operation.payloads, + this.operation.predecessor, + this.operation.salt, + MINDELAY, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength') + .withArgs(this.operation.targets.length, this.operation.payloads.length, 0n); }); it('length of batch parameter must match #1', async function () { - await expectRevertCustomError( - this.mock.scheduleBatch( - this.operation.targets, - this.operation.values, - [], - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - ), - 'TimelockInvalidOperationLength', - [this.operation.targets.length, 0, this.operation.payloads.length], - ); + await expect( + this.mock + .connect(this.proposer) + .scheduleBatch( + this.operation.targets, + this.operation.values, + [], + this.operation.predecessor, + this.operation.salt, + MINDELAY, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength') + .withArgs(this.operation.targets.length, 0n, this.operation.payloads.length); }); it('prevent non-proposer from committing', async function () { - await expectRevertCustomError( - this.mock.scheduleBatch( - this.operation.targets, - this.operation.values, - this.operation.payloads, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: other }, - ), - `AccessControlUnauthorizedAccount`, - [other, PROPOSER_ROLE], - ); + await expect( + this.mock + .connect(this.other) + .scheduleBatch( + this.operation.targets, + this.operation.values, + this.operation.payloads, + this.operation.predecessor, + this.operation.salt, + MINDELAY, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount') + .withArgs(this.other.address, PROPOSER_ROLE); }); it('enforce minimum delay', async function () { - await expectRevertCustomError( - this.mock.scheduleBatch( - this.operation.targets, - this.operation.values, - this.operation.payloads, - this.operation.predecessor, - this.operation.salt, - MINDELAY - 1, - { from: proposer }, - ), - 'TimelockInsufficientDelay', - [MINDELAY, MINDELAY - 1], - ); + await expect( + this.mock + .connect(this.proposer) + .scheduleBatch( + this.operation.targets, + this.operation.values, + this.operation.payloads, + this.operation.predecessor, + this.operation.salt, + MINDELAY - 1n, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockInsufficientDelay') + .withArgs(MINDELAY - 1n, MINDELAY); }); }); @@ -579,236 +612,248 @@ contract('TimelockController', function (accounts) { beforeEach(async function () { this.operation = genOperationBatch( Array(8).fill('0x76E53CcEb05131Ef5248553bEBDb8F70536830b1'), - Array(8).fill(0), + Array(8).fill(0n), Array(8).fill('0x58a60f63'), - ZERO_BYTES32, + ethers.ZeroHash, '0x9545eeabc7a7586689191f78a5532443698538e54211b5bd4d7dc0fc0102b5c7', ); }); it('revert if operation is not scheduled', async function () { - await expectRevertCustomError( - this.mock.executeBatch( - this.operation.targets, - this.operation.values, - this.operation.payloads, - this.operation.predecessor, - this.operation.salt, - { from: executor }, - ), - 'TimelockUnexpectedOperationState', - [this.operation.id, proposalStatesToBitMap(OperationState.Ready)], - ); - }); - - describe('with scheduled operation', function () { - beforeEach(async function () { - ({ receipt: this.receipt, logs: this.logs } = await this.mock.scheduleBatch( - this.operation.targets, - this.operation.values, - this.operation.payloads, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - )); - }); - - it('revert if execution comes too early 1/2', async function () { - await expectRevertCustomError( - this.mock.executeBatch( + await expect( + this.mock + .connect(this.executor) + .executeBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, - { from: executor }, ), - 'TimelockUnexpectedOperationState', - [this.operation.id, proposalStatesToBitMap(OperationState.Ready)], - ); - }); - - it('revert if execution comes too early 2/2', async function () { - const timestamp = await this.mock.getTimestamp(this.operation.id); - await time.increaseTo(timestamp - 5); // -1 is to tight, test sometime fails - - await expectRevertCustomError( - this.mock.executeBatch( - this.operation.targets, - this.operation.values, - this.operation.payloads, - this.operation.predecessor, - this.operation.salt, - { from: executor }, - ), - 'TimelockUnexpectedOperationState', - [this.operation.id, proposalStatesToBitMap(OperationState.Ready)], - ); - }); - - describe('on time', function () { - beforeEach(async function () { - const timestamp = await this.mock.getTimestamp(this.operation.id); - await time.increaseTo(timestamp); - }); + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready)); + }); - it('executor can reveal', async function () { - const receipt = await this.mock.executeBatch( + describe('with scheduled operation', function () { + beforeEach(async function () { + await this.mock + .connect(this.proposer) + .scheduleBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, - { from: executor }, + MINDELAY, ); - for (const i in this.operation.targets) { - expectEvent(receipt, 'CallExecuted', { - id: this.operation.id, - index: web3.utils.toBN(i), - target: this.operation.targets[i], - value: web3.utils.toBN(this.operation.values[i]), - data: this.operation.payloads[i], - }); - } - }); + }); - it('prevent non-executor from revealing', async function () { - await expectRevertCustomError( - this.mock.executeBatch( + it('revert if execution comes too early 1/2', async function () { + await expect( + this.mock + .connect(this.executor) + .executeBatch( this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, - { from: other }, ), - `AccessControlUnauthorizedAccount`, - [other, EXECUTOR_ROLE], - ); - }); + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready)); + }); - it('length mismatch #1', async function () { - await expectRevertCustomError( - this.mock.executeBatch( - [], + it('revert if execution comes too early 2/2', async function () { + // -1 is to tight, test sometime fails + await this.mock.getTimestamp(this.operation.id).then(clock => time.forward.timestamp(clock - 5n)); + + await expect( + this.mock + .connect(this.executor) + .executeBatch( + this.operation.targets, this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, - { from: executor }, ), - 'TimelockInvalidOperationLength', - [0, this.operation.payloads.length, this.operation.values.length], - ); + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(this.operation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready)); + }); + + describe('on time', function () { + beforeEach(async function () { + await this.mock.getTimestamp(this.operation.id).then(clock => time.forward.timestamp(clock)); }); - it('length mismatch #2', async function () { - await expectRevertCustomError( - this.mock.executeBatch( + it('executor can reveal', async function () { + const tx = this.mock + .connect(this.executor) + .executeBatch( this.operation.targets, - [], + this.operation.values, this.operation.payloads, this.operation.predecessor, this.operation.salt, - { from: executor }, - ), - 'TimelockInvalidOperationLength', - [this.operation.targets.length, this.operation.payloads.length, 0], - ); + ); + for (const i in this.operation.targets) { + expect(tx) + .to.emit(this.mock, 'CallExecuted') + .withArgs( + this.operation.id, + i, + this.operation.targets[i], + this.operation.values[i], + this.operation.payloads[i], + ); + } + }); + + it('prevent non-executor from revealing', async function () { + await expect( + this.mock + .connect(this.other) + .executeBatch( + this.operation.targets, + this.operation.values, + this.operation.payloads, + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount') + .withArgs(this.other.address, EXECUTOR_ROLE); + }); + + it('length mismatch #1', async function () { + await expect( + this.mock + .connect(this.executor) + .executeBatch( + [], + this.operation.values, + this.operation.payloads, + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength') + .withArgs(0, this.operation.payloads.length, this.operation.values.length); + }); + + it('length mismatch #2', async function () { + await expect( + this.mock + .connect(this.executor) + .executeBatch( + this.operation.targets, + [], + this.operation.payloads, + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength') + .withArgs(this.operation.targets.length, this.operation.payloads.length, 0n); }); it('length mismatch #3', async function () { - await expectRevertCustomError( - this.mock.executeBatch( - this.operation.targets, - this.operation.values, - [], - this.operation.predecessor, - this.operation.salt, - { from: executor }, - ), - 'TimelockInvalidOperationLength', - [this.operation.targets.length, 0, this.operation.values.length], - ); + await expect( + this.mock + .connect(this.executor) + .executeBatch( + this.operation.targets, + this.operation.values, + [], + this.operation.predecessor, + this.operation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockInvalidOperationLength') + .withArgs(this.operation.targets.length, 0n, this.operation.values.length); }); it('prevents reentrancy execution', async function () { // Create operation - const reentrant = await TimelockReentrant.new(); + const reentrant = await ethers.deployContract('$TimelockReentrant'); const reentrantBatchOperation = genOperationBatch( - [reentrant.address], - [0], - [reentrant.contract.methods.reenter().encodeABI()], - ZERO_BYTES32, + [reentrant], + [0n], + [reentrant.interface.encodeFunctionData('reenter')], + ethers.ZeroHash, salt, ); // Schedule so it can be executed - await this.mock.scheduleBatch( - reentrantBatchOperation.targets, - reentrantBatchOperation.values, - reentrantBatchOperation.payloads, - reentrantBatchOperation.predecessor, - reentrantBatchOperation.salt, - MINDELAY, - { from: proposer }, - ); + await this.mock + .connect(this.proposer) + .scheduleBatch( + reentrantBatchOperation.targets, + reentrantBatchOperation.values, + reentrantBatchOperation.payloads, + reentrantBatchOperation.predecessor, + reentrantBatchOperation.salt, + MINDELAY, + ); // Advance on time to make the operation executable - const timestamp = await this.mock.getTimestamp(reentrantBatchOperation.id); - await time.increaseTo(timestamp); + await this.mock.getTimestamp(reentrantBatchOperation.id).then(clock => time.forward.timestamp(clock)); // Grant executor role to the reentrant contract - await this.mock.grantRole(EXECUTOR_ROLE, reentrant.address, { from: admin }); + await this.mock.connect(this.admin).grantRole(EXECUTOR_ROLE, reentrant); // Prepare reenter - const data = this.mock.contract.methods - .executeBatch( - reentrantBatchOperation.targets, - reentrantBatchOperation.values, - reentrantBatchOperation.payloads, - reentrantBatchOperation.predecessor, - reentrantBatchOperation.salt, - ) - .encodeABI(); - await reentrant.enableRentrancy(this.mock.address, data); + const data = this.mock.interface.encodeFunctionData('executeBatch', [ + reentrantBatchOperation.targets.map(getAddress), + reentrantBatchOperation.values, + reentrantBatchOperation.payloads, + reentrantBatchOperation.predecessor, + reentrantBatchOperation.salt, + ]); + await reentrant.enableRentrancy(this.mock, data); // Expect to fail - await expectRevertCustomError( - this.mock.executeBatch( - reentrantBatchOperation.targets, - reentrantBatchOperation.values, - reentrantBatchOperation.payloads, - reentrantBatchOperation.predecessor, - reentrantBatchOperation.salt, - { from: executor }, - ), - 'TimelockUnexpectedOperationState', - [reentrantBatchOperation.id, proposalStatesToBitMap(OperationState.Ready)], - ); + await expect( + this.mock + .connect(this.executor) + .executeBatch( + reentrantBatchOperation.targets, + reentrantBatchOperation.values, + reentrantBatchOperation.payloads, + reentrantBatchOperation.predecessor, + reentrantBatchOperation.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs(reentrantBatchOperation.id, GovernorHelper.proposalStatesToBitMap(OperationState.Ready)); // Disable reentrancy await reentrant.disableReentrancy(); const nonReentrantBatchOperation = reentrantBatchOperation; // Not anymore // Try again successfully - const receipt = await this.mock.executeBatch( - nonReentrantBatchOperation.targets, - nonReentrantBatchOperation.values, - nonReentrantBatchOperation.payloads, - nonReentrantBatchOperation.predecessor, - nonReentrantBatchOperation.salt, - { from: executor }, - ); + const tx = this.mock + .connect(this.executor) + .executeBatch( + nonReentrantBatchOperation.targets, + nonReentrantBatchOperation.values, + nonReentrantBatchOperation.payloads, + nonReentrantBatchOperation.predecessor, + nonReentrantBatchOperation.salt, + ); for (const i in nonReentrantBatchOperation.targets) { - expectEvent(receipt, 'CallExecuted', { - id: nonReentrantBatchOperation.id, - index: web3.utils.toBN(i), - target: nonReentrantBatchOperation.targets[i], - value: web3.utils.toBN(nonReentrantBatchOperation.values[i]), - data: nonReentrantBatchOperation.payloads[i], - }); + expect(tx) + .to.emit(this.mock, 'CallExecuted') + .withArgs( + nonReentrantBatchOperation.id, + i, + nonReentrantBatchOperation.targets[i], + nonReentrantBatchOperation.values[i], + nonReentrantBatchOperation.payloads[i], + ); } }); }); @@ -816,39 +861,41 @@ contract('TimelockController', function (accounts) { it('partial execution', async function () { const operation = genOperationBatch( - [this.callreceivermock.address, this.callreceivermock.address, this.callreceivermock.address], - [0, 0, 0], + [this.callreceivermock, this.callreceivermock, this.callreceivermock], + [0n, 0n, 0n], [ - this.callreceivermock.contract.methods.mockFunction().encodeABI(), - this.callreceivermock.contract.methods.mockFunctionRevertsNoReason().encodeABI(), - this.callreceivermock.contract.methods.mockFunction().encodeABI(), + this.callreceivermock.interface.encodeFunctionData('mockFunction'), + this.callreceivermock.interface.encodeFunctionData('mockFunctionRevertsNoReason'), + this.callreceivermock.interface.encodeFunctionData('mockFunction'), ], - ZERO_BYTES32, + ethers.ZeroHash, '0x8ac04aa0d6d66b8812fb41d39638d37af0a9ab11da507afd65c509f8ed079d3e', ); - await this.mock.scheduleBatch( - operation.targets, - operation.values, - operation.payloads, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); - await expectRevertCustomError( - this.mock.executeBatch( + await this.mock + .connect(this.proposer) + .scheduleBatch( operation.targets, operation.values, operation.payloads, operation.predecessor, operation.salt, - { from: executor }, - ), - 'FailedInnerCall', - [], - ); + MINDELAY, + ); + + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + + await expect( + this.mock + .connect(this.executor) + .executeBatch( + operation.targets, + operation.values, + operation.payloads, + operation.predecessor, + operation.salt, + ), + ).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); }); }); }); @@ -857,81 +904,78 @@ contract('TimelockController', function (accounts) { beforeEach(async function () { this.operation = genOperation( '0xC6837c44AA376dbe1d2709F13879E040CAb653ca', - 0, + 0n, '0x296e58dd', - ZERO_BYTES32, + ethers.ZeroHash, '0xa2485763600634800df9fc9646fb2c112cf98649c55f63dd1d9c7d13a64399d9', ); - ({ receipt: this.receipt, logs: this.logs } = await this.mock.schedule( - this.operation.target, - this.operation.value, - this.operation.data, - this.operation.predecessor, - this.operation.salt, - MINDELAY, - { from: proposer }, - )); + await this.mock + .connect(this.proposer) + .schedule( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + this.operation.salt, + MINDELAY, + ); }); it('canceller can cancel', async function () { - const receipt = await this.mock.cancel(this.operation.id, { from: canceller }); - expectEvent(receipt, 'Cancelled', { id: this.operation.id }); + await expect(this.mock.connect(this.canceller).cancel(this.operation.id)) + .to.emit(this.mock, 'Cancelled') + .withArgs(this.operation.id); }); it('cannot cancel invalid operation', async function () { - await expectRevertCustomError( - this.mock.cancel(constants.ZERO_BYTES32, { from: canceller }), - 'TimelockUnexpectedOperationState', - [constants.ZERO_BYTES32, proposalStatesToBitMap([OperationState.Waiting, OperationState.Ready])], - ); + await expect(this.mock.connect(this.canceller).cancel(ethers.ZeroHash)) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexpectedOperationState') + .withArgs( + ethers.ZeroHash, + GovernorHelper.proposalStatesToBitMap([OperationState.Waiting, OperationState.Ready]), + ); }); it('prevent non-canceller from canceling', async function () { - await expectRevertCustomError( - this.mock.cancel(this.operation.id, { from: other }), - `AccessControlUnauthorizedAccount`, - [other, CANCELLER_ROLE], - ); + await expect(this.mock.connect(this.other).cancel(this.operation.id)) + .to.be.revertedWithCustomError(this.mock, 'AccessControlUnauthorizedAccount') + .withArgs(this.other.address, CANCELLER_ROLE); }); }); }); describe('maintenance', function () { it('prevent unauthorized maintenance', async function () { - await expectRevertCustomError(this.mock.updateDelay(0, { from: other }), 'TimelockUnauthorizedCaller', [other]); + await expect(this.mock.connect(this.other).updateDelay(0n)) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnauthorizedCaller') + .withArgs(this.other.address); }); it('timelock scheduled maintenance', async function () { const newDelay = time.duration.hours(6); const operation = genOperation( - this.mock.address, - 0, - this.mock.contract.methods.updateDelay(newDelay.toString()).encodeABI(), - ZERO_BYTES32, + this.mock, + 0n, + this.mock.interface.encodeFunctionData('updateDelay', [newDelay]), + ethers.ZeroHash, '0xf8e775b2c5f4d66fb5c7fa800f35ef518c262b6014b3c0aee6ea21bff157f108', ); - await this.mock.schedule( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); - const receipt = await this.mock.execute( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - { from: executor }, - ); - expectEvent(receipt, 'MinDelayChange', { newDuration: newDelay.toString(), oldDuration: MINDELAY }); + await this.mock + .connect(this.proposer) + .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); - expect(await this.mock.getMinDelay()).to.be.bignumber.equal(newDelay); + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + + await expect( + this.mock + .connect(this.executor) + .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt), + ) + .to.emit(this.mock, 'MinDelayChange') + .withArgs(MINDELAY, newDelay); + + expect(await this.mock.getMinDelay()).to.equal(newDelay); }); }); @@ -939,71 +983,77 @@ contract('TimelockController', function (accounts) { beforeEach(async function () { this.operation1 = genOperation( '0xdE66bD4c97304200A95aE0AadA32d6d01A867E39', - 0, + 0n, '0x01dc731a', - ZERO_BYTES32, + ethers.ZeroHash, '0x64e932133c7677402ead2926f86205e2ca4686aebecf5a8077627092b9bb2feb', ); this.operation2 = genOperation( '0x3c7944a3F1ee7fc8c5A5134ba7c79D11c3A1FCa3', - 0, + 0n, '0x8f531849', this.operation1.id, '0x036e1311cac523f9548e6461e29fb1f8f9196b91910a41711ea22f5de48df07d', ); - await this.mock.schedule( - this.operation1.target, - this.operation1.value, - this.operation1.data, - this.operation1.predecessor, - this.operation1.salt, - MINDELAY, - { from: proposer }, - ); - await this.mock.schedule( - this.operation2.target, - this.operation2.value, - this.operation2.data, - this.operation2.predecessor, - this.operation2.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); - }); - - it('cannot execute before dependency', async function () { - await expectRevertCustomError( - this.mock.execute( + await this.mock + .connect(this.proposer) + .schedule( + this.operation1.target, + this.operation1.value, + this.operation1.data, + this.operation1.predecessor, + this.operation1.salt, + MINDELAY, + ); + await this.mock + .connect(this.proposer) + .schedule( this.operation2.target, this.operation2.value, this.operation2.data, this.operation2.predecessor, this.operation2.salt, - { from: executor }, - ), - 'TimelockUnexecutedPredecessor', - [this.operation1.id], - ); + MINDELAY, + ); + + await this.mock.getTimestamp(this.operation2.id).then(clock => time.forward.timestamp(clock)); + }); + + it('cannot execute before dependency', async function () { + await expect( + this.mock + .connect(this.executor) + .execute( + this.operation2.target, + this.operation2.value, + this.operation2.data, + this.operation2.predecessor, + this.operation2.salt, + ), + ) + .to.be.revertedWithCustomError(this.mock, 'TimelockUnexecutedPredecessor') + .withArgs(this.operation1.id); }); it('can execute after dependency', async function () { - await this.mock.execute( - this.operation1.target, - this.operation1.value, - this.operation1.data, - this.operation1.predecessor, - this.operation1.salt, - { from: executor }, - ); - await this.mock.execute( - this.operation2.target, - this.operation2.value, - this.operation2.data, - this.operation2.predecessor, - this.operation2.salt, - { from: executor }, - ); + await this.mock + .connect(this.executor) + .execute( + this.operation1.target, + this.operation1.value, + this.operation1.data, + this.operation1.predecessor, + this.operation1.salt, + ); + await this.mock + .connect(this.executor) + .execute( + this.operation2.target, + this.operation2.value, + this.operation2.data, + this.operation2.predecessor, + this.operation2.salt, + ); }); }); @@ -1012,274 +1062,219 @@ contract('TimelockController', function (accounts) { it('call', async function () { const operation = genOperation( - this.implementation2.address, - 0, - this.implementation2.contract.methods.setValue(42).encodeABI(), - ZERO_BYTES32, + this.implementation2, + 0n, + this.implementation2.interface.encodeFunctionData('setValue', [42n]), + ethers.ZeroHash, '0x8043596363daefc89977b25f9d9b4d06c3910959ef0c4d213557a903e1b555e2', ); - await this.mock.schedule( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); - await this.mock.execute( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - { from: executor }, - ); + await this.mock + .connect(this.proposer) + .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); - expect(await this.implementation2.getValue()).to.be.bignumber.equal(web3.utils.toBN(42)); + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + + await this.mock + .connect(this.executor) + .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt); + + expect(await this.implementation2.getValue()).to.equal(42n); }); it('call reverting', async function () { const operation = genOperation( - this.callreceivermock.address, - 0, - this.callreceivermock.contract.methods.mockFunctionRevertsNoReason().encodeABI(), - ZERO_BYTES32, + this.callreceivermock, + 0n, + this.callreceivermock.interface.encodeFunctionData('mockFunctionRevertsNoReason'), + ethers.ZeroHash, '0xb1b1b276fdf1a28d1e00537ea73b04d56639128b08063c1a2f70a52e38cba693', ); - await this.mock.schedule( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); - await expectRevertCustomError( - this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { - from: executor, - }), - 'FailedInnerCall', - [], - ); + await this.mock + .connect(this.proposer) + .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); + + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + + await expect( + this.mock + .connect(this.executor) + .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt), + ).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); }); it('call throw', async function () { const operation = genOperation( - this.callreceivermock.address, - 0, - this.callreceivermock.contract.methods.mockFunctionThrows().encodeABI(), - ZERO_BYTES32, + this.callreceivermock, + 0n, + this.callreceivermock.interface.encodeFunctionData('mockFunctionThrows'), + ethers.ZeroHash, '0xe5ca79f295fc8327ee8a765fe19afb58f4a0cbc5053642bfdd7e73bc68e0fc67', ); - await this.mock.schedule( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); + await this.mock + .connect(this.proposer) + .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); + + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + // Targeted function reverts with a panic code (0x1) + the timelock bubble the panic code - await expectRevert.unspecified( - this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { - from: executor, - }), - ); + await expect( + this.mock + .connect(this.executor) + .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt), + ).to.be.revertedWithPanic(PANIC_CODES.ASSERTION_ERROR); }); it('call out of gas', async function () { const operation = genOperation( - this.callreceivermock.address, - 0, - this.callreceivermock.contract.methods.mockFunctionOutOfGas().encodeABI(), - ZERO_BYTES32, + this.callreceivermock, + 0n, + this.callreceivermock.interface.encodeFunctionData('mockFunctionOutOfGas'), + ethers.ZeroHash, '0xf3274ce7c394c5b629d5215723563a744b817e1730cca5587c567099a14578fd', ); - await this.mock.schedule( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); - await expectRevertCustomError( - this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { - from: executor, - gas: '100000', - }), - 'FailedInnerCall', - [], - ); + await this.mock + .connect(this.proposer) + .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); + + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + + await expect( + this.mock + .connect(this.executor) + .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { + gasLimit: '100000', + }), + ).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); }); it('call payable with eth', async function () { const operation = genOperation( - this.callreceivermock.address, + this.callreceivermock, 1, - this.callreceivermock.contract.methods.mockFunction().encodeABI(), - ZERO_BYTES32, + this.callreceivermock.interface.encodeFunctionData('mockFunction'), + ethers.ZeroHash, '0x5ab73cd33477dcd36c1e05e28362719d0ed59a7b9ff14939de63a43073dc1f44', ); - await this.mock.schedule( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); - - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - - await this.mock.execute( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - { from: executor, value: 1 }, - ); + await this.mock + .connect(this.proposer) + .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); + + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(1)); + await this.mock + .connect(this.executor) + .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { + value: 1, + }); + + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(1n); }); it('call nonpayable with eth', async function () { const operation = genOperation( - this.callreceivermock.address, + this.callreceivermock, 1, - this.callreceivermock.contract.methods.mockFunctionNonPayable().encodeABI(), - ZERO_BYTES32, + this.callreceivermock.interface.encodeFunctionData('mockFunctionNonPayable'), + ethers.ZeroHash, '0xb78edbd920c7867f187e5aa6294ae5a656cfbf0dea1ccdca3751b740d0f2bdf8', ); - await this.mock.schedule( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); + await this.mock + .connect(this.proposer) + .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); - await expectRevertCustomError( - this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { - from: executor, - }), - 'FailedInnerCall', - [], - ); + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n); + + await expect( + this.mock + .connect(this.executor) + .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt), + ).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n); }); it('call reverting with eth', async function () { const operation = genOperation( - this.callreceivermock.address, + this.callreceivermock, 1, - this.callreceivermock.contract.methods.mockFunctionRevertsNoReason().encodeABI(), - ZERO_BYTES32, + this.callreceivermock.interface.encodeFunctionData('mockFunctionRevertsNoReason'), + ethers.ZeroHash, '0xdedb4563ef0095db01d81d3f2decf57cf83e4a72aa792af14c43a792b56f4de6', ); - await this.mock.schedule( - operation.target, - operation.value, - operation.data, - operation.predecessor, - operation.salt, - MINDELAY, - { from: proposer }, - ); - await time.increase(MINDELAY); + await this.mock + .connect(this.proposer) + .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); + await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); - await expectRevertCustomError( - this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { - from: executor, - }), - 'FailedInnerCall', - [], - ); + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); + await expect( + this.mock + .connect(this.executor) + .execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt), + ).to.be.revertedWithCustomError(this.mock, 'FailedInnerCall'); + + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n); }); }); describe('safe receive', function () { describe('ERC721', function () { - const name = 'Non Fungible Token'; - const symbol = 'NFT'; - const tokenId = new BN(1); + const tokenId = 1n; beforeEach(async function () { - this.token = await ERC721.new(name, symbol); - await this.token.$_mint(other, tokenId); + this.token = await ethers.deployContract('$ERC721', ['Non Fungible Token', 'NFT']); + await this.token.$_mint(this.other, tokenId); }); it('can receive an ERC721 safeTransfer', async function () { - await this.token.safeTransferFrom(other, this.mock.address, tokenId, { from: other }); + await this.token.connect(this.other).safeTransferFrom(this.other, this.mock, tokenId); }); }); describe('ERC1155', function () { - const uri = 'https://token-cdn-domain/{id}.json'; const tokenIds = { - 1: new BN(1000), - 2: new BN(2000), - 3: new BN(3000), + 1: 1000n, + 2: 2000n, + 3: 3000n, }; beforeEach(async function () { - this.token = await ERC1155.new(uri); - await this.token.$_mintBatch(other, Object.keys(tokenIds), Object.values(tokenIds), '0x'); + this.token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']); + await this.token.$_mintBatch(this.other, Object.keys(tokenIds), Object.values(tokenIds), '0x'); }); it('can receive ERC1155 safeTransfer', async function () { - await this.token.safeTransferFrom( - other, - this.mock.address, - ...Object.entries(tokenIds)[0], // id + amount + await this.token.connect(this.other).safeTransferFrom( + this.other, + this.mock, + ...Object.entries(tokenIds)[0n], // id + amount '0x', - { from: other }, ); }); it('can receive ERC1155 safeBatchTransfer', async function () { - await this.token.safeBatchTransferFrom( - other, - this.mock.address, - Object.keys(tokenIds), - Object.values(tokenIds), - '0x', - { from: other }, - ); + await this.token + .connect(this.other) + .safeBatchTransferFrom(this.other, this.mock, Object.keys(tokenIds), Object.values(tokenIds), '0x'); }); }); }); diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 22265cc25fe..0d2a33c7345 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -1,59 +1,77 @@ -const { expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const Enums = require('../../helpers/enums'); const { GovernorHelper } = require('../../helpers/governance'); - -const Governor = artifacts.require('$GovernorVoteMocks'); -const CallReceiver = artifacts.require('CallReceiverMock'); +const { bigint: Enums } = require('../../helpers/enums'); const TOKENS = [ - { Token: artifacts.require('$ERC721Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC721VotesTimestampMock'), mode: 'timestamp' }, + { Token: '$ERC721Votes', mode: 'blocknumber' }, + { Token: '$ERC721VotesTimestampMock', mode: 'timestamp' }, ]; -contract('GovernorERC721', function (accounts) { - const [owner, voter1, voter2, voter3, voter4] = accounts; - - const name = 'OZ-Governor'; - const version = '1'; - const tokenName = 'MockNFToken'; - const tokenSymbol = 'MTKN'; - const NFT0 = web3.utils.toBN(0); - const NFT1 = web3.utils.toBN(1); - const NFT2 = web3.utils.toBN(2); - const NFT3 = web3.utils.toBN(3); - const NFT4 = web3.utils.toBN(4); - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockNFToken'; +const tokenSymbol = 'MTKN'; +const NFT0 = 0n; +const NFT1 = 1n; +const NFT2 = 2n; +const NFT3 = 3n; +const NFT4 = 4n; +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); + +describe('GovernorERC721', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [owner, voter1, voter2, voter3, voter4] = await ethers.getSigners(); + const receiver = await ethers.deployContract('CallReceiverMock'); + + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const mock = await ethers.deployContract('$GovernorMock', [ + name, // name + votingDelay, // initialVotingDelay + votingPeriod, // initialVotingPeriod + 0n, // initialProposalThreshold + token, // tokenAddress + 10n, // quorumNumeratorValue + ]); + + await owner.sendTransaction({ to: mock, value }); + await Promise.all([NFT0, NFT1, NFT2, NFT3, NFT4].map(tokenId => token.$_mint(owner, tokenId))); + + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token, to: voter1, tokenId: NFT0 }); + await helper.connect(owner).delegate({ token, to: voter2, tokenId: NFT1 }); + await helper.connect(owner).delegate({ token, to: voter2, tokenId: NFT2 }); + await helper.connect(owner).delegate({ token, to: voter3, tokenId: NFT3 }); + await helper.connect(owner).delegate({ token, to: voter4, tokenId: NFT4 }); + + return { + owner, + voter1, + voter2, + voter3, + voter4, + receiver, + token, + mock, + helper, + }; + }; + + describe(`using ${Token}`, function () { beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.mock = await Governor.new(name, this.token.address); - this.receiver = await CallReceiver.new(); - - this.helper = new GovernorHelper(this.mock, mode); - - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); - - await Promise.all([NFT0, NFT1, NFT2, NFT3, NFT4].map(tokenId => this.token.$_mint(owner, tokenId))); - await this.helper.delegate({ token: this.token, to: voter1, tokenId: NFT0 }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT1 }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT2 }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, tokenId: NFT3 }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, tokenId: NFT4 }, { from: owner }); - - // default proposal + Object.assign(this, await loadFixture(fixture)); + // initiate fresh proposal this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunction'), value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), }, ], '', @@ -61,55 +79,52 @@ contract('GovernorERC721', function (accounts) { }); it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + expect(await this.mock.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token.target); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + expect(await this.mock.quorum(0n)).to.equal(0n); + + expect(await this.token.getVotes(this.voter1)).to.equal(1n); // NFT0 + expect(await this.token.getVotes(this.voter2)).to.equal(2n); // NFT1 & NFT2 + expect(await this.token.getVotes(this.voter3)).to.equal(1n); // NFT3 + expect(await this.token.getVotes(this.voter4)).to.equal(1n); // NFT4 }); it('voting with ERC721 token', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), 'VoteCast', { - voter: voter1, - support: Enums.VoteType.For, - weight: '1', - }); - - expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', { - voter: voter2, - support: Enums.VoteType.For, - weight: '2', - }); - - expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', { - voter: voter3, - support: Enums.VoteType.Against, - weight: '1', - }); - - expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', { - voter: voter4, - support: Enums.VoteType.Abstain, - weight: '1', - }); + await expect(this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter1.address, this.proposal.id, Enums.VoteType.For, 1n, ''); + + await expect(this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter2.address, this.proposal.id, Enums.VoteType.For, 2n, ''); + + await expect(this.helper.connect(this.voter3).vote({ support: Enums.VoteType.Against })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter3.address, this.proposal.id, Enums.VoteType.Against, 1n, ''); + + await expect(this.helper.connect(this.voter4).vote({ support: Enums.VoteType.Abstain })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter4.address, this.proposal.id, Enums.VoteType.Abstain, 1n, ''); await this.helper.waitForDeadline(); await this.helper.execute(); - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); - - await this.mock.proposalVotes(this.proposal.id).then(results => { - expect(results.forVotes).to.be.bignumber.equal('3'); - expect(results.againstVotes).to.be.bignumber.equal('1'); - expect(results.abstainVotes).to.be.bignumber.equal('1'); - }); + expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter3)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter4)).to.be.true; + + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([ + 1n, // againstVotes + 3n, // forVotes + 1n, // abstainVotes + ]); }); }); } diff --git a/test/governance/extensions/GovernorPreventLateQuorum.test.js b/test/governance/extensions/GovernorPreventLateQuorum.test.js index 17ae05a73fb..8defa70144c 100644 --- a/test/governance/extensions/GovernorPreventLateQuorum.test.js +++ b/test/governance/extensions/GovernorPreventLateQuorum.test.js @@ -1,66 +1,66 @@ -const { expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const Enums = require('../../helpers/enums'); const { GovernorHelper } = require('../../helpers/governance'); -const { clockFromReceipt } = require('../../helpers/time'); -const { expectRevertCustomError } = require('../../helpers/customError'); - -const Governor = artifacts.require('$GovernorPreventLateQuorumMock'); -const CallReceiver = artifacts.require('CallReceiverMock'); +const { bigint: Enums } = require('../../helpers/enums'); +const { bigint: time } = require('../../helpers/time'); const TOKENS = [ - { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, ]; -contract('GovernorPreventLateQuorum', function (accounts) { - const [owner, proposer, voter1, voter2, voter3, voter4] = accounts; - - const name = 'OZ-Governor'; - const version = '1'; - const tokenName = 'MockToken'; - const tokenSymbol = 'MTKN'; - const tokenSupply = web3.utils.toWei('100'); - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const lateQuorumVoteExtension = web3.utils.toBN(8); - const quorum = web3.utils.toWei('1'); - const value = web3.utils.toWei('1'); - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const votingDelay = 4n; +const votingPeriod = 16n; +const lateQuorumVoteExtension = 8n; +const quorum = ethers.parseEther('1'); +const value = ethers.parseEther('1'); + +describe('GovernorPreventLateQuorum', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [owner, proposer, voter1, voter2, voter3, voter4] = await ethers.getSigners(); + const receiver = await ethers.deployContract('CallReceiverMock'); + + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const mock = await ethers.deployContract('$GovernorPreventLateQuorumMock', [ + name, // name + votingDelay, // initialVotingDelay + votingPeriod, // initialVotingPeriod + 0n, // initialProposalThreshold + token, // tokenAddress + lateQuorumVoteExtension, + quorum, + ]); + + await owner.sendTransaction({ to: mock, value }); + await token.$_mint(owner, tokenSupply); + + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') }); + + return { owner, proposer, voter1, voter2, voter3, voter4, receiver, token, mock, helper }; + }; + + describe(`using ${Token}`, function () { beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.mock = await Governor.new( - name, - votingDelay, - votingPeriod, - 0, - this.token.address, - lateQuorumVoteExtension, - quorum, - ); - this.receiver = await CallReceiver.new(); - - this.helper = new GovernorHelper(this.mock, mode); - - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); - - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - - // default proposal + Object.assign(this, await loadFixture(fixture)); + // initiate fresh proposal this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunction'), value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), }, ], '', @@ -68,110 +68,101 @@ contract('GovernorPreventLateQuorum', function (accounts) { }); it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal(quorum); - expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal(lateQuorumVoteExtension); + expect(await this.mock.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token.target); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.equal(quorum); + expect(await this.mock.lateQuorumVoteExtension()).to.equal(lateQuorumVoteExtension); }); it('nominal workflow unaffected', async function () { - const txPropose = await this.helper.propose({ from: proposer }); + const txPropose = await this.helper.connect(this.proposer).propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter3).vote({ support: Enums.VoteType.Against }); + await this.helper.connect(this.voter4).vote({ support: Enums.VoteType.Abstain }); await this.helper.waitForDeadline(); await this.helper.execute(); - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); - - await this.mock.proposalVotes(this.proposal.id).then(results => { - expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17')); - expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5')); - expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2')); - }); - - const voteStart = web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay); - const voteEnd = web3.utils - .toBN(await clockFromReceipt[mode](txPropose.receipt)) - .add(votingDelay) - .add(votingPeriod); - expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(voteStart); - expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(voteEnd); - - expectEvent(txPropose, 'ProposalCreated', { - proposalId: this.proposal.id, - proposer, - targets: this.proposal.targets, - // values: this.proposal.values.map(value => web3.utils.toBN(value)), - signatures: this.proposal.signatures, - calldatas: this.proposal.data, - voteStart, - voteEnd, - description: this.proposal.description, - }); + expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter3)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter4)).to.be.true; + + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([ + ethers.parseEther('5'), // againstVotes + ethers.parseEther('17'), // forVotes + ethers.parseEther('2'), // abstainVotes + ]); + + const voteStart = (await time.clockFromReceipt[mode](txPropose)) + votingDelay; + const voteEnd = (await time.clockFromReceipt[mode](txPropose)) + votingDelay + votingPeriod; + expect(await this.mock.proposalSnapshot(this.proposal.id)).to.equal(voteStart); + expect(await this.mock.proposalDeadline(this.proposal.id)).to.equal(voteEnd); + + await expect(txPropose) + .to.emit(this.mock, 'ProposalCreated') + .withArgs( + this.proposal.id, + this.proposer.address, + this.proposal.targets, + this.proposal.values, + this.proposal.signatures, + this.proposal.data, + voteStart, + voteEnd, + this.proposal.description, + ); }); it('Delay is extended to prevent last minute take-over', async function () { - const txPropose = await this.helper.propose({ from: proposer }); + const txPropose = await this.helper.connect(this.proposer).propose(); // compute original schedule - const startBlock = web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay); - const endBlock = web3.utils - .toBN(await clockFromReceipt[mode](txPropose.receipt)) - .add(votingDelay) - .add(votingPeriod); - expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock); - expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(endBlock); - + const snapshotTimepoint = (await time.clockFromReceipt[mode](txPropose)) + votingDelay; + const deadlineTimepoint = (await time.clockFromReceipt[mode](txPropose)) + votingDelay + votingPeriod; + expect(await this.mock.proposalSnapshot(this.proposal.id)).to.equal(snapshotTimepoint); + expect(await this.mock.proposalDeadline(this.proposal.id)).to.equal(deadlineTimepoint); // wait for the last minute to vote - await this.helper.waitForDeadline(-1); - const txVote = await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.waitForDeadline(-1n); + const txVote = await this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For }); // cannot execute yet - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Active); // compute new extended schedule - const extendedDeadline = web3.utils - .toBN(await clockFromReceipt[mode](txVote.receipt)) - .add(lateQuorumVoteExtension); - expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock); - expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(extendedDeadline); + const extendedDeadline = (await time.clockFromReceipt[mode](txVote)) + lateQuorumVoteExtension; + expect(await this.mock.proposalSnapshot(this.proposal.id)).to.equal(snapshotTimepoint); + expect(await this.mock.proposalDeadline(this.proposal.id)).to.equal(extendedDeadline); // still possible to vote - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.Against }); await this.helper.waitForDeadline(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); - await this.helper.waitForDeadline(+1); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Active); + await this.helper.waitForDeadline(1n); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Defeated); // check extension event - expectEvent(txVote, 'ProposalExtended', { proposalId: this.proposal.id, extendedDeadline }); + await expect(txVote).to.emit(this.mock, 'ProposalExtended').withArgs(this.proposal.id, extendedDeadline); }); describe('onlyGovernance updates', function () { it('setLateQuorumVoteExtension is protected', async function () { - await expectRevertCustomError( - this.mock.setLateQuorumVoteExtension(0, { from: owner }), - 'GovernorOnlyExecutor', - [owner], - ); + await expect(this.mock.connect(this.owner).setLateQuorumVoteExtension(0n)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); it('can setLateQuorumVoteExtension through governance', async function () { this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.setLateQuorumVoteExtension('0').encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setLateQuorumVoteExtension', [0n]), }, ], '', @@ -179,15 +170,14 @@ contract('GovernorPreventLateQuorum', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - expectEvent(await this.helper.execute(), 'LateQuorumVoteExtensionSet', { - oldVoteExtension: lateQuorumVoteExtension, - newVoteExtension: '0', - }); + await expect(this.helper.execute()) + .to.emit(this.mock, 'LateQuorumVoteExtensionSet') + .withArgs(lateQuorumVoteExtension, 0n); - expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal('0'); + expect(await this.mock.lateQuorumVoteExtension()).to.equal(0n); }); }); }); diff --git a/test/governance/extensions/GovernorStorage.test.js b/test/governance/extensions/GovernorStorage.test.js index 99a97886c37..911c3244076 100644 --- a/test/governance/extensions/GovernorStorage.test.js +++ b/test/governance/extensions/GovernorStorage.test.js @@ -1,149 +1,154 @@ -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const Enums = require('../../helpers/enums'); const { GovernorHelper, timelockSalt } = require('../../helpers/governance'); - -const Timelock = artifacts.require('TimelockController'); -const Governor = artifacts.require('$GovernorStorageMock'); -const CallReceiver = artifacts.require('CallReceiverMock'); +const { bigint: Enums } = require('../../helpers/enums'); const TOKENS = [ - { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, ]; -contract('GovernorStorage', function (accounts) { - const [owner, voter1, voter2, voter3, voter4] = accounts; - - const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000'; - const PROPOSER_ROLE = web3.utils.soliditySha3('PROPOSER_ROLE'); - const EXECUTOR_ROLE = web3.utils.soliditySha3('EXECUTOR_ROLE'); - const CANCELLER_ROLE = web3.utils.soliditySha3('CANCELLER_ROLE'); - - const name = 'OZ-Governor'; - const version = '1'; - const tokenName = 'MockToken'; - const tokenSymbol = 'MTKN'; - const tokenSupply = web3.utils.toWei('100'); - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { +const DEFAULT_ADMIN_ROLE = ethers.ZeroHash; +const PROPOSER_ROLE = ethers.id('PROPOSER_ROLE'); +const EXECUTOR_ROLE = ethers.id('EXECUTOR_ROLE'); +const CANCELLER_ROLE = ethers.id('CANCELLER_ROLE'); + +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); +const delay = 3600n; + +describe('GovernorStorage', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [deployer, owner, proposer, voter1, voter2, voter3, voter4] = await ethers.getSigners(); + const receiver = await ethers.deployContract('CallReceiverMock'); + + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const timelock = await ethers.deployContract('TimelockController', [delay, [], [], deployer]); + const mock = await ethers.deployContract('$GovernorStorageMock', [ + name, + votingDelay, + votingPeriod, + 0n, + timelock, + token, + 0n, + ]); + + await owner.sendTransaction({ to: timelock, value }); + await token.$_mint(owner, tokenSupply); + await timelock.grantRole(PROPOSER_ROLE, mock); + await timelock.grantRole(PROPOSER_ROLE, owner); + await timelock.grantRole(CANCELLER_ROLE, mock); + await timelock.grantRole(CANCELLER_ROLE, owner); + await timelock.grantRole(EXECUTOR_ROLE, ethers.ZeroAddress); + await timelock.revokeRole(DEFAULT_ADMIN_ROLE, deployer); + + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') }); + + return { deployer, owner, proposer, voter1, voter2, voter3, voter4, receiver, token, timelock, mock, helper }; + }; + + describe(`using ${Token}`, function () { beforeEach(async function () { - const [deployer] = await web3.eth.getAccounts(); - - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.timelock = await Timelock.new(3600, [], [], deployer); - this.mock = await Governor.new( - name, - votingDelay, - votingPeriod, - 0, - this.timelock.address, - this.token.address, - 0, - ); - this.receiver = await CallReceiver.new(); - - this.helper = new GovernorHelper(this.mock, mode); - - await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); - - // normal setup: governor is proposer, everyone is executor, timelock is its own admin - await this.timelock.grantRole(PROPOSER_ROLE, this.mock.address); - await this.timelock.grantRole(PROPOSER_ROLE, owner); - await this.timelock.grantRole(CANCELLER_ROLE, this.mock.address); - await this.timelock.grantRole(CANCELLER_ROLE, owner); - await this.timelock.grantRole(EXECUTOR_ROLE, constants.ZERO_ADDRESS); - await this.timelock.revokeRole(DEFAULT_ADMIN_ROLE, deployer); - - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - - // default proposal + Object.assign(this, await loadFixture(fixture)); + // initiate fresh proposal this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('mockFunction'), value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), }, ], '', ); this.proposal.timelockid = await this.timelock.hashOperationBatch( ...this.proposal.shortProposal.slice(0, 3), - '0x0', - timelockSalt(this.mock.address, this.proposal.shortProposal[3]), + ethers.ZeroHash, + timelockSalt(this.mock.target, this.proposal.shortProposal[3]), ); }); describe('proposal indexing', function () { it('before propose', async function () { - expect(await this.mock.proposalCount()).to.be.bignumber.equal('0'); + expect(await this.mock.proposalCount()).to.equal(0n); - // panic code 0x32 (out-of-bound) - await expectRevert.unspecified(this.mock.proposalDetailsAt(0)); + await expect(this.mock.proposalDetailsAt(0n)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); - await expectRevertCustomError(this.mock.proposalDetails(this.proposal.id), 'GovernorNonexistentProposal', [ - this.proposal.id, - ]); + await expect(this.mock.proposalDetails(this.proposal.id)) + .to.be.revertedWithCustomError(this.mock, 'GovernorNonexistentProposal') + .withArgs(this.proposal.id); }); it('after propose', async function () { await this.helper.propose(); - expect(await this.mock.proposalCount()).to.be.bignumber.equal('1'); + expect(await this.mock.proposalCount()).to.equal(1n); - const proposalDetailsAt0 = await this.mock.proposalDetailsAt(0); - expect(proposalDetailsAt0[0]).to.be.bignumber.equal(this.proposal.id); - expect(proposalDetailsAt0[1]).to.be.deep.equal(this.proposal.targets); - expect(proposalDetailsAt0[2].map(x => x.toString())).to.be.deep.equal(this.proposal.values); - expect(proposalDetailsAt0[3]).to.be.deep.equal(this.proposal.fulldata); - expect(proposalDetailsAt0[4]).to.be.equal(this.proposal.descriptionHash); + expect(await this.mock.proposalDetailsAt(0n)).to.deep.equal([ + this.proposal.id, + this.proposal.targets, + this.proposal.values, + this.proposal.data, + this.proposal.descriptionHash, + ]); - const proposalDetailsForId = await this.mock.proposalDetails(this.proposal.id); - expect(proposalDetailsForId[0]).to.be.deep.equal(this.proposal.targets); - expect(proposalDetailsForId[1].map(x => x.toString())).to.be.deep.equal(this.proposal.values); - expect(proposalDetailsForId[2]).to.be.deep.equal(this.proposal.fulldata); - expect(proposalDetailsForId[3]).to.be.equal(this.proposal.descriptionHash); + expect(await this.mock.proposalDetails(this.proposal.id)).to.deep.equal([ + this.proposal.targets, + this.proposal.values, + this.proposal.data, + this.proposal.descriptionHash, + ]); }); }); it('queue and execute by id', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter3).vote({ support: Enums.VoteType.Against }); + await this.helper.connect(this.voter4).vote({ support: Enums.VoteType.Abstain }); await this.helper.waitForDeadline(); - const txQueue = await this.mock.queue(this.proposal.id); - await this.helper.waitForEta(); - const txExecute = await this.mock.execute(this.proposal.id); - expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { id: this.proposal.timelockid }); - await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallSalt', { - id: this.proposal.timelockid, - }); + await expect(this.mock.queue(this.proposal.id)) + .to.emit(this.mock, 'ProposalQueued') + .withArgs(this.proposal.id, anyValue) + .to.emit(this.timelock, 'CallScheduled') + .withArgs(this.proposal.timelockid, ...Array(6).fill(anyValue)) + .to.emit(this.timelock, 'CallSalt') + .withArgs(this.proposal.timelockid, anyValue); + + await this.helper.waitForEta(); - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txExecute.tx, this.timelock, 'CallExecuted', { id: this.proposal.timelockid }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + await expect(this.mock.execute(this.proposal.id)) + .to.emit(this.mock, 'ProposalExecuted') + .withArgs(this.proposal.id) + .to.emit(this.timelock, 'CallExecuted') + .withArgs(this.proposal.timelockid, ...Array(4).fill(anyValue)) + .to.emit(this.receiver, 'MockFunctionCalled'); }); it('cancel by id', async function () { - await this.helper.propose(); - const txCancel = await this.mock.cancel(this.proposal.id); - expectEvent(txCancel, 'ProposalCanceled', { proposalId: this.proposal.id }); + await this.helper.connect(this.proposer).propose(); + await expect(this.mock.connect(this.proposer).cancel(this.proposal.id)) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); }); }); } diff --git a/test/governance/extensions/GovernorTimelockAccess.test.js b/test/governance/extensions/GovernorTimelockAccess.test.js index 252a3d52ed5..2f16d1b991e 100644 --- a/test/governance/extensions/GovernorTimelockAccess.test.js +++ b/test/governance/extensions/GovernorTimelockAccess.test.js @@ -1,132 +1,120 @@ -const { expectEvent, time } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); -const Enums = require('../../helpers/enums'); -const { GovernorHelper, proposalStatesToBitMap } = require('../../helpers/governance'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const { clockFromReceipt } = require('../../helpers/time'); +const { GovernorHelper } = require('../../helpers/governance'); +const { bigint: Enums } = require('../../helpers/enums'); +const { bigint: time } = require('../../helpers/time'); +const { max } = require('../../helpers/math'); const { selector } = require('../../helpers/methods'); const { hashOperation } = require('../../helpers/access-manager'); -const AccessManager = artifacts.require('$AccessManager'); -const Governor = artifacts.require('$GovernorTimelockAccessMock'); -const AccessManagedTarget = artifacts.require('$AccessManagedTarget'); -const Ownable = artifacts.require('$Ownable'); +function prepareOperation({ sender, target, value = 0n, data = '0x' }) { + return { + id: hashOperation(sender, target, data), + operation: { target, value, data }, + selector: data.slice(0, 10).padEnd(10, '0'), + }; +} const TOKENS = [ - { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, ]; -contract('GovernorTimelockAccess', function (accounts) { - const [admin, voter1, voter2, voter3, voter4, other] = accounts; - - const name = 'OZ-Governor'; - const version = '1'; - const tokenName = 'MockToken'; - const tokenSymbol = 'MTKN'; - const tokenSupply = web3.utils.toWei('100'); - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); + +describe('GovernorTimelockAccess', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [admin, voter1, voter2, voter3, voter4, other] = await ethers.getSigners(); + + const manager = await ethers.deployContract('$AccessManager', [admin]); + const receiver = await ethers.deployContract('$AccessManagedTarget', [manager]); + + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const mock = await ethers.deployContract('$GovernorTimelockAccessMock', [ + name, + votingDelay, + votingPeriod, + 0n, + manager, + 0n, + token, + 0n, + ]); + + await admin.sendTransaction({ to: mock, value }); + await token.$_mint(admin, tokenSupply); + + const helper = new GovernorHelper(mock, mode); + await helper.connect(admin).delegate({ token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(admin).delegate({ token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(admin).delegate({ token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(admin).delegate({ token, to: voter4, value: ethers.parseEther('2') }); + + return { admin, voter1, voter2, voter3, voter4, other, manager, receiver, token, mock, helper }; + }; + + describe(`using ${Token}`, function () { beforeEach(async function () { - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.manager = await AccessManager.new(admin); - this.mock = await Governor.new( - name, - votingDelay, - votingPeriod, - 0, // proposal threshold - this.manager.address, - 0, // base delay - this.token.address, - 0, // quorum - ); - this.receiver = await AccessManagedTarget.new(this.manager.address); - - this.helper = new GovernorHelper(this.mock, mode); - - await web3.eth.sendTransaction({ from: admin, to: this.mock.address, value }); - - await this.token.$_mint(admin, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: admin }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: admin }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: admin }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: admin }); - - // default proposals - this.restricted = {}; - this.restricted.selector = this.receiver.contract.methods.fnRestricted().encodeABI(); - this.restricted.operation = { - target: this.receiver.address, - value: '0', - data: this.restricted.selector, - }; - this.restricted.operationId = hashOperation( - this.mock.address, - this.restricted.operation.target, - this.restricted.operation.data, - ); + Object.assign(this, await loadFixture(fixture)); - this.unrestricted = {}; - this.unrestricted.selector = this.receiver.contract.methods.fnUnrestricted().encodeABI(); - this.unrestricted.operation = { - target: this.receiver.address, - value: '0', - data: this.unrestricted.selector, - }; - this.unrestricted.operationId = hashOperation( - this.mock.address, - this.unrestricted.operation.target, - this.unrestricted.operation.data, - ); + // restricted proposal + this.restricted = prepareOperation({ + sender: this.mock.target, + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('fnRestricted'), + }); - this.fallback = {}; - this.fallback.operation = { - target: this.receiver.address, - value: '0', + this.unrestricted = prepareOperation({ + sender: this.mock.target, + target: this.receiver.target, + data: this.receiver.interface.encodeFunctionData('fnUnrestricted'), + }); + + this.fallback = prepareOperation({ + sender: this.mock.target, + target: this.receiver.target, data: '0x1234', - }; - this.fallback.operationId = hashOperation( - this.mock.address, - this.fallback.operation.target, - this.fallback.operation.data, - ); + }); }); it('accepts ether transfers', async function () { - await web3.eth.sendTransaction({ from: admin, to: this.mock.address, value: 1 }); + await this.admin.sendTransaction({ to: this.mock, value: 1n }); }); it('post deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + expect(await this.mock.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token.target); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + expect(await this.mock.quorum(0n)).to.equal(0n); - expect(await this.mock.accessManager()).to.be.equal(this.manager.address); + expect(await this.mock.accessManager()).to.equal(this.manager.target); }); it('sets base delay (seconds)', async function () { - const baseDelay = time.duration.hours(10); + const baseDelay = time.duration.hours(10n); // Only through governance - await expectRevertCustomError( - this.mock.setBaseDelaySeconds(baseDelay, { from: voter1 }), - 'GovernorOnlyExecutor', - [voter1], - ); + await expect(this.mock.connect(this.voter1).setBaseDelaySeconds(baseDelay)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.voter1.address); this.proposal = await this.helper.setProposal( [ { - target: this.mock.address, - value: '0', - data: this.mock.contract.methods.setBaseDelaySeconds(baseDelay).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setBaseDelaySeconds', [baseDelay]), }, ], 'descr', @@ -134,95 +122,90 @@ contract('GovernorTimelockAccess', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - const receipt = await this.helper.execute(); - expectEvent(receipt, 'BaseDelaySet', { - oldBaseDelaySeconds: '0', - newBaseDelaySeconds: baseDelay, - }); + await expect(this.helper.execute()).to.emit(this.mock, 'BaseDelaySet').withArgs(0n, baseDelay); - expect(await this.mock.baseDelaySeconds()).to.be.bignumber.eq(baseDelay); + expect(await this.mock.baseDelaySeconds()).to.equal(baseDelay); }); it('sets access manager ignored', async function () { const selectors = ['0x12345678', '0x87654321', '0xabcdef01']; // Only through governance - await expectRevertCustomError( - this.mock.setAccessManagerIgnored(other, selectors, true, { from: voter1 }), - 'GovernorOnlyExecutor', - [voter1], - ); + await expect(this.mock.connect(this.voter1).setAccessManagerIgnored(this.other, selectors, true)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.voter1.address); // Ignore - const helperIgnore = new GovernorHelper(this.mock, mode); - await helperIgnore.setProposal( + await this.helper.setProposal( [ { - target: this.mock.address, - value: '0', - data: this.mock.contract.methods.setAccessManagerIgnored(other, selectors, true).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setAccessManagerIgnored', [ + this.other.address, + selectors, + true, + ]), }, ], 'descr', ); + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.waitForDeadline(); - await helperIgnore.propose(); - await helperIgnore.waitForSnapshot(); - await helperIgnore.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await helperIgnore.waitForDeadline(); - const ignoreReceipt = await helperIgnore.execute(); - + const ignoreReceipt = this.helper.execute(); for (const selector of selectors) { - expectEvent(ignoreReceipt, 'AccessManagerIgnoredSet', { - target: other, - selector, - ignored: true, - }); - expect(await this.mock.isAccessManagerIgnored(other, selector)).to.be.true; + await expect(ignoreReceipt) + .to.emit(this.mock, 'AccessManagerIgnoredSet') + .withArgs(this.other.address, selector, true); + expect(await this.mock.isAccessManagerIgnored(this.other, selector)).to.be.true; } // Unignore - const helperUnignore = new GovernorHelper(this.mock, mode); - await helperUnignore.setProposal( + await this.helper.setProposal( [ { - target: this.mock.address, - value: '0', - data: this.mock.contract.methods.setAccessManagerIgnored(other, selectors, false).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setAccessManagerIgnored', [ + this.other.address, + selectors, + false, + ]), }, ], 'descr', ); - await helperUnignore.propose(); - await helperUnignore.waitForSnapshot(); - await helperUnignore.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await helperUnignore.waitForDeadline(); - const unignoreReceipt = await helperUnignore.execute(); + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.waitForDeadline(); + const unignoreReceipt = this.helper.execute(); for (const selector of selectors) { - expectEvent(unignoreReceipt, 'AccessManagerIgnoredSet', { - target: other, - selector, - ignored: false, - }); - expect(await this.mock.isAccessManagerIgnored(other, selector)).to.be.false; + await expect(unignoreReceipt) + .to.emit(this.mock, 'AccessManagerIgnoredSet') + .withArgs(this.other.address, selector, false); + expect(await this.mock.isAccessManagerIgnored(this.other, selector)).to.be.false; } }); it('sets access manager ignored when target is the governor', async function () { - const other = this.mock.address; const selectors = ['0x12345678', '0x87654321', '0xabcdef01']; await this.helper.setProposal( [ { - target: this.mock.address, - value: '0', - data: this.mock.contract.methods.setAccessManagerIgnored(other, selectors, true).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('setAccessManagerIgnored', [ + this.mock.target, + selectors, + true, + ]), }, ], 'descr', @@ -230,154 +213,141 @@ contract('GovernorTimelockAccess', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - const receipt = await this.helper.execute(); + const tx = this.helper.execute(); for (const selector of selectors) { - expectEvent(receipt, 'AccessManagerIgnoredSet', { - target: other, - selector, - ignored: true, - }); - expect(await this.mock.isAccessManagerIgnored(other, selector)).to.be.true; + await expect(tx).to.emit(this.mock, 'AccessManagerIgnoredSet').withArgs(this.mock.target, selector, true); + expect(await this.mock.isAccessManagerIgnored(this.mock, selector)).to.be.true; } }); it('does not need to queue proposals with no delay', async function () { - const roleId = '1'; - - const executionDelay = web3.utils.toBN(0); - const baseDelay = web3.utils.toBN(0); + const roleId = 1n; + const executionDelay = 0n; + const baseDelay = 0n; // Set execution delay - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, executionDelay, { from: admin }); + await this.manager.connect(this.admin).setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay); // Set base delay await this.mock.$_setBaseDelaySeconds(baseDelay); - this.proposal = await this.helper.setProposal([this.restricted.operation], 'descr'); + await this.helper.setProposal([this.restricted.operation], 'descr'); await this.helper.propose(); expect(await this.mock.proposalNeedsQueuing(this.helper.currentProposal.id)).to.be.false; }); it('needs to queue proposals with any delay', async function () { - const roleId = '1'; - + const roleId = 1n; const delays = [ - [time.duration.hours(1), time.duration.hours(2)], - [time.duration.hours(2), time.duration.hours(1)], + [time.duration.hours(1n), time.duration.hours(2n)], + [time.duration.hours(2n), time.duration.hours(1n)], ]; for (const [executionDelay, baseDelay] of delays) { // Set execution delay - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, executionDelay, { from: admin }); + await this.manager + .connect(this.admin) + .setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay); // Set base delay await this.mock.$_setBaseDelaySeconds(baseDelay); - const helper = new GovernorHelper(this.mock, mode); - this.proposal = await helper.setProposal( + await this.helper.setProposal( [this.restricted.operation], `executionDelay=${executionDelay.toString()}}baseDelay=${baseDelay.toString()}}`, ); - await helper.propose(); - expect(await this.mock.proposalNeedsQueuing(helper.currentProposal.id)).to.be.true; + await this.helper.propose(); + expect(await this.mock.proposalNeedsQueuing(this.helper.currentProposal.id)).to.be.true; } }); describe('execution plan', function () { it('returns plan for delayed operations', async function () { - const roleId = '1'; - + const roleId = 1n; const delays = [ - [time.duration.hours(1), time.duration.hours(2)], - [time.duration.hours(2), time.duration.hours(1)], + [time.duration.hours(1n), time.duration.hours(2n)], + [time.duration.hours(2n), time.duration.hours(1n)], ]; for (const [executionDelay, baseDelay] of delays) { // Set execution delay - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, executionDelay, { from: admin }); + await this.manager + .connect(this.admin) + .setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay); // Set base delay await this.mock.$_setBaseDelaySeconds(baseDelay); - const helper = new GovernorHelper(this.mock, mode); - this.proposal = await helper.setProposal( + this.proposal = await this.helper.setProposal( [this.restricted.operation], `executionDelay=${executionDelay.toString()}}baseDelay=${baseDelay.toString()}}`, ); - await helper.propose(); - const { delay: planDelay, indirect, withDelay } = await this.mock.proposalExecutionPlan(this.proposal.id); - const maxDelay = web3.utils.toBN(Math.max(baseDelay.toNumber(), executionDelay.toNumber())); - expect(planDelay).to.be.bignumber.eq(maxDelay); - expect(indirect).to.deep.eq([true]); - expect(withDelay).to.deep.eq([true]); + await this.helper.propose(); + + expect(await this.mock.proposalExecutionPlan(this.proposal.id)).to.deep.equal([ + max(baseDelay, executionDelay), + [true], + [true], + ]); } }); it('returns plan for not delayed operations', async function () { - const roleId = '1'; - - const executionDelay = web3.utils.toBN(0); - const baseDelay = web3.utils.toBN(0); + const roleId = 1n; + const executionDelay = 0n; + const baseDelay = 0n; // Set execution delay - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, executionDelay, { from: admin }); + await this.manager + .connect(this.admin) + .setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay); // Set base delay await this.mock.$_setBaseDelaySeconds(baseDelay); this.proposal = await this.helper.setProposal([this.restricted.operation], `descr`); await this.helper.propose(); - const { delay: planDelay, indirect, withDelay } = await this.mock.proposalExecutionPlan(this.proposal.id); - expect(planDelay).to.be.bignumber.eq(web3.utils.toBN(0)); - expect(indirect).to.deep.eq([true]); - expect(withDelay).to.deep.eq([false]); + + expect(await this.mock.proposalExecutionPlan(this.proposal.id)).to.deep.equal([0n, [true], [false]]); }); it('returns plan for an operation ignoring the manager', async function () { - await this.mock.$_setAccessManagerIgnored(this.receiver.address, this.restricted.selector, true); - - const roleId = '1'; + await this.mock.$_setAccessManagerIgnored(this.receiver, this.restricted.selector, true); + const roleId = 1n; const delays = [ - [time.duration.hours(1), time.duration.hours(2)], - [time.duration.hours(2), time.duration.hours(1)], + [time.duration.hours(1n), time.duration.hours(2n)], + [time.duration.hours(2n), time.duration.hours(1n)], ]; for (const [executionDelay, baseDelay] of delays) { // Set execution delay - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, executionDelay, { from: admin }); + await this.manager + .connect(this.admin) + .setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay); // Set base delay await this.mock.$_setBaseDelaySeconds(baseDelay); - const helper = new GovernorHelper(this.mock, mode); - this.proposal = await helper.setProposal( + this.proposal = await this.helper.setProposal( [this.restricted.operation], `executionDelay=${executionDelay.toString()}}baseDelay=${baseDelay.toString()}}`, ); - await helper.propose(); - const { delay: planDelay, indirect, withDelay } = await this.mock.proposalExecutionPlan(this.proposal.id); - expect(planDelay).to.be.bignumber.eq(baseDelay); - expect(indirect).to.deep.eq([false]); - expect(withDelay).to.deep.eq([false]); + await this.helper.propose(); + + expect(await this.mock.proposalExecutionPlan(this.proposal.id)).to.deep.equal([ + baseDelay, + [false], + [false], + ]); } }); }); @@ -394,49 +364,47 @@ contract('GovernorTimelockAccess', function (accounts) { this.proposal = await this.helper.setProposal([this.unrestricted.operation], 'descr'); await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); if (await this.mock.proposalNeedsQueuing(this.proposal.id)) { - const txQueue = await this.helper.queue(); - expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); + expect(await this.helper.queue()) + .to.emit(this.mock, 'ProposalQueued') + .withArgs(this.proposal.id); } if (delay > 0) { await this.helper.waitForEta(); } - const txExecute = await this.helper.execute(); - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - expectEvent.inTransaction(txExecute, this.receiver, 'CalledUnrestricted'); + expect(await this.helper.execute()) + .to.emit(this.mock, 'ProposalExecuted') + .withArgs(this.proposal.id) + .to.not.emit(this.receiver, 'CalledUnrestricted'); }); } }); it('reverts when an operation is executed before eta', async function () { - const delay = time.duration.hours(2); + const delay = time.duration.hours(2n); await this.mock.$_setBaseDelaySeconds(delay); this.proposal = await this.helper.setProposal([this.unrestricted.operation], 'descr'); await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnmetDelay', [ - this.proposal.id, - await this.mock.proposalEta(this.proposal.id), - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnmetDelay') + .withArgs(this.proposal.id, await this.mock.proposalEta(this.proposal.id)); }); it('reverts with a proposal including multiple operations but one of those was cancelled in the manager', async function () { - const delay = time.duration.hours(2); - const roleId = '1'; + const delay = time.duration.hours(2n); + const roleId = 1n; - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, delay, { from: admin }); + await this.manager.connect(this.admin).setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, delay); // Set proposals const original = new GovernorHelper(this.mock, mode); @@ -445,83 +413,79 @@ contract('GovernorTimelockAccess', function (accounts) { // Go through all the governance process await original.propose(); await original.waitForSnapshot(); - await original.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await original.connect(this.voter1).vote({ support: Enums.VoteType.For }); await original.waitForDeadline(); await original.queue(); await original.waitForEta(); // Suddenly cancel one of the proposed operations in the manager - await this.manager.cancel(this.mock.address, this.restricted.operation.target, this.restricted.operation.data, { - from: admin, - }); + await this.manager + .connect(this.admin) + .cancel(this.mock, this.restricted.operation.target, this.restricted.operation.data); // Reschedule the same operation in a different proposal to avoid "AccessManagerNotScheduled" error const rescheduled = new GovernorHelper(this.mock, mode); await rescheduled.setProposal([this.restricted.operation], 'descr'); await rescheduled.propose(); await rescheduled.waitForSnapshot(); - await rescheduled.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await rescheduled.connect(this.voter1).vote({ support: Enums.VoteType.For }); await rescheduled.waitForDeadline(); await rescheduled.queue(); // This will schedule it again in the manager await rescheduled.waitForEta(); // Attempt to execute - await expectRevertCustomError(original.execute(), 'GovernorMismatchedNonce', [ - original.currentProposal.id, - 1, - 2, - ]); + await expect(original.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorMismatchedNonce') + .withArgs(original.currentProposal.id, 1, 2); }); it('single operation with access manager delay', async function () { - const delay = 1000; - const roleId = '1'; + const delay = 1000n; + const roleId = 1n; - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, delay, { from: admin }); + await this.manager.connect(this.admin).setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, delay); this.proposal = await this.helper.setProposal([this.restricted.operation], 'descr'); await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); const txQueue = await this.helper.queue(); await this.helper.waitForEta(); const txExecute = await this.helper.execute(); - expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txQueue.tx, this.manager, 'OperationScheduled', { - operationId: this.restricted.operationId, - nonce: '1', - schedule: web3.utils.toBN(await clockFromReceipt.timestamp(txQueue.receipt)).addn(delay), - caller: this.mock.address, - target: this.restricted.operation.target, - data: this.restricted.operation.data, - }); + await expect(txQueue) + .to.emit(this.mock, 'ProposalQueued') + .withArgs(this.proposal.id, anyValue) + .to.emit(this.manager, 'OperationScheduled') + .withArgs( + this.restricted.id, + 1n, + (await time.clockFromReceipt.timestamp(txQueue)) + delay, + this.mock.target, + this.restricted.operation.target, + this.restricted.operation.data, + ); - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txExecute.tx, this.manager, 'OperationExecuted', { - operationId: this.restricted.operationId, - nonce: '1', - }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'CalledRestricted'); + await expect(txExecute) + .to.emit(this.mock, 'ProposalExecuted') + .withArgs(this.proposal.id) + .to.emit(this.manager, 'OperationExecuted') + .withArgs(this.restricted.id, 1n) + .to.emit(this.receiver, 'CalledRestricted'); }); it('bundle of varied operations', async function () { - const managerDelay = 1000; - const roleId = '1'; - - const baseDelay = managerDelay * 2; + const managerDelay = 1000n; + const roleId = 1n; + const baseDelay = managerDelay * 2n; await this.mock.$_setBaseDelaySeconds(baseDelay); - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, managerDelay, { from: admin }); + await this.manager.connect(this.admin).setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, managerDelay); this.proposal = await this.helper.setProposal( [this.restricted.operation, this.unrestricted.operation, this.fallback.operation], @@ -530,41 +494,44 @@ contract('GovernorTimelockAccess', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); const txQueue = await this.helper.queue(); await this.helper.waitForEta(); const txExecute = await this.helper.execute(); - expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txQueue.tx, this.manager, 'OperationScheduled', { - operationId: this.restricted.operationId, - nonce: '1', - schedule: web3.utils.toBN(await clockFromReceipt.timestamp(txQueue.receipt)).addn(baseDelay), - caller: this.mock.address, - target: this.restricted.operation.target, - data: this.restricted.operation.data, - }); + await expect(txQueue) + .to.emit(this.mock, 'ProposalQueued') + .withArgs(this.proposal.id, anyValue) + .to.emit(this.manager, 'OperationScheduled') + .withArgs( + this.restricted.id, + 1n, + (await time.clockFromReceipt.timestamp(txQueue)) + baseDelay, + this.mock.target, + this.restricted.operation.target, + this.restricted.operation.data, + ); - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txExecute.tx, this.manager, 'OperationExecuted', { - operationId: this.restricted.operationId, - nonce: '1', - }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'CalledRestricted'); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'CalledUnrestricted'); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'CalledFallback'); + await expect(txExecute) + .to.emit(this.mock, 'ProposalExecuted') + .withArgs(this.proposal.id) + .to.emit(this.manager, 'OperationExecuted') + .withArgs(this.restricted.id, 1n) + .to.emit(this.receiver, 'CalledRestricted') + .to.emit(this.receiver, 'CalledUnrestricted') + .to.emit(this.receiver, 'CalledFallback'); }); describe('cancel', function () { - const delay = 1000; - const roleId = '1'; + const delay = 1000n; + const roleId = 1n; beforeEach(async function () { - await this.manager.setTargetFunctionRole(this.receiver.address, [this.restricted.selector], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, delay, { from: admin }); + await this.manager + .connect(this.admin) + .setTargetFunctionRole(this.receiver, [this.restricted.selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, delay); }); it('cancels restricted with delay after queue (internal)', async function () { @@ -572,23 +539,25 @@ contract('GovernorTimelockAccess', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - const txCancel = await this.helper.cancel('internal'); - expectEvent(txCancel, 'ProposalCanceled', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txCancel.tx, this.manager, 'OperationCanceled', { - operationId: this.restricted.operationId, - nonce: '1', - }); + await expect(this.helper.cancel('internal')) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id) + .to.emit(this.manager, 'OperationCanceled') + .withArgs(this.restricted.id, 1n); await this.helper.waitForEta(); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('cancels restricted with queueing if the same operation is part of a more recent proposal (internal)', async function () { @@ -599,17 +568,14 @@ contract('GovernorTimelockAccess', function (accounts) { // Go through all the governance process await original.propose(); await original.waitForSnapshot(); - await original.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await original.connect(this.voter1).vote({ support: Enums.VoteType.For }); await original.waitForDeadline(); await original.queue(); // Cancel the operation in the manager - await this.manager.cancel( - this.mock.address, - this.restricted.operation.target, - this.restricted.operation.data, - { from: admin }, - ); + await this.manager + .connect(this.admin) + .cancel(this.mock, this.restricted.operation.target, this.restricted.operation.data); // Another proposal is added with the same operation const rescheduled = new GovernorHelper(this.mock, mode); @@ -618,21 +584,26 @@ contract('GovernorTimelockAccess', function (accounts) { // Queue the new proposal await rescheduled.propose(); await rescheduled.waitForSnapshot(); - await rescheduled.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await rescheduled.connect(this.voter1).vote({ support: Enums.VoteType.For }); await rescheduled.waitForDeadline(); await rescheduled.queue(); // This will schedule it again in the manager // Cancel const eta = await this.mock.proposalEta(rescheduled.currentProposal.id); - const txCancel = await original.cancel('internal'); - expectEvent(txCancel, 'ProposalCanceled', { proposalId: original.currentProposal.id }); - - await time.increase(eta); // waitForEta() - await expectRevertCustomError(original.execute(), 'GovernorUnexpectedProposalState', [ - original.currentProposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + + await expect(original.cancel('internal')) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(original.currentProposal.id); + + await time.clock.timestamp().then(clock => time.forward.timestamp(max(clock + 1n, eta))); + + await expect(original.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + original.currentProposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('cancels unrestricted with queueing (internal)', async function () { @@ -640,20 +611,25 @@ contract('GovernorTimelockAccess', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); const eta = await this.mock.proposalEta(this.proposal.id); - const txCancel = await this.helper.cancel('internal'); - expectEvent(txCancel, 'ProposalCanceled', { proposalId: this.proposal.id }); - - await time.increase(eta); // waitForEta() - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + + await expect(this.helper.cancel('internal')) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); + + await time.clock.timestamp().then(clock => time.forward.timestamp(max(clock + 1n, eta))); + + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('cancels unrestricted without queueing (internal)', async function () { @@ -661,28 +637,31 @@ contract('GovernorTimelockAccess', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); // await this.helper.queue(); // const eta = await this.mock.proposalEta(this.proposal.id); - const txCancel = await this.helper.cancel('internal'); - expectEvent(txCancel, 'ProposalCanceled', { proposalId: this.proposal.id }); - - // await time.increase(eta); // waitForEta() - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await expect(this.helper.cancel('internal')) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); + + // await time.forward.timestamp(eta); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('cancels calls already canceled by guardian', async function () { - const operationA = { target: this.receiver.address, data: this.restricted.selector + '00' }; - const operationB = { target: this.receiver.address, data: this.restricted.selector + '01' }; - const operationC = { target: this.receiver.address, data: this.restricted.selector + '02' }; - const operationAId = hashOperation(this.mock.address, operationA.target, operationA.data); - const operationBId = hashOperation(this.mock.address, operationB.target, operationB.data); + const operationA = { target: this.receiver.target, data: this.restricted.selector + '00' }; + const operationB = { target: this.receiver.target, data: this.restricted.selector + '01' }; + const operationC = { target: this.receiver.target, data: this.restricted.selector + '02' }; + const operationAId = hashOperation(this.mock.target, operationA.target, operationA.data); + const operationBId = hashOperation(this.mock.target, operationB.target, operationB.data); const proposal1 = new GovernorHelper(this.mock, mode); const proposal2 = new GovernorHelper(this.mock, mode); @@ -692,7 +671,7 @@ contract('GovernorTimelockAccess', function (accounts) { for (const p of [proposal1, proposal2]) { await p.propose(); await p.waitForSnapshot(); - await p.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await p.connect(this.voter1).vote({ support: Enums.VoteType.For }); await p.waitForDeadline(); } @@ -700,18 +679,24 @@ contract('GovernorTimelockAccess', function (accounts) { await proposal1.queue(); // Cannot queue the second proposal: operation A already scheduled with delay - await expectRevertCustomError(proposal2.queue(), 'AccessManagerAlreadyScheduled', [operationAId]); + await expect(proposal2.queue()) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerAlreadyScheduled') + .withArgs(operationAId); // Admin cancels operation B on the manager - await this.manager.cancel(this.mock.address, operationB.target, operationB.data, { from: admin }); + await this.manager.connect(this.admin).cancel(this.mock, operationB.target, operationB.data); // Still cannot queue the second proposal: operation A already scheduled with delay - await expectRevertCustomError(proposal2.queue(), 'AccessManagerAlreadyScheduled', [operationAId]); + await expect(proposal2.queue()) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerAlreadyScheduled') + .withArgs(operationAId); await proposal1.waitForEta(); // Cannot execute first proposal: operation B has been canceled - await expectRevertCustomError(proposal1.execute(), 'AccessManagerNotScheduled', [operationBId]); + await expect(proposal1.execute()) + .to.be.revertedWithCustomError(this.manager, 'AccessManagerNotScheduled') + .withArgs(operationBId); // Cancel the first proposal to release operation A await proposal1.cancel('internal'); @@ -728,43 +713,41 @@ contract('GovernorTimelockAccess', function (accounts) { describe('ignore AccessManager', function () { it('defaults', async function () { - expect(await this.mock.isAccessManagerIgnored(this.receiver.address, this.restricted.selector)).to.equal( - false, - ); - expect(await this.mock.isAccessManagerIgnored(this.mock.address, '0x12341234')).to.equal(true); + expect(await this.mock.isAccessManagerIgnored(this.receiver, this.restricted.selector)).to.be.false; + expect(await this.mock.isAccessManagerIgnored(this.mock, '0x12341234')).to.be.true; }); it('internal setter', async function () { - const p1 = { target: this.receiver.address, selector: this.restricted.selector, ignored: true }; - const tx1 = await this.mock.$_setAccessManagerIgnored(p1.target, p1.selector, p1.ignored); - expect(await this.mock.isAccessManagerIgnored(p1.target, p1.selector)).to.equal(p1.ignored); - expectEvent(tx1, 'AccessManagerIgnoredSet', p1); - - const p2 = { target: this.mock.address, selector: '0x12341234', ignored: false }; - const tx2 = await this.mock.$_setAccessManagerIgnored(p2.target, p2.selector, p2.ignored); - expect(await this.mock.isAccessManagerIgnored(p2.target, p2.selector)).to.equal(p2.ignored); - expectEvent(tx2, 'AccessManagerIgnoredSet', p2); + await expect(this.mock.$_setAccessManagerIgnored(this.receiver, this.restricted.selector, true)) + .to.emit(this.mock, 'AccessManagerIgnoredSet') + .withArgs(this.receiver.target, this.restricted.selector, true); + + expect(await this.mock.isAccessManagerIgnored(this.receiver, this.restricted.selector)).to.be.true; + + await expect(this.mock.$_setAccessManagerIgnored(this.mock, '0x12341234', false)) + .to.emit(this.mock, 'AccessManagerIgnoredSet') + .withArgs(this.mock.target, '0x12341234', false); + + expect(await this.mock.isAccessManagerIgnored(this.mock, '0x12341234')).to.be.false; }); it('external setter', async function () { const setAccessManagerIgnored = (...args) => - this.mock.contract.methods.setAccessManagerIgnored(...args).encodeABI(); + this.mock.interface.encodeFunctionData('setAccessManagerIgnored', args); await this.helper.setProposal( [ { - target: this.mock.address, + target: this.mock.target, data: setAccessManagerIgnored( - this.receiver.address, + this.receiver.target, [this.restricted.selector, this.unrestricted.selector], true, ), - value: '0', }, { - target: this.mock.address, - data: setAccessManagerIgnored(this.mock.address, ['0x12341234', '0x67896789'], false), - value: '0', + target: this.mock.target, + data: setAccessManagerIgnored(this.mock.target, ['0x12341234', '0x67896789'], false), }, ], 'descr', @@ -772,65 +755,59 @@ contract('GovernorTimelockAccess', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - const tx = await this.helper.execute(); - expectEvent(tx, 'AccessManagerIgnoredSet'); + await expect(this.helper.execute()).to.emit(this.mock, 'AccessManagerIgnoredSet'); - expect(await this.mock.isAccessManagerIgnored(this.receiver.address, this.restricted.selector)).to.equal( - true, - ); - expect(await this.mock.isAccessManagerIgnored(this.receiver.address, this.unrestricted.selector)).to.equal( - true, - ); - - expect(await this.mock.isAccessManagerIgnored(this.mock.address, '0x12341234')).to.equal(false); - expect(await this.mock.isAccessManagerIgnored(this.mock.address, '0x67896789')).to.equal(false); + expect(await this.mock.isAccessManagerIgnored(this.receiver, this.restricted.selector)).to.be.true; + expect(await this.mock.isAccessManagerIgnored(this.receiver, this.unrestricted.selector)).to.be.true; + expect(await this.mock.isAccessManagerIgnored(this.mock, '0x12341234')).to.be.false; + expect(await this.mock.isAccessManagerIgnored(this.mock, '0x67896789')).to.be.false; }); it('locked function', async function () { const setAccessManagerIgnored = selector('setAccessManagerIgnored(address,bytes4[],bool)'); - await expectRevertCustomError( - this.mock.$_setAccessManagerIgnored(this.mock.address, setAccessManagerIgnored, true), - 'GovernorLockedIgnore', - [], - ); - await this.mock.$_setAccessManagerIgnored(this.receiver.address, setAccessManagerIgnored, true); + + await expect( + this.mock.$_setAccessManagerIgnored(this.mock, setAccessManagerIgnored, true), + ).to.be.revertedWithCustomError(this.mock, 'GovernorLockedIgnore'); + + await this.mock.$_setAccessManagerIgnored(this.receiver, setAccessManagerIgnored, true); }); it('ignores access manager', async function () { - const amount = 100; - - const target = this.token.address; - const data = this.token.contract.methods.transfer(voter4, amount).encodeABI(); + const amount = 100n; + const target = this.token.target; + const data = this.token.interface.encodeFunctionData('transfer', [this.voter4.address, amount]); const selector = data.slice(0, 10); - await this.token.$_mint(this.mock.address, amount); + await this.token.$_mint(this.mock, amount); - const roleId = '1'; - await this.manager.setTargetFunctionRole(target, [selector], roleId, { from: admin }); - await this.manager.grantRole(roleId, this.mock.address, 0, { from: admin }); + const roleId = 1n; + await this.manager.connect(this.admin).setTargetFunctionRole(target, [selector], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, 0); - this.proposal = await this.helper.setProposal([{ target, data, value: '0' }], '1'); + await this.helper.setProposal([{ target, data }], 'descr #1'); await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - await expectRevertCustomError(this.helper.execute(), 'ERC20InsufficientBalance', [ - this.manager.address, - 0, - amount, - ]); + + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.token, 'ERC20InsufficientBalance') + .withArgs(this.manager.target, 0n, amount); await this.mock.$_setAccessManagerIgnored(target, selector, true); - await this.helper.setProposal([{ target, data, value: '0' }], '2'); + await this.helper.setProposal([{ target, data }], 'descr #2'); await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - const tx = await this.helper.execute(); - expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.mock.address }); + + await expect(this.helper.execute()) + .to.emit(this.token, 'Transfer') + .withArgs(this.mock.target, this.voter4.address, amount); }); }); @@ -838,32 +815,29 @@ contract('GovernorTimelockAccess', function (accounts) { const method = selector('$_checkOwner()'); beforeEach(async function () { - this.ownable = await Ownable.new(this.manager.address); + this.ownable = await ethers.deployContract('$Ownable', [this.manager]); this.operation = { - target: this.ownable.address, - value: '0', - data: this.ownable.contract.methods.$_checkOwner().encodeABI(), + target: this.ownable.target, + data: this.ownable.interface.encodeFunctionData('$_checkOwner'), }; }); it('succeeds with delay', async function () { - const roleId = '1'; - const executionDelay = time.duration.hours(2); - const baseDelay = time.duration.hours(1); + const roleId = 1n; + const executionDelay = time.duration.hours(2n); + const baseDelay = time.duration.hours(1n); // Set execution delay - await this.manager.setTargetFunctionRole(this.ownable.address, [method], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, executionDelay, { from: admin }); + await this.manager.connect(this.admin).setTargetFunctionRole(this.ownable, [method], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay); // Set base delay await this.mock.$_setBaseDelaySeconds(baseDelay); - this.proposal = await this.helper.setProposal([this.operation], `descr`); + await this.helper.setProposal([this.operation], `descr`); await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); @@ -871,23 +845,21 @@ contract('GovernorTimelockAccess', function (accounts) { }); it('succeeds without delay', async function () { - const roleId = '1'; - const executionDelay = web3.utils.toBN(0); - const baseDelay = web3.utils.toBN(0); + const roleId = 1n; + const executionDelay = 0n; + const baseDelay = 0n; // Set execution delay - await this.manager.setTargetFunctionRole(this.ownable.address, [method], roleId, { - from: admin, - }); - await this.manager.grantRole(roleId, this.mock.address, executionDelay, { from: admin }); + await this.manager.connect(this.admin).setTargetFunctionRole(this.ownable, [method], roleId); + await this.manager.connect(this.admin).grantRole(roleId, this.mock, executionDelay); // Set base delay await this.mock.$_setBaseDelaySeconds(baseDelay); - this.proposal = await this.helper.setProposal([this.operation], `descr`); + await this.helper.setProposal([this.operation], `descr`); await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.execute(); // Don't revert }); diff --git a/test/governance/extensions/GovernorTimelockCompound.test.js b/test/governance/extensions/GovernorTimelockCompound.test.js index 56191eb5056..ecc71dc0907 100644 --- a/test/governance/extensions/GovernorTimelockCompound.test.js +++ b/test/governance/extensions/GovernorTimelockCompound.test.js @@ -1,77 +1,71 @@ -const { ethers } = require('ethers'); -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); -const Enums = require('../../helpers/enums'); -const { GovernorHelper, proposalStatesToBitMap } = require('../../helpers/governance'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const { clockFromReceipt } = require('../../helpers/time'); - -const Timelock = artifacts.require('CompTimelock'); -const Governor = artifacts.require('$GovernorTimelockCompoundMock'); -const CallReceiver = artifacts.require('CallReceiverMock'); -const ERC721 = artifacts.require('$ERC721'); -const ERC1155 = artifacts.require('$ERC1155'); +const { GovernorHelper } = require('../../helpers/governance'); +const { bigint: Enums } = require('../../helpers/enums'); +const { bigint: time } = require('../../helpers/time'); const TOKENS = [ - { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, ]; -contract('GovernorTimelockCompound', function (accounts) { - const [owner, voter1, voter2, voter3, voter4, other] = accounts; - - const name = 'OZ-Governor'; - const version = '1'; - const tokenName = 'MockToken'; - const tokenSymbol = 'MTKN'; - const tokenSupply = web3.utils.toWei('100'); - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); - - const defaultDelay = 2 * 86400; - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); +const defaultDelay = time.duration.days(2n); + +describe('GovernorTimelockCompound', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [deployer, owner, voter1, voter2, voter3, voter4, other] = await ethers.getSigners(); + const receiver = await ethers.deployContract('CallReceiverMock'); + + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const predictGovernor = await deployer + .getNonce() + .then(nonce => ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 1 })); + const timelock = await ethers.deployContract('CompTimelock', [predictGovernor, defaultDelay]); + const mock = await ethers.deployContract('$GovernorTimelockCompoundMock', [ + name, + votingDelay, + votingPeriod, + 0n, + timelock, + token, + 0n, + ]); + + await owner.sendTransaction({ to: timelock, value }); + await token.$_mint(owner, tokenSupply); + + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') }); + + return { deployer, owner, voter1, voter2, voter3, voter4, other, receiver, token, mock, timelock, helper }; + }; + + describe(`using ${Token}`, function () { beforeEach(async function () { - const [deployer] = await web3.eth.getAccounts(); - - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - - // Need to predict governance address to set it as timelock admin with a delayed transfer - const nonce = await web3.eth.getTransactionCount(deployer); - const predictGovernor = ethers.getCreateAddress({ from: deployer, nonce: nonce + 1 }); - - this.timelock = await Timelock.new(predictGovernor, defaultDelay); - this.mock = await Governor.new( - name, - votingDelay, - votingPeriod, - 0, - this.timelock.address, - this.token.address, - 0, - ); - this.receiver = await CallReceiver.new(); - - this.helper = new GovernorHelper(this.mock, mode); - - await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); - - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + Object.assign(this, await loadFixture(fixture)); // default proposal this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, + target: this.receiver.target, value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), + data: this.receiver.interface.encodeFunctionData('mockFunction'), }, ], '', @@ -79,46 +73,55 @@ contract('GovernorTimelockCompound', function (accounts) { }); it("doesn't accept ether transfers", async function () { - await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 })); + await expect(this.owner.sendTransaction({ to: this.mock, value: 1n })).to.be.revertedWithCustomError( + this.mock, + 'GovernorDisabledDeposit', + ); }); it('post deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - - expect(await this.mock.timelock()).to.be.equal(this.timelock.address); - expect(await this.timelock.admin()).to.be.equal(this.mock.address); + expect(await this.mock.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token.target); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + expect(await this.mock.quorum(0n)).to.equal(0n); + + expect(await this.mock.timelock()).to.equal(this.timelock.target); + expect(await this.timelock.admin()).to.equal(this.mock.target); }); it('nominal', async function () { - expect(await this.mock.proposalEta(this.proposal.id)).to.be.bignumber.equal('0'); - expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.equal(true); + expect(await this.mock.proposalEta(this.proposal.id)).to.equal(0n); + expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.true; await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter3).vote({ support: Enums.VoteType.Against }); + await this.helper.connect(this.voter4).vote({ support: Enums.VoteType.Abstain }); await this.helper.waitForDeadline(); const txQueue = await this.helper.queue(); - const eta = web3.utils.toBN(await clockFromReceipt.timestamp(txQueue.receipt)).addn(defaultDelay); - expect(await this.mock.proposalEta(this.proposal.id)).to.be.bignumber.equal(eta); - expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.equal(true); + const eta = (await time.clockFromReceipt.timestamp(txQueue)) + defaultDelay; + expect(await this.mock.proposalEta(this.proposal.id)).to.equal(eta); + expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.true; await this.helper.waitForEta(); const txExecute = await this.helper.execute(); - expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txQueue.tx, this.timelock, 'QueueTransaction', { eta }); - - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txExecute.tx, this.timelock, 'ExecuteTransaction', { eta }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + await expect(txQueue) + .to.emit(this.mock, 'ProposalQueued') + .withArgs(this.proposal.id, eta) + .to.emit(this.timelock, 'QueueTransaction') + .withArgs(...Array(5).fill(anyValue), eta); + + await expect(txExecute) + .to.emit(this.mock, 'ProposalExecuted') + .withArgs(this.proposal.id) + .to.emit(this.timelock, 'ExecuteTransaction') + .withArgs(...Array(5).fill(anyValue), eta) + .to.emit(this.receiver, 'MockFunctionCalled'); }); describe('should revert', function () { @@ -126,29 +129,35 @@ contract('GovernorTimelockCompound', function (accounts) { it('if already queued', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Queued, - proposalStatesToBitMap([Enums.ProposalState.Succeeded]), - ]); + await expect(this.helper.queue()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Queued, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ); }); it('if proposal contains duplicate calls', async function () { const action = { - target: this.token.address, - data: this.token.contract.methods.approve(this.receiver.address, constants.MAX_UINT256).encodeABI(), + target: this.token.target, + data: this.token.interface.encodeFunctionData('approve', [this.receiver.target, ethers.MaxUint256]), }; const { id } = this.helper.setProposal([action, action], ''); await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - await expectRevertCustomError(this.helper.queue(), 'GovernorAlreadyQueuedProposal', [id]); - await expectRevertCustomError(this.helper.execute(), 'GovernorNotQueuedProposal', [id]); + await expect(this.helper.queue()) + .to.be.revertedWithCustomError(this.mock, 'GovernorAlreadyQueuedProposal') + .withArgs(id); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorNotQueuedProposal') + .withArgs(id); }); }); @@ -156,25 +165,26 @@ contract('GovernorTimelockCompound', function (accounts) { it('if not queued', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(+1); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.waitForDeadline(1n); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Succeeded); - await expectRevertCustomError(this.helper.execute(), 'GovernorNotQueuedProposal', [this.proposal.id]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorNotQueuedProposal') + .withArgs(this.proposal.id); }); it('if too early', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Queued); - await expectRevert( - this.helper.execute(), + await expect(this.helper.execute()).to.be.rejectedWith( "Timelock::executeTransaction: Transaction hasn't surpassed time lock", ); }); @@ -182,96 +192,86 @@ contract('GovernorTimelockCompound', function (accounts) { it('if too late', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - await this.helper.waitForEta(+30 * 86400); + await this.helper.waitForEta(time.duration.days(30)); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Expired); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Expired); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Expired, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Expired, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('if already executed', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); await this.helper.execute(); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Executed, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Executed, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); }); describe('on safe receive', function () { describe('ERC721', function () { - const name = 'Non Fungible Token'; - const symbol = 'NFT'; - const tokenId = web3.utils.toBN(1); + const tokenId = 1n; beforeEach(async function () { - this.token = await ERC721.new(name, symbol); - await this.token.$_mint(owner, tokenId); + this.token = await ethers.deployContract('$ERC721', ['Non Fungible Token', 'NFT']); + await this.token.$_mint(this.owner, tokenId); }); it("can't receive an ERC721 safeTransfer", async function () { - await expectRevertCustomError( - this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner }), - 'GovernorDisabledDeposit', - [], - ); + await expect( + this.token.connect(this.owner).safeTransferFrom(this.owner, this.mock, tokenId), + ).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit'); }); }); describe('ERC1155', function () { - const uri = 'https://token-cdn-domain/{id}.json'; const tokenIds = { - 1: web3.utils.toBN(1000), - 2: web3.utils.toBN(2000), - 3: web3.utils.toBN(3000), + 1: 1000n, + 2: 2000n, + 3: 3000n, }; beforeEach(async function () { - this.token = await ERC1155.new(uri); - await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); + this.token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']); + await this.token.$_mintBatch(this.owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); }); it("can't receive ERC1155 safeTransfer", async function () { - await expectRevertCustomError( - this.token.safeTransferFrom( - owner, - this.mock.address, + await expect( + this.token.connect(this.owner).safeTransferFrom( + this.owner, + this.mock, ...Object.entries(tokenIds)[0], // id + amount '0x', - { from: owner }, ), - 'GovernorDisabledDeposit', - [], - ); + ).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit'); }); it("can't receive ERC1155 safeBatchTransfer", async function () { - await expectRevertCustomError( - this.token.safeBatchTransferFrom( - owner, - this.mock.address, - Object.keys(tokenIds), - Object.values(tokenIds), - '0x', - { from: owner }, - ), - 'GovernorDisabledDeposit', - [], - ); + await expect( + this.token + .connect(this.owner) + .safeBatchTransferFrom(this.owner, this.mock, Object.keys(tokenIds), Object.values(tokenIds), '0x'), + ).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit'); }); }); }); @@ -281,111 +281,114 @@ contract('GovernorTimelockCompound', function (accounts) { it('cancel before queue prevents scheduling', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + await expect(this.helper.cancel('internal')) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded]), - ]); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Canceled); + + await expect(this.helper.queue()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ); }); it('cancel after queue prevents executing', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + await expect(this.helper.cancel('internal')) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Canceled); + + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); }); describe('onlyGovernance', function () { describe('relay', function () { beforeEach(async function () { - await this.token.$_mint(this.mock.address, 1); + await this.token.$_mint(this.mock, 1); }); it('is protected', async function () { - await expectRevertCustomError( - this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI(), { - from: owner, - }), - 'GovernorOnlyExecutor', - [owner], - ); + await expect( + this.mock + .connect(this.owner) + .relay(this.token, 0, this.token.interface.encodeFunctionData('transfer', [this.other.address, 1n])), + ) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); it('can be executed through governance', async function () { this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods - .relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()) - .encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('relay', [ + this.token.target, + 0n, + this.token.interface.encodeFunctionData('transfer', [this.other.address, 1n]), + ]), }, ], '', ); - expect(await this.token.balanceOf(this.mock.address), 1); - expect(await this.token.balanceOf(other), 0); - await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - expect(await this.token.balanceOf(this.mock.address), 0); - expect(await this.token.balanceOf(other), 1); + const txExecute = this.helper.execute(); - await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { - from: this.mock.address, - to: other, - value: '1', - }); + await expect(txExecute).to.changeTokenBalances(this.token, [this.mock, this.other], [-1n, 1n]); + + await expect(txExecute).to.emit(this.token, 'Transfer').withArgs(this.mock.target, this.other.address, 1n); }); }); describe('updateTimelock', function () { beforeEach(async function () { - this.newTimelock = await Timelock.new(this.mock.address, 7 * 86400); + this.newTimelock = await ethers.deployContract('CompTimelock', [this.mock, time.duration.days(7n)]); }); it('is protected', async function () { - await expectRevertCustomError( - this.mock.updateTimelock(this.newTimelock.address, { from: owner }), - 'GovernorOnlyExecutor', - [owner], - ); + await expect(this.mock.connect(this.owner).updateTimelock(this.newTimelock)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); it('can be executed through governance to', async function () { this.helper.setProposal( [ { - target: this.timelock.address, - data: this.timelock.contract.methods.setPendingAdmin(owner).encodeABI(), + target: this.timelock.target, + data: this.timelock.interface.encodeFunctionData('setPendingAdmin', [this.owner.address]), }, { - target: this.mock.address, - data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('updateTimelock', [this.newTimelock.target]), }, ], '', @@ -393,28 +396,35 @@ contract('GovernorTimelockCompound', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - expectEvent(txExecute, 'TimelockChange', { - oldTimelock: this.timelock.address, - newTimelock: this.newTimelock.address, - }); + await expect(this.helper.execute()) + .to.emit(this.mock, 'TimelockChange') + .withArgs(this.timelock.target, this.newTimelock.target); - expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); + expect(await this.mock.timelock()).to.equal(this.newTimelock.target); }); }); it('can transfer timelock to new governor', async function () { - const newGovernor = await Governor.new(name, 8, 32, 0, this.timelock.address, this.token.address, 0); + const newGovernor = await ethers.deployContract('$GovernorTimelockCompoundMock', [ + name, + 8n, + 32n, + 0n, + this.timelock, + this.token, + 0n, + ]); + this.helper.setProposal( [ { - target: this.timelock.address, - data: this.timelock.contract.methods.setPendingAdmin(newGovernor.address).encodeABI(), + target: this.timelock.target, + data: this.timelock.interface.encodeFunctionData('setPendingAdmin', [newGovernor.target]), }, ], '', @@ -422,18 +432,15 @@ contract('GovernorTimelockCompound', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - await expectEvent.inTransaction(txExecute.tx, this.timelock, 'NewPendingAdmin', { - newPendingAdmin: newGovernor.address, - }); + await expect(this.helper.execute()).to.emit(this.timelock, 'NewPendingAdmin').withArgs(newGovernor.target); await newGovernor.__acceptAdmin(); - expect(await this.timelock.admin()).to.be.bignumber.equal(newGovernor.address); + expect(await this.timelock.admin()).to.equal(newGovernor.target); }); }); }); diff --git a/test/governance/extensions/GovernorTimelockControl.test.js b/test/governance/extensions/GovernorTimelockControl.test.js index ec03d614475..9f6bceb5baa 100644 --- a/test/governance/extensions/GovernorTimelockControl.test.js +++ b/test/governance/extensions/GovernorTimelockControl.test.js @@ -1,88 +1,79 @@ -const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); -const Enums = require('../../helpers/enums'); -const { GovernorHelper, proposalStatesToBitMap, timelockSalt } = require('../../helpers/governance'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const { clockFromReceipt } = require('../../helpers/time'); - -const Timelock = artifacts.require('TimelockController'); -const Governor = artifacts.require('$GovernorTimelockControlMock'); -const CallReceiver = artifacts.require('CallReceiverMock'); -const ERC721 = artifacts.require('$ERC721'); -const ERC1155 = artifacts.require('$ERC1155'); +const { GovernorHelper, timelockSalt } = require('../../helpers/governance'); +const { bigint: Enums } = require('../../helpers/enums'); +const { bigint: time } = require('../../helpers/time'); const TOKENS = [ - { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, ]; -contract('GovernorTimelockControl', function (accounts) { - const [owner, voter1, voter2, voter3, voter4, other] = accounts; - - const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000'; - const PROPOSER_ROLE = web3.utils.soliditySha3('PROPOSER_ROLE'); - const EXECUTOR_ROLE = web3.utils.soliditySha3('EXECUTOR_ROLE'); - const CANCELLER_ROLE = web3.utils.soliditySha3('CANCELLER_ROLE'); - - const name = 'OZ-Governor'; - const version = '1'; - const tokenName = 'MockToken'; - const tokenSymbol = 'MTKN'; - const tokenSupply = web3.utils.toWei('100'); - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); - - const delay = 3600; - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { +const DEFAULT_ADMIN_ROLE = ethers.ZeroHash; +const PROPOSER_ROLE = ethers.id('PROPOSER_ROLE'); +const EXECUTOR_ROLE = ethers.id('EXECUTOR_ROLE'); +const CANCELLER_ROLE = ethers.id('CANCELLER_ROLE'); + +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); +const delay = time.duration.hours(1n); + +describe('GovernorTimelockControl', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [deployer, owner, voter1, voter2, voter3, voter4, other] = await ethers.getSigners(); + const receiver = await ethers.deployContract('CallReceiverMock'); + + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const timelock = await ethers.deployContract('TimelockController', [delay, [], [], deployer]); + const mock = await ethers.deployContract('$GovernorTimelockControlMock', [ + name, + votingDelay, + votingPeriod, + 0n, + timelock, + token, + 0n, + ]); + + await owner.sendTransaction({ to: timelock, value }); + await token.$_mint(owner, tokenSupply); + await timelock.grantRole(PROPOSER_ROLE, mock); + await timelock.grantRole(PROPOSER_ROLE, owner); + await timelock.grantRole(CANCELLER_ROLE, mock); + await timelock.grantRole(CANCELLER_ROLE, owner); + await timelock.grantRole(EXECUTOR_ROLE, ethers.ZeroAddress); + await timelock.revokeRole(DEFAULT_ADMIN_ROLE, deployer); + + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') }); + + return { deployer, owner, voter1, voter2, voter3, voter4, other, receiver, token, mock, timelock, helper }; + }; + + describe(`using ${Token}`, function () { beforeEach(async function () { - const [deployer] = await web3.eth.getAccounts(); - - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.timelock = await Timelock.new(delay, [], [], deployer); - this.mock = await Governor.new( - name, - votingDelay, - votingPeriod, - 0, - this.timelock.address, - this.token.address, - 0, - ); - this.receiver = await CallReceiver.new(); - - this.helper = new GovernorHelper(this.mock, mode); - - this.PROPOSER_ROLE = await this.timelock.PROPOSER_ROLE(); - this.EXECUTOR_ROLE = await this.timelock.EXECUTOR_ROLE(); - this.CANCELLER_ROLE = await this.timelock.CANCELLER_ROLE(); - - await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); - - // normal setup: governor is proposer, everyone is executor, timelock is its own admin - await this.timelock.grantRole(PROPOSER_ROLE, this.mock.address); - await this.timelock.grantRole(PROPOSER_ROLE, owner); - await this.timelock.grantRole(CANCELLER_ROLE, this.mock.address); - await this.timelock.grantRole(CANCELLER_ROLE, owner); - await this.timelock.grantRole(EXECUTOR_ROLE, constants.ZERO_ADDRESS); - await this.timelock.revokeRole(DEFAULT_ADMIN_ROLE, deployer); - - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + Object.assign(this, await loadFixture(fixture)); // default proposal this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, + target: this.receiver.target, value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), + data: this.receiver.interface.encodeFunctionData('mockFunction'), }, ], '', @@ -90,54 +81,63 @@ contract('GovernorTimelockControl', function (accounts) { this.proposal.timelockid = await this.timelock.hashOperationBatch( ...this.proposal.shortProposal.slice(0, 3), - '0x0', - timelockSalt(this.mock.address, this.proposal.shortProposal[3]), + ethers.ZeroHash, + timelockSalt(this.mock.target, this.proposal.shortProposal[3]), ); }); it("doesn't accept ether transfers", async function () { - await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 })); + await expect(this.owner.sendTransaction({ to: this.mock, value: 1n })).to.be.revertedWithCustomError( + this.mock, + 'GovernorDisabledDeposit', + ); }); it('post deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + expect(await this.mock.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token.target); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + expect(await this.mock.quorum(0n)).to.equal(0n); - expect(await this.mock.timelock()).to.be.equal(this.timelock.address); + expect(await this.mock.timelock()).to.equal(this.timelock.target); }); it('nominal', async function () { - expect(await this.mock.proposalEta(this.proposal.id)).to.be.bignumber.equal('0'); - expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.equal(true); + expect(await this.mock.proposalEta(this.proposal.id)).to.equal(0n); + expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.true; await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter3).vote({ support: Enums.VoteType.Against }); + await this.helper.connect(this.voter4).vote({ support: Enums.VoteType.Abstain }); await this.helper.waitForDeadline(); - const txQueue = await this.helper.queue(); - await this.helper.waitForEta(); - - const eta = web3.utils.toBN(await clockFromReceipt.timestamp(txQueue.receipt)).addn(delay); - expect(await this.mock.proposalEta(this.proposal.id)).to.be.bignumber.equal(eta); - expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.equal(true); - const txExecute = await this.helper.execute(); + expect(await this.mock.proposalNeedsQueuing(this.proposal.id)).to.be.true; + const txQueue = await this.helper.queue(); - expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { id: this.proposal.timelockid }); - await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallSalt', { - id: this.proposal.timelockid, - }); + const eta = (await time.clockFromReceipt.timestamp(txQueue)) + delay; + expect(await this.mock.proposalEta(this.proposal.id)).to.equal(eta); + await this.helper.waitForEta(); - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txExecute.tx, this.timelock, 'CallExecuted', { id: this.proposal.timelockid }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + const txExecute = this.helper.execute(); + + await expect(txQueue) + .to.emit(this.mock, 'ProposalQueued') + .withArgs(this.proposal.id, anyValue) + .to.emit(this.timelock, 'CallScheduled') + .withArgs(this.proposal.timelockid, ...Array(6).fill(anyValue)) + .to.emit(this.timelock, 'CallSalt') + .withArgs(this.proposal.timelockid, anyValue); + + await expect(txExecute) + .to.emit(this.mock, 'ProposalExecuted') + .withArgs(this.proposal.id) + .to.emit(this.timelock, 'CallExecuted') + .withArgs(this.proposal.timelockid, ...Array(4).fill(anyValue)) + .to.emit(this.receiver, 'MockFunctionCalled'); }); describe('should revert', function () { @@ -145,14 +145,16 @@ contract('GovernorTimelockControl', function (accounts) { it('if already queued', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Queued, - proposalStatesToBitMap([Enums.ProposalState.Succeeded]), - ]); + await expect(this.helper.queue()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Queued, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ); }); }); @@ -160,66 +162,69 @@ contract('GovernorTimelockControl', function (accounts) { it('if not queued', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(+1); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); + await this.helper.waitForDeadline(1n); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Succeeded); - await expectRevertCustomError(this.helper.execute(), 'TimelockUnexpectedOperationState', [ - this.proposal.timelockid, - proposalStatesToBitMap(Enums.OperationState.Ready), - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.timelock, 'TimelockUnexpectedOperationState') + .withArgs(this.proposal.timelockid, GovernorHelper.proposalStatesToBitMap(Enums.OperationState.Ready)); }); it('if too early', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Queued); - await expectRevertCustomError(this.helper.execute(), 'TimelockUnexpectedOperationState', [ - this.proposal.timelockid, - proposalStatesToBitMap(Enums.OperationState.Ready), - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.timelock, 'TimelockUnexpectedOperationState') + .withArgs(this.proposal.timelockid, GovernorHelper.proposalStatesToBitMap(Enums.OperationState.Ready)); }); it('if already executed', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); await this.helper.execute(); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Executed, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Executed, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('if already executed by another proposer', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); await this.timelock.executeBatch( ...this.proposal.shortProposal.slice(0, 3), - '0x0', - timelockSalt(this.mock.address, this.proposal.shortProposal[3]), + ethers.ZeroHash, + timelockSalt(this.mock.target, this.proposal.shortProposal[3]), ); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Executed, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Executed, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); }); }); @@ -228,178 +233,179 @@ contract('GovernorTimelockControl', function (accounts) { it('cancel before queue prevents scheduling', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + await expect(this.helper.cancel('internal')) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); + + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Canceled); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded]), - ]); + await expect(this.helper.queue()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ); }); it('cancel after queue prevents executing', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + await expect(this.helper.cancel('internal')) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Canceled); + + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Canceled, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); it('cancel on timelock is reflected on governor', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Queued); - expectEvent(await this.timelock.cancel(this.proposal.timelockid, { from: owner }), 'Cancelled', { - id: this.proposal.timelockid, - }); + await expect(this.timelock.connect(this.owner).cancel(this.proposal.timelockid)) + .to.emit(this.timelock, 'Cancelled') + .withArgs(this.proposal.timelockid); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + expect(await this.mock.state(this.proposal.id)).to.equal(Enums.ProposalState.Canceled); }); }); describe('onlyGovernance', function () { describe('relay', function () { beforeEach(async function () { - await this.token.$_mint(this.mock.address, 1); + await this.token.$_mint(this.mock, 1); }); it('is protected', async function () { - await expectRevertCustomError( - this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI(), { - from: owner, - }), - 'GovernorOnlyExecutor', - [owner], - ); + await expect( + this.mock + .connect(this.owner) + .relay(this.token, 0n, this.token.interface.encodeFunctionData('transfer', [this.other.address, 1n])), + ) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); it('can be executed through governance', async function () { this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods - .relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()) - .encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('relay', [ + this.token.target, + 0n, + this.token.interface.encodeFunctionData('transfer', [this.other.address, 1n]), + ]), }, ], '', ); - expect(await this.token.balanceOf(this.mock.address), 1); - expect(await this.token.balanceOf(other), 0); - await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); - expect(await this.token.balanceOf(this.mock.address), 0); - expect(await this.token.balanceOf(other), 1); + await expect(txExecute).to.changeTokenBalances(this.token, [this.mock, this.other], [-1n, 1n]); - await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { - from: this.mock.address, - to: other, - value: '1', - }); + await expect(txExecute).to.emit(this.token, 'Transfer').withArgs(this.mock.target, this.other.address, 1n); }); it('is payable and can transfer eth to EOA', async function () { - const t2g = web3.utils.toBN(128); // timelock to governor - const g2o = web3.utils.toBN(100); // governor to eoa (other) + const t2g = 128n; // timelock to governor + const g2o = 100n; // governor to eoa (other) this.helper.setProposal( [ { - target: this.mock.address, + target: this.mock.target, value: t2g, - data: this.mock.contract.methods.relay(other, g2o, '0x').encodeABI(), + data: this.mock.interface.encodeFunctionData('relay', [this.other.address, g2o, '0x']), }, ], '', ); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - const timelockBalance = await web3.eth.getBalance(this.timelock.address).then(web3.utils.toBN); - const otherBalance = await web3.eth.getBalance(other).then(web3.utils.toBN); - await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); - await this.helper.execute(); - expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(timelockBalance.sub(t2g)); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(t2g.sub(g2o)); - expect(await web3.eth.getBalance(other)).to.be.bignumber.equal(otherBalance.add(g2o)); + await expect(this.helper.execute()).to.changeEtherBalances( + [this.timelock, this.mock, this.other], + [-t2g, t2g - g2o, g2o], + ); }); it('protected against other proposers', async function () { - const target = this.mock.address; - const value = web3.utils.toWei('0'); - const data = this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(); - const predecessor = constants.ZERO_BYTES32; - const salt = constants.ZERO_BYTES32; - - await this.timelock.schedule(target, value, data, predecessor, salt, delay, { from: owner }); - - await time.increase(delay); - - await expectRevertCustomError( - this.timelock.execute(target, value, data, predecessor, salt, { from: owner }), - 'QueueEmpty', // Bubbled up from Governor - [], + const call = [ + this.mock, + 0n, + this.mock.interface.encodeFunctionData('relay', [ethers.ZeroAddress, 0n, '0x']), + ethers.ZeroHash, + ethers.ZeroHash, + ]; + + await this.timelock.connect(this.owner).schedule(...call, delay); + + await time.clock.timestamp().then(clock => time.forward.timestamp(clock + delay)); + + // Error bubbled up from Governor + await expect(this.timelock.connect(this.owner).execute(...call)).to.be.revertedWithCustomError( + this.mock, + 'QueueEmpty', ); }); }); describe('updateTimelock', function () { beforeEach(async function () { - this.newTimelock = await Timelock.new( + this.newTimelock = await ethers.deployContract('TimelockController', [ delay, - [this.mock.address], - [this.mock.address], - constants.ZERO_ADDRESS, - ); + [this.mock], + [this.mock], + ethers.ZeroAddress, + ]); }); it('is protected', async function () { - await expectRevertCustomError( - this.mock.updateTimelock(this.newTimelock.address, { from: owner }), - 'GovernorOnlyExecutor', - [owner], - ); + await expect(this.mock.connect(this.owner).updateTimelock(this.newTimelock)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); it('can be executed through governance to', async function () { this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('updateTimelock', [this.newTimelock.target]), }, ], '', @@ -407,81 +413,64 @@ contract('GovernorTimelockControl', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - expectEvent(txExecute, 'TimelockChange', { - oldTimelock: this.timelock.address, - newTimelock: this.newTimelock.address, - }); + await expect(this.helper.execute()) + .to.emit(this.mock, 'TimelockChange') + .withArgs(this.timelock.target, this.newTimelock.target); - expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); + expect(await this.mock.timelock()).to.equal(this.newTimelock.target); }); }); describe('on safe receive', function () { describe('ERC721', function () { - const name = 'Non Fungible Token'; - const symbol = 'NFT'; - const tokenId = web3.utils.toBN(1); + const tokenId = 1n; beforeEach(async function () { - this.token = await ERC721.new(name, symbol); - await this.token.$_mint(owner, tokenId); + this.token = await ethers.deployContract('$ERC721', ['Non Fungible Token', 'NFT']); + await this.token.$_mint(this.owner, tokenId); }); it("can't receive an ERC721 safeTransfer", async function () { - await expectRevertCustomError( - this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner }), - 'GovernorDisabledDeposit', - [], - ); + await expect( + this.token.connect(this.owner).safeTransferFrom(this.owner, this.mock, tokenId), + ).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit'); }); }); describe('ERC1155', function () { - const uri = 'https://token-cdn-domain/{id}.json'; const tokenIds = { - 1: web3.utils.toBN(1000), - 2: web3.utils.toBN(2000), - 3: web3.utils.toBN(3000), + 1: 1000n, + 2: 2000n, + 3: 3000n, }; beforeEach(async function () { - this.token = await ERC1155.new(uri); - await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); + this.token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']); + await this.token.$_mintBatch(this.owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); }); it("can't receive ERC1155 safeTransfer", async function () { - await expectRevertCustomError( - this.token.safeTransferFrom( - owner, - this.mock.address, + await expect( + this.token.connect(this.owner).safeTransferFrom( + this.owner, + this.mock, ...Object.entries(tokenIds)[0], // id + amount '0x', - { from: owner }, ), - 'GovernorDisabledDeposit', - [], - ); + ).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit'); }); it("can't receive ERC1155 safeBatchTransfer", async function () { - await expectRevertCustomError( - this.token.safeBatchTransferFrom( - owner, - this.mock.address, - Object.keys(tokenIds), - Object.values(tokenIds), - '0x', - { from: owner }, - ), - 'GovernorDisabledDeposit', - [], - ); + await expect( + this.token + .connect(this.owner) + .safeBatchTransferFrom(this.owner, this.mock, Object.keys(tokenIds), Object.values(tokenIds), '0x'), + ).to.be.revertedWithCustomError(this.mock, 'GovernorDisabledDeposit'); }); }); }); @@ -491,8 +480,8 @@ contract('GovernorTimelockControl', function (accounts) { this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.nonGovernanceFunction().encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('nonGovernanceFunction'), }, ], '', @@ -500,7 +489,7 @@ contract('GovernorTimelockControl', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); diff --git a/test/governance/extensions/GovernorVotesQuorumFraction.test.js b/test/governance/extensions/GovernorVotesQuorumFraction.test.js index ece9c78d6f1..cae4c76f0fa 100644 --- a/test/governance/extensions/GovernorVotesQuorumFraction.test.js +++ b/test/governance/extensions/GovernorVotesQuorumFraction.test.js @@ -1,58 +1,60 @@ -const { expectEvent, time } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture, mine } = require('@nomicfoundation/hardhat-network-helpers'); -const Enums = require('../../helpers/enums'); -const { GovernorHelper, proposalStatesToBitMap } = require('../../helpers/governance'); -const { clock } = require('../../helpers/time'); -const { expectRevertCustomError } = require('../../helpers/customError'); - -const Governor = artifacts.require('$GovernorMock'); -const CallReceiver = artifacts.require('CallReceiverMock'); +const { GovernorHelper } = require('../../helpers/governance'); +const { bigint: Enums } = require('../../helpers/enums'); +const { bigint: time } = require('../../helpers/time'); const TOKENS = [ - { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, ]; -contract('GovernorVotesQuorumFraction', function (accounts) { - const [owner, voter1, voter2, voter3, voter4] = accounts; - - const name = 'OZ-Governor'; - const version = '1'; - const tokenName = 'MockToken'; - const tokenSymbol = 'MTKN'; - const tokenSupply = web3.utils.toBN(web3.utils.toWei('100')); - const ratio = web3.utils.toBN(8); // percents - const newRatio = web3.utils.toBN(6); // percents - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { - beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.mock = await Governor.new(name, votingDelay, votingPeriod, 0, this.token.address, ratio); - this.receiver = await CallReceiver.new(); +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const ratio = 8n; // percents +const newRatio = 6n; // percents +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); + +describe('GovernorVotesQuorumFraction', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [owner, voter1, voter2, voter3, voter4] = await ethers.getSigners(); + + const receiver = await ethers.deployContract('CallReceiverMock'); + + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const mock = await ethers.deployContract('$GovernorMock', [name, votingDelay, votingPeriod, 0n, token, ratio]); - this.helper = new GovernorHelper(this.mock, mode); + await owner.sendTransaction({ to: mock, value }); + await token.$_mint(owner, tokenSupply); - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') }); - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + return { owner, voter1, voter2, voter3, voter4, receiver, token, mock, helper }; + }; + + describe(`using ${Token}`, function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); // default proposal this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, + target: this.receiver.target, value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), + data: this.receiver.interface.encodeFunctionData('mockFunction'), }, ], '', @@ -60,22 +62,22 @@ contract('GovernorVotesQuorumFraction', function (accounts) { }); it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(ratio); - expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100'); - expect(await clock[mode]().then(timepoint => this.mock.quorum(timepoint - 1))).to.be.bignumber.equal( - tokenSupply.mul(ratio).divn(100), + expect(await this.mock.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token.target); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.equal(0n); + expect(await this.mock.quorumNumerator()).to.equal(ratio); + expect(await this.mock.quorumDenominator()).to.equal(100n); + expect(await time.clock[mode]().then(clock => this.mock.quorum(clock - 1n))).to.equal( + (tokenSupply * ratio) / 100n, ); }); it('quroum reached', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); await this.helper.execute(); }); @@ -83,30 +85,30 @@ contract('GovernorVotesQuorumFraction', function (accounts) { it('quroum not reached', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Defeated, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') + .withArgs( + this.proposal.id, + Enums.ProposalState.Defeated, + GovernorHelper.proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ); }); describe('onlyGovernance updates', function () { it('updateQuorumNumerator is protected', async function () { - await expectRevertCustomError( - this.mock.updateQuorumNumerator(newRatio, { from: owner }), - 'GovernorOnlyExecutor', - [owner], - ); + await expect(this.mock.connect(this.owner).updateQuorumNumerator(newRatio)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.owner.address); }); it('can updateQuorumNumerator through governance', async function () { this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.updateQuorumNumerator(newRatio).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('updateQuorumNumerator', [newRatio]), }, ], '', @@ -114,36 +116,33 @@ contract('GovernorVotesQuorumFraction', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - expectEvent(await this.helper.execute(), 'QuorumNumeratorUpdated', { - oldQuorumNumerator: ratio, - newQuorumNumerator: newRatio, - }); + await expect(this.helper.execute()).to.emit(this.mock, 'QuorumNumeratorUpdated').withArgs(ratio, newRatio); - expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(newRatio); - expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100'); + expect(await this.mock.quorumNumerator()).to.equal(newRatio); + expect(await this.mock.quorumDenominator()).to.equal(100n); // it takes one block for the new quorum to take effect - expect(await clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1))).to.be.bignumber.equal( - tokenSupply.mul(ratio).divn(100), + expect(await time.clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1n))).to.equal( + (tokenSupply * ratio) / 100n, ); - await time.advanceBlock(); + await mine(); - expect(await clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1))).to.be.bignumber.equal( - tokenSupply.mul(newRatio).divn(100), + expect(await time.clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1n))).to.equal( + (tokenSupply * newRatio) / 100n, ); }); it('cannot updateQuorumNumerator over the maximum', async function () { - const quorumNumerator = 101; + const quorumNumerator = 101n; this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods.updateQuorumNumerator(quorumNumerator).encodeABI(), + target: this.mock.target, + data: this.mock.interface.encodeFunctionData('updateQuorumNumerator', [quorumNumerator]), }, ], '', @@ -151,15 +150,14 @@ contract('GovernorVotesQuorumFraction', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); const quorumDenominator = await this.mock.quorumDenominator(); - await expectRevertCustomError(this.helper.execute(), 'GovernorInvalidQuorumFraction', [ - quorumNumerator, - quorumDenominator, - ]); + await expect(this.helper.execute()) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidQuorumFraction') + .withArgs(quorumNumerator, quorumDenominator); }); }); }); diff --git a/test/governance/extensions/GovernorWithParams.test.js b/test/governance/extensions/GovernorWithParams.test.js index bbac688a23c..194a8aa6f1e 100644 --- a/test/governance/extensions/GovernorWithParams.test.js +++ b/test/governance/extensions/GovernorWithParams.test.js @@ -1,66 +1,62 @@ -const { expectEvent } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const Enums = require('../../helpers/enums'); -const { getDomain, domainType, ExtendedBallot } = require('../../helpers/eip712'); const { GovernorHelper } = require('../../helpers/governance'); -const { expectRevertCustomError } = require('../../helpers/customError'); - -const Governor = artifacts.require('$GovernorWithParamsMock'); -const CallReceiver = artifacts.require('CallReceiverMock'); -const ERC1271WalletMock = artifacts.require('ERC1271WalletMock'); - -const rawParams = { - uintParam: web3.utils.toBN('42'), - strParam: 'These are my params', -}; - -const encodedParams = web3.eth.abi.encodeParameters(['uint256', 'string'], Object.values(rawParams)); +const { bigint: Enums } = require('../../helpers/enums'); +const { getDomain, ExtendedBallot } = require('../../helpers/eip712'); const TOKENS = [ - { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, ]; -contract('GovernorWithParams', function (accounts) { - const [owner, proposer, voter1, voter2, voter3, voter4] = accounts; +const name = 'OZ-Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); + +const params = { + decoded: [42n, 'These are my params'], + encoded: ethers.AbiCoder.defaultAbiCoder().encode(['uint256', 'string'], [42n, 'These are my params']), +}; - const name = 'OZ-Governor'; - const version = '1'; - const tokenName = 'MockToken'; - const tokenSymbol = 'MTKN'; - const tokenSupply = web3.utils.toWei('100'); - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); +describe('GovernorWithParams', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [owner, proposer, voter1, voter2, voter3, voter4, other] = await ethers.getSigners(); + const receiver = await ethers.deployContract('CallReceiverMock'); - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { - beforeEach(async function () { - this.chainId = await web3.eth.getChainId(); - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.mock = await Governor.new(name, this.token.address); - this.receiver = await CallReceiver.new(); + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const mock = await ethers.deployContract('$GovernorWithParamsMock', [name, token]); - this.helper = new GovernorHelper(this.mock, mode); + await owner.sendTransaction({ to: mock, value }); + await token.$_mint(owner, tokenSupply); - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') }); - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + return { owner, proposer, voter1, voter2, voter3, voter4, other, receiver, token, mock, helper }; + }; + + describe(`using ${Token}`, function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); // default proposal this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, + target: this.receiver.target, value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), + data: this.receiver.interface.encodeFunctionData('mockFunction'), }, ], '', @@ -68,201 +64,180 @@ contract('GovernorWithParams', function (accounts) { }); it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token.target); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); }); it('nominal is unaffected', async function () { - await this.helper.propose({ from: proposer }); + await this.helper.connect(this.proposer).propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For, reason: 'This is nice' }); + await this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For }); + await this.helper.connect(this.voter3).vote({ support: Enums.VoteType.Against }); + await this.helper.connect(this.voter4).vote({ support: Enums.VoteType.Abstain }); await this.helper.waitForDeadline(); await this.helper.execute(); - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); - expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value); + expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.true; + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.receiver)).to.equal(value); }); it('Voting with params is properly supported', async function () { - await this.helper.propose({ from: proposer }); + await this.helper.connect(this.proposer).propose(); await this.helper.waitForSnapshot(); - const weight = web3.utils.toBN(web3.utils.toWei('7')).sub(rawParams.uintParam); + const weight = ethers.parseEther('7') - params.decoded[0]; - const tx = await this.helper.vote( - { + await expect( + this.helper.connect(this.voter2).vote({ support: Enums.VoteType.For, reason: 'no particular reason', - params: encodedParams, - }, - { from: voter2 }, - ); - - expectEvent(tx, 'CountParams', { ...rawParams }); - expectEvent(tx, 'VoteCastWithParams', { - voter: voter2, - proposalId: this.proposal.id, - support: Enums.VoteType.For, - weight, - reason: 'no particular reason', - params: encodedParams, - }); + params: params.encoded, + }), + ) + .to.emit(this.mock, 'CountParams') + .withArgs(...params.decoded) + .to.emit(this.mock, 'VoteCastWithParams') + .withArgs( + this.voter2.address, + this.proposal.id, + Enums.VoteType.For, + weight, + 'no particular reason', + params.encoded, + ); - const votes = await this.mock.proposalVotes(this.proposal.id); - expect(votes.forVotes).to.be.bignumber.equal(weight); + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, weight, 0n]); }); describe('voting by signature', function () { - beforeEach(async function () { - this.voterBySig = Wallet.generate(); - this.voterBySig.address = web3.utils.toChecksumAddress(this.voterBySig.getAddressString()); - - this.data = (contract, message) => - getDomain(contract).then(domain => ({ - primaryType: 'ExtendedBallot', - types: { - EIP712Domain: domainType(domain), - ExtendedBallot, - }, - domain, - message, - })); - - this.sign = privateKey => async (contract, message) => - ethSigUtil.signTypedMessage(privateKey, { data: await this.data(contract, message) }); - }); - it('supports EOA signatures', async function () { - await this.token.delegate(this.voterBySig.address, { from: voter2 }); - - const weight = web3.utils.toBN(web3.utils.toWei('7')).sub(rawParams.uintParam); - - const nonce = await this.mock.nonces(this.voterBySig.address); + await this.token.connect(this.voter2).delegate(this.other); // Run proposal await this.helper.propose(); await this.helper.waitForSnapshot(); - const tx = await this.helper.vote({ - support: Enums.VoteType.For, - voter: this.voterBySig.address, - nonce, - reason: 'no particular reason', - params: encodedParams, - signature: this.sign(this.voterBySig.getPrivateKey()), - }); - expectEvent(tx, 'CountParams', { ...rawParams }); - expectEvent(tx, 'VoteCastWithParams', { - voter: this.voterBySig.address, + // Prepare vote + const weight = ethers.parseEther('7') - params.decoded[0]; + const nonce = await this.mock.nonces(this.other); + const data = { proposalId: this.proposal.id, support: Enums.VoteType.For, - weight, + voter: this.other.address, + nonce, reason: 'no particular reason', - params: encodedParams, - }); + params: params.encoded, + signature: (contract, message) => + getDomain(contract).then(domain => this.other.signTypedData(domain, { ExtendedBallot }, message)), + }; + + // Vote + await expect(this.helper.vote(data)) + .to.emit(this.mock, 'CountParams') + .withArgs(...params.decoded) + .to.emit(this.mock, 'VoteCastWithParams') + .withArgs(data.voter, data.proposalId, data.support, weight, data.reason, data.params); - const votes = await this.mock.proposalVotes(this.proposal.id); - expect(votes.forVotes).to.be.bignumber.equal(weight); - expect(await this.mock.nonces(this.voterBySig.address)).to.be.bignumber.equal(nonce.addn(1)); + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, weight, 0n]); + expect(await this.mock.nonces(this.other)).to.equal(nonce + 1n); }); it('supports EIP-1271 signature signatures', async function () { - const ERC1271WalletOwner = Wallet.generate(); - ERC1271WalletOwner.address = web3.utils.toChecksumAddress(ERC1271WalletOwner.getAddressString()); - - const wallet = await ERC1271WalletMock.new(ERC1271WalletOwner.address); - - await this.token.delegate(wallet.address, { from: voter2 }); - - const weight = web3.utils.toBN(web3.utils.toWei('7')).sub(rawParams.uintParam); - - const nonce = await this.mock.nonces(wallet.address); + const wallet = await ethers.deployContract('ERC1271WalletMock', [this.other]); + await this.token.connect(this.voter2).delegate(wallet); // Run proposal await this.helper.propose(); await this.helper.waitForSnapshot(); - const tx = await this.helper.vote({ - support: Enums.VoteType.For, - voter: wallet.address, - nonce, - reason: 'no particular reason', - params: encodedParams, - signature: this.sign(ERC1271WalletOwner.getPrivateKey()), - }); - expectEvent(tx, 'CountParams', { ...rawParams }); - expectEvent(tx, 'VoteCastWithParams', { - voter: wallet.address, + // Prepare vote + const weight = ethers.parseEther('7') - params.decoded[0]; + const nonce = await this.mock.nonces(this.other); + const data = { proposalId: this.proposal.id, support: Enums.VoteType.For, - weight, + voter: wallet.target, + nonce, reason: 'no particular reason', - params: encodedParams, - }); + params: params.encoded, + signature: (contract, message) => + getDomain(contract).then(domain => this.other.signTypedData(domain, { ExtendedBallot }, message)), + }; + + // Vote + await expect(this.helper.vote(data)) + .to.emit(this.mock, 'CountParams') + .withArgs(...params.decoded) + .to.emit(this.mock, 'VoteCastWithParams') + .withArgs(data.voter, data.proposalId, data.support, weight, data.reason, data.params); - const votes = await this.mock.proposalVotes(this.proposal.id); - expect(votes.forVotes).to.be.bignumber.equal(weight); - expect(await this.mock.nonces(wallet.address)).to.be.bignumber.equal(nonce.addn(1)); + expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, weight, 0n]); + expect(await this.mock.nonces(wallet)).to.equal(nonce + 1n); }); it('reverts if signature does not match signer', async function () { - await this.token.delegate(this.voterBySig.address, { from: voter2 }); - - const nonce = await this.mock.nonces(this.voterBySig.address); - - const signature = this.sign(this.voterBySig.getPrivateKey()); + await this.token.connect(this.voter2).delegate(this.other); // Run proposal await this.helper.propose(); await this.helper.waitForSnapshot(); - const voteParams = { + + // Prepare vote + const nonce = await this.mock.nonces(this.other); + const data = { + proposalId: this.proposal.id, support: Enums.VoteType.For, - voter: this.voterBySig.address, + voter: this.other.address, nonce, - signature: async (...params) => { - const sig = await signature(...params); - const tamperedSig = web3.utils.hexToBytes(sig); - tamperedSig[42] ^= 0xff; - return web3.utils.bytesToHex(tamperedSig); - }, reason: 'no particular reason', - params: encodedParams, + params: params.encoded, + // tampered signature + signature: (contract, message) => + getDomain(contract) + .then(domain => this.other.signTypedData(domain, { ExtendedBallot }, message)) + .then(signature => { + const tamperedSig = ethers.toBeArray(signature); + tamperedSig[42] ^= 0xff; + return ethers.hexlify(tamperedSig); + }), }; - await expectRevertCustomError(this.helper.vote(voteParams), 'GovernorInvalidSignature', [voteParams.voter]); + // Vote + await expect(this.helper.vote(data)) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature') + .withArgs(data.voter); }); it('reverts if vote nonce is incorrect', async function () { - await this.token.delegate(this.voterBySig.address, { from: voter2 }); - - const nonce = await this.mock.nonces(this.voterBySig.address); + await this.token.connect(this.voter2).delegate(this.other); // Run proposal await this.helper.propose(); await this.helper.waitForSnapshot(); - const voteParams = { + + // Prepare vote + const nonce = await this.mock.nonces(this.other); + const data = { + proposalId: this.proposal.id, support: Enums.VoteType.For, - voter: this.voterBySig.address, - nonce: nonce.addn(1), - signature: this.sign(this.voterBySig.getPrivateKey()), + voter: this.other.address, + nonce: nonce + 1n, reason: 'no particular reason', - params: encodedParams, + params: params.encoded, + signature: (contract, message) => + getDomain(contract).then(domain => this.other.signTypedData(domain, { ExtendedBallot }, message)), }; - await expectRevertCustomError( - this.helper.vote(voteParams), - // The signature check implies the nonce can't be tampered without changing the signer - 'GovernorInvalidSignature', - [voteParams.voter], - ); + // Vote + await expect(this.helper.vote(data)) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature') + .withArgs(data.voter); }); }); }); diff --git a/test/governance/utils/ERC6372.behavior.js b/test/governance/utils/ERC6372.behavior.js index 5e8633f01cd..b5a6cb13c0b 100644 --- a/test/governance/utils/ERC6372.behavior.js +++ b/test/governance/utils/ERC6372.behavior.js @@ -1,19 +1,19 @@ -const { clock } = require('../../helpers/time'); +const { bigint: time } = require('../../helpers/time'); function shouldBehaveLikeERC6372(mode = 'blocknumber') { - describe('should implement ERC6372', function () { + describe('should implement ERC-6372', function () { beforeEach(async function () { this.mock = this.mock ?? this.token ?? this.votes; }); it('clock is correct', async function () { - expect(await this.mock.clock()).to.be.bignumber.equal(await clock[mode]().then(web3.utils.toBN)); + expect(await this.mock.clock()).to.equal(await time.clock[mode]()); }); it('CLOCK_MODE is correct', async function () { const params = new URLSearchParams(await this.mock.CLOCK_MODE()); - expect(params.get('mode')).to.be.equal(mode); - expect(params.get('from')).to.be.equal(mode == 'blocknumber' ? 'default' : null); + expect(params.get('mode')).to.equal(mode); + expect(params.get('from')).to.equal(mode == 'blocknumber' ? 'default' : null); }); }); } diff --git a/test/governance/utils/Votes.behavior.js b/test/governance/utils/Votes.behavior.js index 6243cf4e447..a08f184c8ac 100644 --- a/test/governance/utils/Votes.behavior.js +++ b/test/governance/utils/Votes.behavior.js @@ -1,303 +1,277 @@ -const { constants, expectEvent, time } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { mine } = require('@nomicfoundation/hardhat-network-helpers'); -const { MAX_UINT256, ZERO_ADDRESS } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; +const { bigint: time } = require('../../helpers/time'); +const { getDomain, Delegation } = require('../../helpers/eip712'); const { shouldBehaveLikeERC6372 } = require('./ERC6372.behavior'); -const { getDomain, domainType, Delegation } = require('../../helpers/eip712'); -const { clockFromReceipt } = require('../../helpers/time'); -const { expectRevertCustomError } = require('../../helpers/customError'); - -const buildAndSignDelegation = (contract, message, pk) => - getDomain(contract) - .then(domain => ({ - primaryType: 'Delegation', - types: { EIP712Domain: domainType(domain), Delegation }, - domain, - message, - })) - .then(data => fromRpcSig(ethSigUtil.signTypedMessage(pk, { data }))); - -function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungible = true }) { + +function shouldBehaveLikeVotes(tokens, { mode = 'blocknumber', fungible = true }) { + beforeEach(async function () { + [this.delegator, this.delegatee, this.alice, this.bob, this.other] = this.accounts; + this.domain = await getDomain(this.votes); + }); + shouldBehaveLikeERC6372(mode); - const getWeight = token => web3.utils.toBN(fungible ? token : 1); + const getWeight = token => (fungible ? token : 1n); describe('run votes workflow', function () { it('initial nonce is 0', async function () { - expect(await this.votes.nonces(accounts[0])).to.be.bignumber.equal('0'); + expect(await this.votes.nonces(this.alice)).to.equal(0n); }); describe('delegation with signature', function () { const token = tokens[0]; it('delegation without tokens', async function () { - expect(await this.votes.delegates(accounts[1])).to.be.equal(ZERO_ADDRESS); + expect(await this.votes.delegates(this.alice)).to.equal(ethers.ZeroAddress); - const { receipt } = await this.votes.delegate(accounts[1], { from: accounts[1] }); - expectEvent(receipt, 'DelegateChanged', { - delegator: accounts[1], - fromDelegate: ZERO_ADDRESS, - toDelegate: accounts[1], - }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + await expect(this.votes.connect(this.alice).delegate(this.alice)) + .to.emit(this.votes, 'DelegateChanged') + .withArgs(this.alice.address, ethers.ZeroAddress, this.alice.address) + .to.not.emit(this.votes, 'DelegateVotesChanged'); - expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[1]); + expect(await this.votes.delegates(this.alice)).to.equal(this.alice.address); }); it('delegation with tokens', async function () { - await this.votes.$_mint(accounts[1], token); + await this.votes.$_mint(this.alice, token); const weight = getWeight(token); - expect(await this.votes.delegates(accounts[1])).to.be.equal(ZERO_ADDRESS); + expect(await this.votes.delegates(this.alice)).to.equal(ethers.ZeroAddress); - const { receipt } = await this.votes.delegate(accounts[1], { from: accounts[1] }); - const timepoint = await clockFromReceipt[mode](receipt); + const tx = await this.votes.connect(this.alice).delegate(this.alice); + const timepoint = await time.clockFromReceipt[mode](tx); - expectEvent(receipt, 'DelegateChanged', { - delegator: accounts[1], - fromDelegate: ZERO_ADDRESS, - toDelegate: accounts[1], - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: accounts[1], - previousVotes: '0', - newVotes: weight, - }); + await expect(tx) + .to.emit(this.votes, 'DelegateChanged') + .withArgs(this.alice.address, ethers.ZeroAddress, this.alice.address) + .to.emit(this.votes, 'DelegateVotesChanged') + .withArgs(this.alice.address, 0n, weight); - expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[1]); - expect(await this.votes.getVotes(accounts[1])).to.be.bignumber.equal(weight); - expect(await this.votes.getPastVotes(accounts[1], timepoint - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.votes.getPastVotes(accounts[1], timepoint)).to.be.bignumber.equal(weight); + expect(await this.votes.delegates(this.alice)).to.equal(this.alice.address); + expect(await this.votes.getVotes(this.alice)).to.equal(weight); + expect(await this.votes.getPastVotes(this.alice, timepoint - 1n)).to.equal(0n); + await mine(); + expect(await this.votes.getPastVotes(this.alice, timepoint)).to.equal(weight); }); it('delegation update', async function () { - await this.votes.delegate(accounts[1], { from: accounts[1] }); - await this.votes.$_mint(accounts[1], token); + await this.votes.connect(this.alice).delegate(this.alice); + await this.votes.$_mint(this.alice, token); const weight = getWeight(token); - expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[1]); - expect(await this.votes.getVotes(accounts[1])).to.be.bignumber.equal(weight); - expect(await this.votes.getVotes(accounts[2])).to.be.bignumber.equal('0'); - - const { receipt } = await this.votes.delegate(accounts[2], { from: accounts[1] }); - const timepoint = await clockFromReceipt[mode](receipt); - - expectEvent(receipt, 'DelegateChanged', { - delegator: accounts[1], - fromDelegate: accounts[1], - toDelegate: accounts[2], - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: accounts[1], - previousVotes: weight, - newVotes: '0', - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: accounts[2], - previousVotes: '0', - newVotes: weight, - }); - - expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[2]); - expect(await this.votes.getVotes(accounts[1])).to.be.bignumber.equal('0'); - expect(await this.votes.getVotes(accounts[2])).to.be.bignumber.equal(weight); - - expect(await this.votes.getPastVotes(accounts[1], timepoint - 1)).to.be.bignumber.equal(weight); - expect(await this.votes.getPastVotes(accounts[2], timepoint - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.votes.getPastVotes(accounts[1], timepoint)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastVotes(accounts[2], timepoint)).to.be.bignumber.equal(weight); + expect(await this.votes.delegates(this.alice)).to.equal(this.alice.address); + expect(await this.votes.getVotes(this.alice)).to.equal(weight); + expect(await this.votes.getVotes(this.bob)).to.equal(0); + + const tx = await this.votes.connect(this.alice).delegate(this.bob); + const timepoint = await time.clockFromReceipt[mode](tx); + + await expect(tx) + .to.emit(this.votes, 'DelegateChanged') + .withArgs(this.alice.address, this.alice.address, this.bob.address) + .to.emit(this.votes, 'DelegateVotesChanged') + .withArgs(this.alice.address, weight, 0) + .to.emit(this.votes, 'DelegateVotesChanged') + .withArgs(this.bob.address, 0, weight); + + expect(await this.votes.delegates(this.alice)).to.equal(this.bob.address); + expect(await this.votes.getVotes(this.alice)).to.equal(0n); + expect(await this.votes.getVotes(this.bob)).to.equal(weight); + + expect(await this.votes.getPastVotes(this.alice, timepoint - 1n)).to.equal(weight); + expect(await this.votes.getPastVotes(this.bob, timepoint - 1n)).to.equal(0n); + await mine(); + expect(await this.votes.getPastVotes(this.alice, timepoint)).to.equal(0n); + expect(await this.votes.getPastVotes(this.bob, timepoint)).to.equal(weight); }); describe('with signature', function () { - const delegator = Wallet.generate(); - const [delegatee, other] = accounts; - const nonce = 0; - delegator.address = web3.utils.toChecksumAddress(delegator.getAddressString()); + const nonce = 0n; it('accept signed delegation', async function () { - await this.votes.$_mint(delegator.address, token); + await this.votes.$_mint(this.delegator.address, token); const weight = getWeight(token); - const { v, r, s } = await buildAndSignDelegation( - this.votes, - { - delegatee, - nonce, - expiry: MAX_UINT256, - }, - delegator.getPrivateKey(), - ); - - expect(await this.votes.delegates(delegator.address)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s); - const timepoint = await clockFromReceipt[mode](receipt); - - expectEvent(receipt, 'DelegateChanged', { - delegator: delegator.address, - fromDelegate: ZERO_ADDRESS, - toDelegate: delegatee, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: delegatee, - previousVotes: '0', - newVotes: weight, - }); - - expect(await this.votes.delegates(delegator.address)).to.be.equal(delegatee); - expect(await this.votes.getVotes(delegator.address)).to.be.bignumber.equal('0'); - expect(await this.votes.getVotes(delegatee)).to.be.bignumber.equal(weight); - expect(await this.votes.getPastVotes(delegatee, timepoint - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.votes.getPastVotes(delegatee, timepoint)).to.be.bignumber.equal(weight); + const { r, s, v } = await this.delegator + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.delegatee.address, + nonce, + expiry: ethers.MaxUint256, + }, + ) + .then(ethers.Signature.from); + + expect(await this.votes.delegates(this.delegator.address)).to.equal(ethers.ZeroAddress); + + const tx = await this.votes.delegateBySig(this.delegatee, nonce, ethers.MaxUint256, v, r, s); + const timepoint = await time.clockFromReceipt[mode](tx); + + await expect(tx) + .to.emit(this.votes, 'DelegateChanged') + .withArgs(this.delegator.address, ethers.ZeroAddress, this.delegatee.address) + .to.emit(this.votes, 'DelegateVotesChanged') + .withArgs(this.delegatee.address, 0, weight); + + expect(await this.votes.delegates(this.delegator.address)).to.equal(this.delegatee.address); + expect(await this.votes.getVotes(this.delegator.address)).to.equal(0n); + expect(await this.votes.getVotes(this.delegatee)).to.equal(weight); + expect(await this.votes.getPastVotes(this.delegatee, timepoint - 1n)).to.equal(0n); + await mine(); + expect(await this.votes.getPastVotes(this.delegatee, timepoint)).to.equal(weight); }); it('rejects reused signature', async function () { - const { v, r, s } = await buildAndSignDelegation( - this.votes, - { - delegatee, - nonce, - expiry: MAX_UINT256, - }, - delegator.getPrivateKey(), - ); - - await this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s); - - await expectRevertCustomError( - this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s), - 'InvalidAccountNonce', - [delegator.address, nonce + 1], - ); + const { r, s, v } = await this.delegator + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.delegatee.address, + nonce, + expiry: ethers.MaxUint256, + }, + ) + .then(ethers.Signature.from); + + await this.votes.delegateBySig(this.delegatee, nonce, ethers.MaxUint256, v, r, s); + + await expect(this.votes.delegateBySig(this.delegatee, nonce, ethers.MaxUint256, v, r, s)) + .to.be.revertedWithCustomError(this.votes, 'InvalidAccountNonce') + .withArgs(this.delegator.address, nonce + 1n); }); it('rejects bad delegatee', async function () { - const { v, r, s } = await buildAndSignDelegation( - this.votes, - { - delegatee, - nonce, - expiry: MAX_UINT256, - }, - delegator.getPrivateKey(), + const { r, s, v } = await this.delegator + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.delegatee.address, + nonce, + expiry: ethers.MaxUint256, + }, + ) + .then(ethers.Signature.from); + + const tx = await this.votes.delegateBySig(this.other, nonce, ethers.MaxUint256, v, r, s); + const receipt = await tx.wait(); + + const [delegateChanged] = receipt.logs.filter( + log => this.votes.interface.parseLog(log)?.name === 'DelegateChanged', ); - - const receipt = await this.votes.delegateBySig(other, nonce, MAX_UINT256, v, r, s); - const { args } = receipt.logs.find(({ event }) => event === 'DelegateChanged'); - expect(args.delegator).to.not.be.equal(delegator.address); - expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); - expect(args.toDelegate).to.be.equal(other); + const { args } = this.votes.interface.parseLog(delegateChanged); + expect(args.delegator).to.not.be.equal(this.delegator.address); + expect(args.fromDelegate).to.equal(ethers.ZeroAddress); + expect(args.toDelegate).to.equal(this.other.address); }); it('rejects bad nonce', async function () { - const { v, r, s } = await buildAndSignDelegation( - this.votes, - { - delegatee, - nonce: nonce + 1, - expiry: MAX_UINT256, - }, - delegator.getPrivateKey(), - ); - - await expectRevertCustomError( - this.votes.delegateBySig(delegatee, nonce + 1, MAX_UINT256, v, r, s), - 'InvalidAccountNonce', - [delegator.address, 0], - ); + const { r, s, v } = await this.delegator + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.delegatee.address, + nonce: nonce + 1n, + expiry: ethers.MaxUint256, + }, + ) + .then(ethers.Signature.from); + + await expect(this.votes.delegateBySig(this.delegatee, nonce + 1n, ethers.MaxUint256, v, r, s)) + .to.be.revertedWithCustomError(this.votes, 'InvalidAccountNonce') + .withArgs(this.delegator.address, 0); }); it('rejects expired permit', async function () { - const expiry = (await time.latest()) - time.duration.weeks(1); - const { v, r, s } = await buildAndSignDelegation( - this.votes, - { - delegatee, - nonce, - expiry, - }, - delegator.getPrivateKey(), - ); - - await expectRevertCustomError( - this.votes.delegateBySig(delegatee, nonce, expiry, v, r, s), - 'VotesExpiredSignature', - [expiry], - ); + const expiry = (await time.clock.timestamp()) - 1n; + const { r, s, v } = await this.delegator + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.delegatee.address, + nonce, + expiry, + }, + ) + .then(ethers.Signature.from); + + await expect(this.votes.delegateBySig(this.delegatee, nonce, expiry, v, r, s)) + .to.be.revertedWithCustomError(this.votes, 'VotesExpiredSignature') + .withArgs(expiry); }); }); }); describe('getPastTotalSupply', function () { beforeEach(async function () { - await this.votes.delegate(accounts[1], { from: accounts[1] }); + await this.votes.connect(this.alice).delegate(this.alice); }); it('reverts if block number >= current block', async function () { const timepoint = 5e10; const clock = await this.votes.clock(); - await expectRevertCustomError(this.votes.getPastTotalSupply(timepoint), 'ERC5805FutureLookup', [ - timepoint, - clock, - ]); + await expect(this.votes.getPastTotalSupply(timepoint)) + .to.be.revertedWithCustomError(this.votes, 'ERC5805FutureLookup') + .withArgs(timepoint, clock); }); it('returns 0 if there are no checkpoints', async function () { - expect(await this.votes.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(0n)).to.equal(0n); }); it('returns the correct checkpointed total supply', async function () { const weight = tokens.map(token => getWeight(token)); // t0 = mint #0 - const t0 = await this.votes.$_mint(accounts[1], tokens[0]); - await time.advanceBlock(); + const t0 = await this.votes.$_mint(this.alice, tokens[0]); + await mine(); // t1 = mint #1 - const t1 = await this.votes.$_mint(accounts[1], tokens[1]); - await time.advanceBlock(); + const t1 = await this.votes.$_mint(this.alice, tokens[1]); + await mine(); // t2 = burn #1 - const t2 = await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[1]); - await time.advanceBlock(); + const t2 = await this.votes.$_burn(...(fungible ? [this.alice] : []), tokens[1]); + await mine(); // t3 = mint #2 - const t3 = await this.votes.$_mint(accounts[1], tokens[2]); - await time.advanceBlock(); + const t3 = await this.votes.$_mint(this.alice, tokens[2]); + await mine(); // t4 = burn #0 - const t4 = await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[0]); - await time.advanceBlock(); + const t4 = await this.votes.$_burn(...(fungible ? [this.alice] : []), tokens[0]); + await mine(); // t5 = burn #2 - const t5 = await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[2]); - await time.advanceBlock(); - - t0.timepoint = await clockFromReceipt[mode](t0.receipt); - t1.timepoint = await clockFromReceipt[mode](t1.receipt); - t2.timepoint = await clockFromReceipt[mode](t2.receipt); - t3.timepoint = await clockFromReceipt[mode](t3.receipt); - t4.timepoint = await clockFromReceipt[mode](t4.receipt); - t5.timepoint = await clockFromReceipt[mode](t5.receipt); - - expect(await this.votes.getPastTotalSupply(t0.timepoint - 1)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastTotalSupply(t0.timepoint)).to.be.bignumber.equal(weight[0]); - expect(await this.votes.getPastTotalSupply(t0.timepoint + 1)).to.be.bignumber.equal(weight[0]); - expect(await this.votes.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal(weight[0].add(weight[1])); - expect(await this.votes.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal(weight[0].add(weight[1])); - expect(await this.votes.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal(weight[0]); - expect(await this.votes.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal(weight[0]); - expect(await this.votes.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal(weight[0].add(weight[2])); - expect(await this.votes.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal(weight[0].add(weight[2])); - expect(await this.votes.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal(weight[2]); - expect(await this.votes.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal(weight[2]); - expect(await this.votes.getPastTotalSupply(t5.timepoint)).to.be.bignumber.equal('0'); - await expectRevertCustomError(this.votes.getPastTotalSupply(t5.timepoint + 1), 'ERC5805FutureLookup', [ - t5.timepoint + 1, // timepoint - t5.timepoint + 1, // clock - ]); + const t5 = await this.votes.$_burn(...(fungible ? [this.alice] : []), tokens[2]); + await mine(); + + t0.timepoint = await time.clockFromReceipt[mode](t0); + t1.timepoint = await time.clockFromReceipt[mode](t1); + t2.timepoint = await time.clockFromReceipt[mode](t2); + t3.timepoint = await time.clockFromReceipt[mode](t3); + t4.timepoint = await time.clockFromReceipt[mode](t4); + t5.timepoint = await time.clockFromReceipt[mode](t5); + + expect(await this.votes.getPastTotalSupply(t0.timepoint - 1n)).to.equal(0); + expect(await this.votes.getPastTotalSupply(t0.timepoint)).to.equal(weight[0]); + expect(await this.votes.getPastTotalSupply(t0.timepoint + 1n)).to.equal(weight[0]); + expect(await this.votes.getPastTotalSupply(t1.timepoint)).to.equal(weight[0] + weight[1]); + expect(await this.votes.getPastTotalSupply(t1.timepoint + 1n)).to.equal(weight[0] + weight[1]); + expect(await this.votes.getPastTotalSupply(t2.timepoint)).to.equal(weight[0]); + expect(await this.votes.getPastTotalSupply(t2.timepoint + 1n)).to.equal(weight[0]); + expect(await this.votes.getPastTotalSupply(t3.timepoint)).to.equal(weight[0] + weight[2]); + expect(await this.votes.getPastTotalSupply(t3.timepoint + 1n)).to.equal(weight[0] + weight[2]); + expect(await this.votes.getPastTotalSupply(t4.timepoint)).to.equal(weight[2]); + expect(await this.votes.getPastTotalSupply(t4.timepoint + 1n)).to.equal(weight[2]); + expect(await this.votes.getPastTotalSupply(t5.timepoint)).to.equal(0); + await expect(this.votes.getPastTotalSupply(t5.timepoint + 1n)) + .to.be.revertedWithCustomError(this.votes, 'ERC5805FutureLookup') + .withArgs(t5.timepoint + 1n, t5.timepoint + 1n); }); }); @@ -305,44 +279,41 @@ function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungibl // https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. describe('Compound test suite', function () { beforeEach(async function () { - await this.votes.$_mint(accounts[1], tokens[0]); - await this.votes.$_mint(accounts[1], tokens[1]); - await this.votes.$_mint(accounts[1], tokens[2]); + await this.votes.$_mint(this.alice, tokens[0]); + await this.votes.$_mint(this.alice, tokens[1]); + await this.votes.$_mint(this.alice, tokens[2]); }); describe('getPastVotes', function () { it('reverts if block number >= current block', async function () { const clock = await this.votes.clock(); const timepoint = 5e10; // far in the future - await expectRevertCustomError(this.votes.getPastVotes(accounts[2], timepoint), 'ERC5805FutureLookup', [ - timepoint, - clock, - ]); + await expect(this.votes.getPastVotes(this.bob, timepoint)) + .to.be.revertedWithCustomError(this.votes, 'ERC5805FutureLookup') + .withArgs(timepoint, clock); }); it('returns 0 if there are no checkpoints', async function () { - expect(await this.votes.getPastVotes(accounts[2], 0)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(this.bob, 0n)).to.equal(0n); }); it('returns the latest block if >= last checkpoint block', async function () { - const { receipt } = await this.votes.delegate(accounts[2], { from: accounts[1] }); - const timepoint = await clockFromReceipt[mode](receipt); - await time.advanceBlock(); - await time.advanceBlock(); - - const latest = await this.votes.getVotes(accounts[2]); - expect(await this.votes.getPastVotes(accounts[2], timepoint)).to.be.bignumber.equal(latest); - expect(await this.votes.getPastVotes(accounts[2], timepoint + 1)).to.be.bignumber.equal(latest); + const delegate = await this.votes.connect(this.alice).delegate(this.bob); + const timepoint = await time.clockFromReceipt[mode](delegate); + await mine(2); + + const latest = await this.votes.getVotes(this.bob); + expect(await this.votes.getPastVotes(this.bob, timepoint)).to.equal(latest); + expect(await this.votes.getPastVotes(this.bob, timepoint + 1n)).to.equal(latest); }); it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const { receipt } = await this.votes.delegate(accounts[2], { from: accounts[1] }); - const timepoint = await clockFromReceipt[mode](receipt); - await time.advanceBlock(); - await time.advanceBlock(); + await mine(); + const delegate = await this.votes.connect(this.alice).delegate(this.bob); + const timepoint = await time.clockFromReceipt[mode](delegate); + await mine(2); - expect(await this.votes.getPastVotes(accounts[2], timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(this.bob, timepoint - 1n)).to.equal(0n); }); }); }); diff --git a/test/governance/utils/Votes.test.js b/test/governance/utils/Votes.test.js index b2b80f9fe18..dda5e5c8251 100644 --- a/test/governance/utils/Votes.test.js +++ b/test/governance/utils/Votes.test.js @@ -1,90 +1,100 @@ -const { constants } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { clockFromReceipt } = require('../../helpers/time'); -const { BNsum } = require('../../helpers/math'); -const { expectRevertCustomError } = require('../../helpers/customError'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -require('array.prototype.at/auto'); +const { bigint: time } = require('../../helpers/time'); +const { sum } = require('../../helpers/math'); +const { zip } = require('../../helpers/iterate'); const { shouldBehaveLikeVotes } = require('./Votes.behavior'); const MODES = { - blocknumber: artifacts.require('$VotesMock'), - timestamp: artifacts.require('$VotesTimestampMock'), + blocknumber: '$VotesMock', + timestamp: '$VotesTimestampMock', }; -contract('Votes', function (accounts) { - const [account1, account2, account3] = accounts; - const amounts = { - [account1]: web3.utils.toBN('10000000000000000000000000'), - [account2]: web3.utils.toBN('10'), - [account3]: web3.utils.toBN('20'), - }; - - const name = 'My Vote'; - const version = '1'; +const AMOUNTS = [ethers.parseEther('10000000'), 10n, 20n]; +describe('Votes', function () { for (const [mode, artifact] of Object.entries(MODES)) { + const fixture = async () => { + const accounts = await ethers.getSigners(); + + const amounts = Object.fromEntries( + zip( + accounts.slice(0, AMOUNTS.length).map(({ address }) => address), + AMOUNTS, + ), + ); + + const name = 'My Vote'; + const version = '1'; + const votes = await ethers.deployContract(artifact, [name, version]); + + return { accounts, amounts, votes, name, version }; + }; + describe(`vote with ${mode}`, function () { beforeEach(async function () { - this.votes = await artifact.new(name, version); + Object.assign(this, await loadFixture(fixture)); }); - shouldBehaveLikeVotes(accounts, Object.values(amounts), { mode, fungible: true }); + shouldBehaveLikeVotes(AMOUNTS, { mode, fungible: true }); it('starts with zero votes', async function () { - expect(await this.votes.getTotalSupply()).to.be.bignumber.equal('0'); + expect(await this.votes.getTotalSupply()).to.equal(0n); }); describe('performs voting operations', function () { beforeEach(async function () { this.txs = []; - for (const [account, amount] of Object.entries(amounts)) { + for (const [account, amount] of Object.entries(this.amounts)) { this.txs.push(await this.votes.$_mint(account, amount)); } }); it('reverts if block number >= current block', async function () { - const lastTxTimepoint = await clockFromReceipt[mode](this.txs.at(-1).receipt); + const lastTxTimepoint = await time.clockFromReceipt[mode](this.txs.at(-1)); const clock = await this.votes.clock(); - await expectRevertCustomError(this.votes.getPastTotalSupply(lastTxTimepoint + 1), 'ERC5805FutureLookup', [ - lastTxTimepoint + 1, - clock, - ]); + await expect(this.votes.getPastTotalSupply(lastTxTimepoint)) + .to.be.revertedWithCustomError(this.votes, 'ERC5805FutureLookup') + .withArgs(lastTxTimepoint, clock); }); it('delegates', async function () { - expect(await this.votes.getVotes(account1)).to.be.bignumber.equal('0'); - expect(await this.votes.getVotes(account2)).to.be.bignumber.equal('0'); - expect(await this.votes.delegates(account1)).to.be.equal(constants.ZERO_ADDRESS); - expect(await this.votes.delegates(account2)).to.be.equal(constants.ZERO_ADDRESS); - - await this.votes.delegate(account1, account1); - - expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(amounts[account1]); - expect(await this.votes.getVotes(account2)).to.be.bignumber.equal('0'); - expect(await this.votes.delegates(account1)).to.be.equal(account1); - expect(await this.votes.delegates(account2)).to.be.equal(constants.ZERO_ADDRESS); - - await this.votes.delegate(account2, account1); - - expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(amounts[account1].add(amounts[account2])); - expect(await this.votes.getVotes(account2)).to.be.bignumber.equal('0'); - expect(await this.votes.delegates(account1)).to.be.equal(account1); - expect(await this.votes.delegates(account2)).to.be.equal(account1); + expect(await this.votes.getVotes(this.accounts[0])).to.equal(0n); + expect(await this.votes.getVotes(this.accounts[1])).to.equal(0n); + expect(await this.votes.delegates(this.accounts[0])).to.equal(ethers.ZeroAddress); + expect(await this.votes.delegates(this.accounts[1])).to.equal(ethers.ZeroAddress); + + await this.votes.delegate(this.accounts[0], ethers.Typed.address(this.accounts[0])); + + expect(await this.votes.getVotes(this.accounts[0])).to.equal(this.amounts[this.accounts[0].address]); + expect(await this.votes.getVotes(this.accounts[1])).to.equal(0n); + expect(await this.votes.delegates(this.accounts[0])).to.equal(this.accounts[0].address); + expect(await this.votes.delegates(this.accounts[1])).to.equal(ethers.ZeroAddress); + + await this.votes.delegate(this.accounts[1], ethers.Typed.address(this.accounts[0])); + + expect(await this.votes.getVotes(this.accounts[0])).to.equal( + this.amounts[this.accounts[0].address] + this.amounts[this.accounts[1].address], + ); + expect(await this.votes.getVotes(this.accounts[1])).to.equal(0n); + expect(await this.votes.delegates(this.accounts[0])).to.equal(this.accounts[0].address); + expect(await this.votes.delegates(this.accounts[1])).to.equal(this.accounts[0].address); }); it('cross delegates', async function () { - await this.votes.delegate(account1, account2); - await this.votes.delegate(account2, account1); + await this.votes.delegate(this.accounts[0], ethers.Typed.address(this.accounts[1].address)); + await this.votes.delegate(this.accounts[1], ethers.Typed.address(this.accounts[0].address)); - expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(amounts[account2]); - expect(await this.votes.getVotes(account2)).to.be.bignumber.equal(amounts[account1]); + expect(await this.votes.getVotes(this.accounts[0])).to.equal(this.amounts[this.accounts[1].address]); + expect(await this.votes.getVotes(this.accounts[1])).to.equal(this.amounts[this.accounts[0].address]); }); it('returns total amount of votes', async function () { - const totalSupply = BNsum(...Object.values(amounts)); - expect(await this.votes.getTotalSupply()).to.be.bignumber.equal(totalSupply); + const totalSupply = sum(...Object.values(this.amounts)); + expect(await this.votes.getTotalSupply()).to.equal(totalSupply); }); }); }); diff --git a/test/helpers/governance.js b/test/helpers/governance.js index fc4e30095a5..c2e79461a16 100644 --- a/test/helpers/governance.js +++ b/test/helpers/governance.js @@ -1,23 +1,10 @@ -const { web3 } = require('hardhat'); -const { forward } = require('../helpers/time'); +const { ethers } = require('hardhat'); +const { forward } = require('./time'); const { ProposalState } = require('./enums'); - -function zip(...args) { - return Array(Math.max(...args.map(array => array.length))) - .fill() - .map((_, i) => args.map(array => array[i])); -} - -function concatHex(...args) { - return web3.utils.bytesToHex([].concat(...args.map(h => web3.utils.hexToBytes(h || '0x')))); -} - -function concatOpts(args, opts = null) { - return opts ? args.concat(opts) : args; -} +const { unique } = require('./iterate'); const timelockSalt = (address, descriptionHash) => - '0x' + web3.utils.toBN(address).shln(96).xor(web3.utils.toBN(descriptionHash)).toString(16, 64); + ethers.toBeHex((ethers.toBigInt(address) << 96n) ^ ethers.toBigInt(descriptionHash), 32); class GovernorHelper { constructor(governor, mode = 'blocknumber') { @@ -25,229 +12,187 @@ class GovernorHelper { this.mode = mode; } - delegate(delegation = {}, opts = null) { + connect(account) { + this.governor = this.governor.connect(account); + return this; + } + + /// Setter and getters + /** + * Specify a proposal either as + * 1) an array of objects [{ target, value, data }] + * 2) an object of arrays { targets: [], values: [], data: [] } + */ + setProposal(actions, description) { + if (Array.isArray(actions)) { + this.targets = actions.map(a => a.target); + this.values = actions.map(a => a.value || 0n); + this.data = actions.map(a => a.data || '0x'); + } else { + ({ targets: this.targets, values: this.values, data: this.data } = actions); + } + this.description = description; + return this; + } + + get id() { + return ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(['address[]', 'uint256[]', 'bytes[]', 'bytes32'], this.shortProposal), + ); + } + + // used for checking events + get signatures() { + return this.data.map(() => ''); + } + + get descriptionHash() { + return ethers.id(this.description); + } + + // condensed version for queueing end executing + get shortProposal() { + return [this.targets, this.values, this.data, this.descriptionHash]; + } + + // full version for proposing + get fullProposal() { + return [this.targets, this.values, this.data, this.description]; + } + + get currentProposal() { + return this; + } + + /// Proposal lifecycle + delegate(delegation) { return Promise.all([ - delegation.token.delegate(delegation.to, { from: delegation.to }), - delegation.value && delegation.token.transfer(...concatOpts([delegation.to, delegation.value]), opts), - delegation.tokenId && + delegation.token.connect(delegation.to).delegate(delegation.to), + delegation.value === undefined || + delegation.token.connect(this.governor.runner).transfer(delegation.to, delegation.value), + delegation.tokenId === undefined || delegation.token .ownerOf(delegation.tokenId) .then(owner => - delegation.token.transferFrom(...concatOpts([owner, delegation.to, delegation.tokenId], opts)), + delegation.token.connect(this.governor.runner).transferFrom(owner, delegation.to, delegation.tokenId), ), ]); } - propose(opts = null) { - const proposal = this.currentProposal; - - return this.governor.methods[ - proposal.useCompatibilityInterface - ? 'propose(address[],uint256[],string[],bytes[],string)' - : 'propose(address[],uint256[],bytes[],string)' - ](...concatOpts(proposal.fullProposal, opts)); + propose() { + return this.governor.propose(...this.fullProposal); } - queue(opts = null) { - const proposal = this.currentProposal; - - return proposal.useCompatibilityInterface - ? this.governor.methods['queue(uint256)'](...concatOpts([proposal.id], opts)) - : this.governor.methods['queue(address[],uint256[],bytes[],bytes32)']( - ...concatOpts(proposal.shortProposal, opts), - ); + queue() { + return this.governor.queue(...this.shortProposal); } - execute(opts = null) { - const proposal = this.currentProposal; - - return proposal.useCompatibilityInterface - ? this.governor.methods['execute(uint256)'](...concatOpts([proposal.id], opts)) - : this.governor.methods['execute(address[],uint256[],bytes[],bytes32)']( - ...concatOpts(proposal.shortProposal, opts), - ); + execute() { + return this.governor.execute(...this.shortProposal); } - cancel(visibility = 'external', opts = null) { - const proposal = this.currentProposal; - + cancel(visibility = 'external') { switch (visibility) { case 'external': - if (proposal.useCompatibilityInterface) { - return this.governor.methods['cancel(uint256)'](...concatOpts([proposal.id], opts)); - } else { - return this.governor.methods['cancel(address[],uint256[],bytes[],bytes32)']( - ...concatOpts(proposal.shortProposal, opts), - ); - } + return this.governor.cancel(...this.shortProposal); + case 'internal': - return this.governor.methods['$_cancel(address[],uint256[],bytes[],bytes32)']( - ...concatOpts(proposal.shortProposal, opts), - ); + return this.governor.$_cancel(...this.shortProposal); + default: throw new Error(`unsupported visibility "${visibility}"`); } } - vote(vote = {}, opts = null) { - const proposal = this.currentProposal; - - return vote.signature - ? // if signature, and either params or reason → - vote.params || vote.reason - ? this.sign(vote).then(signature => - this.governor.castVoteWithReasonAndParamsBySig( - ...concatOpts( - [proposal.id, vote.support, vote.voter, vote.reason || '', vote.params || '', signature], - opts, - ), - ), - ) - : this.sign(vote).then(signature => - this.governor.castVoteBySig(...concatOpts([proposal.id, vote.support, vote.voter, signature], opts)), - ) - : vote.params - ? // otherwise if params - this.governor.castVoteWithReasonAndParams( - ...concatOpts([proposal.id, vote.support, vote.reason || '', vote.params], opts), - ) - : vote.reason - ? // otherwise if reason - this.governor.castVoteWithReason(...concatOpts([proposal.id, vote.support, vote.reason], opts)) - : this.governor.castVote(...concatOpts([proposal.id, vote.support], opts)); - } - - sign(vote = {}) { - return vote.signature(this.governor, this.forgeMessage(vote)); - } - - forgeMessage(vote = {}) { - const proposal = this.currentProposal; - - const message = { proposalId: proposal.id, support: vote.support, voter: vote.voter, nonce: vote.nonce }; - - if (vote.params || vote.reason) { - message.reason = vote.reason || ''; - message.params = vote.params || ''; + async vote(vote = {}) { + let method = 'castVote'; // default + let args = [this.id, vote.support]; // base + + if (vote.signature) { + const sign = await vote.signature(this.governor, this.forgeMessage(vote)); + if (vote.params || vote.reason) { + method = 'castVoteWithReasonAndParamsBySig'; + args.push(vote.voter, vote.reason ?? '', vote.params ?? '0x', sign); + } else { + method = 'castVoteBySig'; + args.push(vote.voter, sign); + } + } else if (vote.params) { + method = 'castVoteWithReasonAndParams'; + args.push(vote.reason ?? '', vote.params); + } else if (vote.reason) { + method = 'castVoteWithReason'; + args.push(vote.reason); } - return message; + return await this.governor[method](...args); } - async waitForSnapshot(offset = 0) { - const proposal = this.currentProposal; - const timepoint = await this.governor.proposalSnapshot(proposal.id); - return forward[this.mode](timepoint.addn(offset)); + /// Clock helpers + async waitForSnapshot(offset = 0n) { + const timepoint = await this.governor.proposalSnapshot(this.id); + return forward[this.mode](timepoint + offset); } - async waitForDeadline(offset = 0) { - const proposal = this.currentProposal; - const timepoint = await this.governor.proposalDeadline(proposal.id); - return forward[this.mode](timepoint.addn(offset)); + async waitForDeadline(offset = 0n) { + const timepoint = await this.governor.proposalDeadline(this.id); + return forward[this.mode](timepoint + offset); } - async waitForEta(offset = 0) { - const proposal = this.currentProposal; - const timestamp = await this.governor.proposalEta(proposal.id); - return forward.timestamp(timestamp.addn(offset)); + async waitForEta(offset = 0n) { + const timestamp = await this.governor.proposalEta(this.id); + return forward.timestamp(timestamp + offset); } - /** - * Specify a proposal either as - * 1) an array of objects [{ target, value, data, signature? }] - * 2) an object of arrays { targets: [], values: [], data: [], signatures?: [] } - */ - setProposal(actions, description) { - let targets, values, signatures, data, useCompatibilityInterface; + /// Other helpers + forgeMessage(vote = {}) { + const message = { proposalId: this.id, support: vote.support, voter: vote.voter, nonce: vote.nonce }; - if (Array.isArray(actions)) { - useCompatibilityInterface = actions.some(a => 'signature' in a); - targets = actions.map(a => a.target); - values = actions.map(a => a.value || '0'); - signatures = actions.map(a => a.signature || ''); - data = actions.map(a => a.data || '0x'); - } else { - useCompatibilityInterface = Array.isArray(actions.signatures); - ({ targets, values, signatures = [], data } = actions); + if (vote.params || vote.reason) { + message.reason = vote.reason ?? ''; + message.params = vote.params ?? '0x'; } - const fulldata = zip( - signatures.map(s => s && web3.eth.abi.encodeFunctionSignature(s)), - data, - ).map(hexs => concatHex(...hexs)); - - const descriptionHash = web3.utils.keccak256(description); - - // condensed version for queueing end executing - const shortProposal = [targets, values, fulldata, descriptionHash]; - - // full version for proposing - const fullProposal = [targets, values, ...(useCompatibilityInterface ? [signatures] : []), data, description]; - - // proposal id - const id = web3.utils.toBN( - web3.utils.keccak256( - web3.eth.abi.encodeParameters(['address[]', 'uint256[]', 'bytes[]', 'bytes32'], shortProposal), - ), - ); - - this.currentProposal = { - id, - targets, - values, - signatures, - data, - fulldata, - description, - descriptionHash, - shortProposal, - fullProposal, - useCompatibilityInterface, - }; - - return this.currentProposal; + return message; } -} -/** - * Encodes a list ProposalStates into a bytes32 representation where each bit enabled corresponds to - * the underlying position in the `ProposalState` enum. For example: - * - * 0x000...10000 - * ^^^^^^------ ... - * ^----- Succeeded - * ^---- Defeated - * ^--- Canceled - * ^-- Active - * ^- Pending - */ -function proposalStatesToBitMap(proposalStates, options = {}) { - if (!Array.isArray(proposalStates)) { - proposalStates = [proposalStates]; - } - const statesCount = Object.keys(ProposalState).length; - let result = 0; - - const uniqueProposalStates = new Set(proposalStates.map(bn => bn.toNumber())); // Remove duplicates - for (const state of uniqueProposalStates) { - if (state < 0 || state >= statesCount) { - expect.fail(`ProposalState ${state} out of possible states (0...${statesCount}-1)`); - } else { - result |= 1 << state; + /** + * Encodes a list ProposalStates into a bytes32 representation where each bit enabled corresponds to + * the underlying position in the `ProposalState` enum. For example: + * + * 0x000...10000 + * ^^^^^^------ ... + * ^----- Succeeded + * ^---- Defeated + * ^--- Canceled + * ^-- Active + * ^- Pending + */ + static proposalStatesToBitMap(proposalStates, options = {}) { + if (!Array.isArray(proposalStates)) { + proposalStates = [proposalStates]; + } + const statesCount = BigInt(Object.keys(ProposalState).length); + let result = 0n; + + for (const state of unique(...proposalStates)) { + if (state < 0n || state >= statesCount) { + expect.fail(`ProposalState ${state} out of possible states (0...${statesCount}-1)`); + } else { + result |= 1n << state; + } } - } - if (options.inverted) { - const mask = 2 ** statesCount - 1; - result = result ^ mask; - } + if (options.inverted) { + const mask = 2n ** statesCount - 1n; + result = result ^ mask; + } - const hex = web3.utils.numberToHex(result); - return web3.utils.padLeft(hex, 64); + return ethers.toBeHex(result, 32); + } } module.exports = { GovernorHelper, - proposalStatesToBitMap, timelockSalt, }; diff --git a/test/helpers/iterate.js b/test/helpers/iterate.js index 2a84dfbebdc..79d1c8c839e 100644 --- a/test/helpers/iterate.js +++ b/test/helpers/iterate.js @@ -3,8 +3,15 @@ const mapValues = (obj, fn) => Object.fromEntries(Object.entries(obj).map(([k, v // Cartesian product of a list of arrays const product = (...arrays) => arrays.reduce((a, b) => a.flatMap(ai => b.map(bi => [...ai, bi])), [[]]); +const unique = (...array) => array.filter((obj, i) => array.indexOf(obj) === i); +const zip = (...args) => + Array(Math.max(...args.map(array => array.length))) + .fill() + .map((_, i) => args.map(array => array[i])); module.exports = { mapValues, product, + unique, + zip, }; diff --git a/test/helpers/time.js b/test/helpers/time.js index 874713ee535..5f85b69158a 100644 --- a/test/helpers/time.js +++ b/test/helpers/time.js @@ -1,3 +1,4 @@ +const { ethers } = require('hardhat'); const { time, mineUpTo } = require('@nomicfoundation/hardhat-network-helpers'); const { mapValues } = require('./iterate'); @@ -8,9 +9,7 @@ module.exports = { }, clockFromReceipt: { blocknumber: receipt => Promise.resolve(receipt.blockNumber), - timestamp: receipt => web3.eth.getBlock(receipt.blockNumber).then(block => block.timestamp), - // TODO: update for ethers receipt - // timestamp: receipt => receipt.getBlock().then(block => block.timestamp), + timestamp: receipt => ethers.provider.getBlock(receipt.blockNumber).then(block => block.timestamp), }, forward: { blocknumber: mineUpTo, @@ -21,8 +20,8 @@ module.exports = { // TODO: deprecate the old version in favor of this one module.exports.bigint = { - clock: mapValues(module.exports.clock, fn => () => fn().then(BigInt)), - clockFromReceipt: mapValues(module.exports.clockFromReceipt, fn => receipt => fn(receipt).then(BigInt)), + clock: mapValues(module.exports.clock, fn => () => fn().then(ethers.toBigInt)), + clockFromReceipt: mapValues(module.exports.clockFromReceipt, fn => receipt => fn(receipt).then(ethers.toBigInt)), forward: module.exports.forward, - duration: mapValues(module.exports.duration, fn => n => BigInt(fn(n))), + duration: mapValues(module.exports.duration, fn => n => ethers.toBigInt(fn(ethers.toNumber(n)))), }; diff --git a/test/helpers/txpool.js b/test/helpers/txpool.js index ecdba546214..b6e960c1014 100644 --- a/test/helpers/txpool.js +++ b/test/helpers/txpool.js @@ -1,30 +1,20 @@ const { network } = require('hardhat'); -const { promisify } = require('util'); - -const queue = promisify(setImmediate); - -async function countPendingTransactions() { - return parseInt(await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending'])); -} +const { mine } = require('@nomicfoundation/hardhat-network-helpers'); +const { unique } = require('./iterate'); async function batchInBlock(txs) { try { // disable auto-mining await network.provider.send('evm_setAutomine', [false]); // send all transactions - const promises = txs.map(fn => fn()); - // wait for node to have all pending transactions - while (txs.length > (await countPendingTransactions())) { - await queue(); - } + const responses = await Promise.all(txs.map(fn => fn())); // mine one block - await network.provider.send('evm_mine'); + await mine(); // fetch receipts - const receipts = await Promise.all(promises); + const receipts = await Promise.all(responses.map(response => response.wait())); // Sanity check, all tx should be in the same block - const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber)); - expect(minedBlocks.size).to.equal(1); - + expect(unique(receipts.map(receipt => receipt.blockNumber))).to.have.lengthOf(1); + // return responses return receipts; } finally { // enable auto-mining @@ -33,6 +23,5 @@ async function batchInBlock(txs) { } module.exports = { - countPendingTransactions, batchInBlock, }; diff --git a/test/token/ERC20/extensions/ERC20Permit.test.js b/test/token/ERC20/extensions/ERC20Permit.test.js index e27a98239bb..538fa7d7f03 100644 --- a/test/token/ERC20/extensions/ERC20Permit.test.js +++ b/test/token/ERC20/extensions/ERC20Permit.test.js @@ -3,9 +3,7 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { getDomain, domainSeparator, Permit } = require('../../../helpers/eip712'); -const { - bigint: { clock, duration }, -} = require('../../../helpers/time'); +const { bigint: time } = require('../../../helpers/time'); const name = 'My Token'; const symbol = 'MTKN'; @@ -97,7 +95,7 @@ describe('ERC20Permit', function () { }); it('rejects expired permit', async function () { - const deadline = (await clock.timestamp()) - duration.weeks(1); + const deadline = (await time.clock.timestamp()) - time.duration.weeks(1); const { v, r, s } = await this.buildData(this.token, deadline) .then(({ domain, types, message }) => this.owner.signTypedData(domain, types, message)) diff --git a/test/token/ERC20/extensions/ERC20Votes.test.js b/test/token/ERC20/extensions/ERC20Votes.test.js index 9ec1c09e935..165d08a1878 100644 --- a/test/token/ERC20/extensions/ERC20Votes.test.js +++ b/test/token/ERC20/extensions/ERC20Votes.test.js @@ -1,585 +1,544 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, time } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS } = constants; +const { loadFixture, mine } = require('@nomicfoundation/hardhat-network-helpers'); + +const { getDomain, Delegation } = require('../../../helpers/eip712'); +const { batchInBlock } = require('../../../helpers/txpool'); +const { bigint: time } = require('../../../helpers/time'); const { shouldBehaveLikeVotes } = require('../../../governance/utils/Votes.behavior'); -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; -const { batchInBlock } = require('../../../helpers/txpool'); -const { getDomain, domainType, Delegation } = require('../../../helpers/eip712'); -const { clock, clockFromReceipt } = require('../../../helpers/time'); -const { expectRevertCustomError } = require('../../../helpers/customError'); +const TOKENS = [ + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, +]; + +const name = 'My Token'; +const symbol = 'MTKN'; +const version = '1'; +const supply = ethers.parseEther('10000000'); -const MODES = { - blocknumber: artifacts.require('$ERC20Votes'), - timestamp: artifacts.require('$ERC20VotesTimestampMock'), -}; +describe('ERC20Votes', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + // accounts is required by shouldBehaveLikeVotes + const accounts = await ethers.getSigners(); + const [holder, recipient, delegatee, other1, other2] = accounts; -contract('ERC20Votes', function (accounts) { - const [holder, recipient, holderDelegatee, other1, other2] = accounts; + const token = await ethers.deployContract(Token, [name, symbol, name, version]); + const domain = await getDomain(token); - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - const supply = new BN('10000000000000000000000000'); + return { accounts, holder, recipient, delegatee, other1, other2, token, domain }; + }; - for (const [mode, artifact] of Object.entries(MODES)) { describe(`vote with ${mode}`, function () { beforeEach(async function () { - this.token = await artifact.new(name, symbol, name, version); + Object.assign(this, await loadFixture(fixture)); this.votes = this.token; }); // includes ERC6372 behavior check - shouldBehaveLikeVotes(accounts, [1, 17, 42], { mode, fungible: true }); + shouldBehaveLikeVotes([1, 17, 42], { mode, fungible: true }); it('initial nonce is 0', async function () { - expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); + expect(await this.token.nonces(this.holder)).to.equal(0n); }); it('minting restriction', async function () { - const value = web3.utils.toBN(1).shln(208); - await expectRevertCustomError(this.token.$_mint(holder, value), 'ERC20ExceededSafeSupply', [ - value, - value.subn(1), - ]); + const value = 2n ** 208n; + await expect(this.token.$_mint(this.holder, value)) + .to.be.revertedWithCustomError(this.token, 'ERC20ExceededSafeSupply') + .withArgs(value, value - 1n); }); it('recent checkpoints', async function () { - await this.token.delegate(holder, { from: holder }); + await this.token.connect(this.holder).delegate(this.holder); for (let i = 0; i < 6; i++) { - await this.token.$_mint(holder, 1); + await this.token.$_mint(this.holder, 1n); } - const timepoint = await clock[mode](); - expect(await this.token.numCheckpoints(holder)).to.be.bignumber.equal('6'); + const timepoint = await time.clock[mode](); + expect(await this.token.numCheckpoints(this.holder)).to.equal(6n); // recent - expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal('5'); + expect(await this.token.getPastVotes(this.holder, timepoint - 1n)).to.equal(5n); // non-recent - expect(await this.token.getPastVotes(holder, timepoint - 6)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(this.holder, timepoint - 6n)).to.equal(0n); }); describe('set delegation', function () { describe('call', function () { it('delegation with balance', async function () { - await this.token.$_mint(holder, supply); - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - const timepoint = await clockFromReceipt[mode](receipt); - - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousVotes: '0', - newVotes: supply, - }); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal(supply); + await this.token.$_mint(this.holder, supply); + expect(await this.token.delegates(this.holder)).to.equal(ethers.ZeroAddress); + + const tx = await this.token.connect(this.holder).delegate(this.holder); + const timepoint = await time.clockFromReceipt[mode](tx); + + await expect(tx) + .to.emit(this.token, 'DelegateChanged') + .withArgs(this.holder.address, ethers.ZeroAddress, this.holder.address) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.holder.address, 0n, supply); + + expect(await this.token.delegates(this.holder)).to.equal(this.holder.address); + expect(await this.token.getVotes(this.holder)).to.equal(supply); + expect(await this.token.getPastVotes(this.holder, timepoint - 1n)).to.equal(0n); + await mine(); + expect(await this.token.getPastVotes(this.holder, timepoint)).to.equal(supply); }); it('delegation without balance', async function () { - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + expect(await this.token.delegates(this.holder)).to.equal(ethers.ZeroAddress); - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + await expect(this.token.connect(this.holder).delegate(this.holder)) + .to.emit(this.token, 'DelegateChanged') + .withArgs(this.holder.address, ethers.ZeroAddress, this.holder.address) + .to.not.emit(this.token, 'DelegateVotesChanged'); - expect(await this.token.delegates(holder)).to.be.equal(holder); + expect(await this.token.delegates(this.holder)).to.equal(this.holder.address); }); }); describe('with signature', function () { - const delegator = Wallet.generate(); - const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); - const nonce = 0; - - const buildData = (contract, message) => - getDomain(contract).then(domain => ({ - primaryType: 'Delegation', - types: { EIP712Domain: domainType(domain), Delegation }, - domain, - message, - })); + const nonce = 0n; beforeEach(async function () { - await this.token.$_mint(delegatorAddress, supply); + await this.token.$_mint(this.holder, supply); }); it('accept signed delegation', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - - expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - const timepoint = await clockFromReceipt[mode](receipt); - - expectEvent(receipt, 'DelegateChanged', { - delegator: delegatorAddress, - fromDelegate: ZERO_ADDRESS, - toDelegate: delegatorAddress, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: delegatorAddress, - previousVotes: '0', - newVotes: supply, - }); - - expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); - - expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(delegatorAddress, timepoint - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(delegatorAddress, timepoint)).to.be.bignumber.equal(supply); + const { r, s, v } = await this.holder + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.holder.address, + nonce, + expiry: ethers.MaxUint256, + }, + ) + .then(ethers.Signature.from); + + expect(await this.token.delegates(this.holder)).to.equal(ethers.ZeroAddress); + + const tx = await this.token.delegateBySig(this.holder, nonce, ethers.MaxUint256, v, r, s); + const timepoint = await time.clockFromReceipt[mode](tx); + + await expect(tx) + .to.emit(this.token, 'DelegateChanged') + .withArgs(this.holder.address, ethers.ZeroAddress, this.holder.address) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.holder.address, 0n, supply); + + expect(await this.token.delegates(this.holder)).to.equal(this.holder.address); + + expect(await this.token.getVotes(this.holder)).to.equal(supply); + expect(await this.token.getPastVotes(this.holder, timepoint - 1n)).to.equal(0n); + await mine(); + expect(await this.token.getPastVotes(this.holder, timepoint)).to.equal(supply); }); it('rejects reused signature', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - - await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - - await expectRevertCustomError( - this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), - 'InvalidAccountNonce', - [delegatorAddress, nonce + 1], - ); + const { r, s, v } = await this.holder + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.holder.address, + nonce, + expiry: ethers.MaxUint256, + }, + ) + .then(ethers.Signature.from); + + await this.token.delegateBySig(this.holder, nonce, ethers.MaxUint256, v, r, s); + + await expect(this.token.delegateBySig(this.holder, nonce, ethers.MaxUint256, v, r, s)) + .to.be.revertedWithCustomError(this.token, 'InvalidAccountNonce') + .withArgs(this.holder.address, nonce + 1n); }); it('rejects bad delegatee', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - - const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); - const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged'); - expect(args.delegator).to.not.be.equal(delegatorAddress); - expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); - expect(args.toDelegate).to.be.equal(holderDelegatee); + const { r, s, v } = await this.holder + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.holder.address, + nonce, + expiry: ethers.MaxUint256, + }, + ) + .then(ethers.Signature.from); + + const tx = await this.token.delegateBySig(this.delegatee, nonce, ethers.MaxUint256, v, r, s); + + const { args } = await tx + .wait() + .then(receipt => receipt.logs.find(event => event.fragment.name == 'DelegateChanged')); + expect(args[0]).to.not.equal(this.holder.address); + expect(args[1]).to.equal(ethers.ZeroAddress); + expect(args[2]).to.equal(this.delegatee.address); }); it('rejects bad nonce', async function () { - const sig = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }).then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })); - const { r, s, v } = fromRpcSig(sig); - - const domain = await getDomain(this.token); - const typedMessage = { - primaryType: 'Delegation', - types: { EIP712Domain: domainType(domain), Delegation }, - domain, - message: { delegatee: delegatorAddress, nonce: nonce + 1, expiry: MAX_UINT256 }, - }; - - await expectRevertCustomError( - this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), - 'InvalidAccountNonce', - [ethSigUtil.recoverTypedSignature({ data: typedMessage, sig }), nonce], + const { r, s, v, serialized } = await this.holder + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.holder.address, + nonce, + expiry: ethers.MaxUint256, + }, + ) + .then(ethers.Signature.from); + + const recovered = ethers.verifyTypedData( + this.domain, + { Delegation }, + { + delegatee: this.holder.address, + nonce: nonce + 1n, + expiry: ethers.MaxUint256, + }, + serialized, ); + + await expect(this.token.delegateBySig(this.holder, nonce + 1n, ethers.MaxUint256, v, r, s)) + .to.be.revertedWithCustomError(this.token, 'InvalidAccountNonce') + .withArgs(recovered, nonce); }); it('rejects expired permit', async function () { - const expiry = (await time.latest()) - time.duration.weeks(1); - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry, - }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - - await expectRevertCustomError( - this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), - 'VotesExpiredSignature', - [expiry], - ); + const expiry = (await time.clock.timestamp()) - time.duration.weeks(1); + + const { r, s, v } = await this.holder + .signTypedData( + this.domain, + { Delegation }, + { + delegatee: this.holder.address, + nonce, + expiry, + }, + ) + .then(ethers.Signature.from); + + await expect(this.token.delegateBySig(this.holder, nonce, expiry, v, r, s)) + .to.be.revertedWithCustomError(this.token, 'VotesExpiredSignature') + .withArgs(expiry); }); }); }); describe('change delegation', function () { beforeEach(async function () { - await this.token.$_mint(holder, supply); - await this.token.delegate(holder, { from: holder }); + await this.token.$_mint(this.holder, supply); + await this.token.connect(this.holder).delegate(this.holder); }); it('call', async function () { - expect(await this.token.delegates(holder)).to.be.equal(holder); - - const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); - const timepoint = await clockFromReceipt[mode](receipt); - - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: holder, - toDelegate: holderDelegatee, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousVotes: supply, - newVotes: '0', - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holderDelegatee, - previousVotes: '0', - newVotes: supply, - }); - - expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); - - expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); - expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holderDelegatee, timepoint - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(holderDelegatee, timepoint)).to.be.bignumber.equal(supply); + expect(await this.token.delegates(this.holder)).to.equal(this.holder.address); + + const tx = await this.token.connect(this.holder).delegate(this.delegatee); + const timepoint = await time.clockFromReceipt[mode](tx); + + await expect(tx) + .to.emit(this.token, 'DelegateChanged') + .withArgs(this.holder.address, this.holder.address, this.delegatee.address) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.holder.address, supply, 0n) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.delegatee.address, 0n, supply); + + expect(await this.token.delegates(this.holder)).to.equal(this.delegatee.address); + + expect(await this.token.getVotes(this.holder)).to.equal(0n); + expect(await this.token.getVotes(this.delegatee)).to.equal(supply); + expect(await this.token.getPastVotes(this.holder, timepoint - 1n)).to.equal(supply); + expect(await this.token.getPastVotes(this.delegatee, timepoint - 1n)).to.equal(0n); + await mine(); + expect(await this.token.getPastVotes(this.holder, timepoint)).to.equal(0n); + expect(await this.token.getPastVotes(this.delegatee, timepoint)).to.equal(supply); }); }); describe('transfers', function () { beforeEach(async function () { - await this.token.$_mint(holder, supply); + await this.token.$_mint(this.holder, supply); }); it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + await expect(this.token.connect(this.holder).transfer(this.recipient, 1n)) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.recipient.address, 1n) + .to.not.emit(this.token, 'DelegateVotesChanged'); - this.holderVotes = '0'; - this.recipientVotes = '0'; + this.holderVotes = 0n; + this.recipientVotes = 0n; }); it('sender delegation', async function () { - await this.token.delegate(holder, { from: holder }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousVotes: supply, - newVotes: supply.subn(1), - }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.holderVotes = supply.subn(1); - this.recipientVotes = '0'; + await this.token.connect(this.holder).delegate(this.holder); + + const tx = await this.token.connect(this.holder).transfer(this.recipient, 1n); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.recipient.address, 1n) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.holder.address, supply, supply - 1n); + + const { logs } = await tx.wait(); + const { index } = logs.find(event => event.fragment.name == 'DelegateVotesChanged'); + for (const event of logs.filter(event => event.fragment.name == 'Transfer')) { + expect(event.index).to.lt(index); + } + + this.holderVotes = supply - 1n; + this.recipientVotes = 0n; }); it('receiver delegation', async function () { - await this.token.delegate(recipient, { from: recipient }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousVotes: '0', newVotes: '1' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.holderVotes = '0'; - this.recipientVotes = '1'; + await this.token.connect(this.recipient).delegate(this.recipient); + + const tx = await this.token.connect(this.holder).transfer(this.recipient, 1n); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.recipient.address, 1n) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.recipient.address, 0n, 1n); + + const { logs } = await tx.wait(); + const { index } = logs.find(event => event.fragment.name == 'DelegateVotesChanged'); + for (const event of logs.filter(event => event.fragment.name == 'Transfer')) { + expect(event.index).to.lt(index); + } + + this.holderVotes = 0n; + this.recipientVotes = 1n; }); it('full delegation', async function () { - await this.token.delegate(holder, { from: holder }); - await this.token.delegate(recipient, { from: recipient }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousVotes: supply, - newVotes: supply.subn(1), - }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousVotes: '0', newVotes: '1' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.holderVotes = supply.subn(1); - this.recipientVotes = '1'; + await this.token.connect(this.holder).delegate(this.holder); + await this.token.connect(this.recipient).delegate(this.recipient); + + const tx = await this.token.connect(this.holder).transfer(this.recipient, 1n); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.recipient.address, 1n) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.holder.address, supply, supply - 1n) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.recipient.address, 0n, 1n); + + const { logs } = await tx.wait(); + const { index } = logs.find(event => event.fragment.name == 'DelegateVotesChanged'); + for (const event of logs.filter(event => event.fragment.name == 'Transfer')) { + expect(event.index).to.lt(index); + } + + this.holderVotes = supply - 1n; + this.recipientVotes = 1n; }); afterEach(async function () { - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); + expect(await this.token.getVotes(this.holder)).to.equal(this.holderVotes); + expect(await this.token.getVotes(this.recipient)).to.equal(this.recipientVotes); // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" - const timepoint = await clock[mode](); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getPastVotes(recipient, timepoint)).to.be.bignumber.equal(this.recipientVotes); + const timepoint = await time.clock[mode](); + await mine(); + expect(await this.token.getPastVotes(this.holder, timepoint)).to.equal(this.holderVotes); + expect(await this.token.getPastVotes(this.recipient, timepoint)).to.equal(this.recipientVotes); }); }); // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. describe('Compound test suite', function () { beforeEach(async function () { - await this.token.$_mint(holder, supply); + await this.token.$_mint(this.holder, supply); }); describe('balanceOf', function () { it('grants to initial account', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.balanceOf(this.holder)).to.equal(supply); }); }); describe('numCheckpoints', function () { it('returns the number of checkpoints for a delegate', async function () { - await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const t1 = await this.token.delegate(other1, { from: recipient }); - t1.timepoint = await clockFromReceipt[mode](t1.receipt); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - - const t2 = await this.token.transfer(other2, 10, { from: recipient }); - t2.timepoint = await clockFromReceipt[mode](t2.receipt); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transfer(other2, 10, { from: recipient }); - t3.timepoint = await clockFromReceipt[mode](t3.receipt); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - t4.timepoint = await clockFromReceipt[mode](t4.receipt); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '100']); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.timepoint.toString(), '90']); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.timepoint.toString(), '80']); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.timepoint.toString(), '100']); - - await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal('100'); - expect(await this.token.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal('90'); - expect(await this.token.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal('80'); - expect(await this.token.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal('100'); + await this.token.connect(this.holder).transfer(this.recipient, 100n); //give an account a few tokens for readability + expect(await this.token.numCheckpoints(this.other1)).to.equal(0n); + + const t1 = await this.token.connect(this.recipient).delegate(this.other1); + t1.timepoint = await time.clockFromReceipt[mode](t1); + expect(await this.token.numCheckpoints(this.other1)).to.equal(1n); + + const t2 = await this.token.connect(this.recipient).transfer(this.other2, 10); + t2.timepoint = await time.clockFromReceipt[mode](t2); + expect(await this.token.numCheckpoints(this.other1)).to.equal(2n); + + const t3 = await this.token.connect(this.recipient).transfer(this.other2, 10); + t3.timepoint = await time.clockFromReceipt[mode](t3); + expect(await this.token.numCheckpoints(this.other1)).to.equal(3n); + + const t4 = await this.token.connect(this.holder).transfer(this.recipient, 20); + t4.timepoint = await time.clockFromReceipt[mode](t4); + expect(await this.token.numCheckpoints(this.other1)).to.equal(4n); + + expect(await this.token.checkpoints(this.other1, 0n)).to.deep.equal([t1.timepoint, 100n]); + expect(await this.token.checkpoints(this.other1, 1n)).to.deep.equal([t2.timepoint, 90n]); + expect(await this.token.checkpoints(this.other1, 2n)).to.deep.equal([t3.timepoint, 80n]); + expect(await this.token.checkpoints(this.other1, 3n)).to.deep.equal([t4.timepoint, 100n]); + await mine(); + expect(await this.token.getPastVotes(this.other1, t1.timepoint)).to.equal(100n); + expect(await this.token.getPastVotes(this.other1, t2.timepoint)).to.equal(90n); + expect(await this.token.getPastVotes(this.other1, t3.timepoint)).to.equal(80n); + expect(await this.token.getPastVotes(this.other1, t4.timepoint)).to.equal(100n); }); it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, '100', { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + await this.token.connect(this.holder).transfer(this.recipient, 100n); + expect(await this.token.numCheckpoints(this.other1)).to.equal(0n); const [t1, t2, t3] = await batchInBlock([ - () => this.token.delegate(other1, { from: recipient, gas: 200000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 200000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 200000 }), + () => this.token.connect(this.recipient).delegate(this.other1, { gasLimit: 200000 }), + () => this.token.connect(this.recipient).transfer(this.other2, 10n, { gasLimit: 200000 }), + () => this.token.connect(this.recipient).transfer(this.other2, 10n, { gasLimit: 200000 }), ]); - t1.timepoint = await clockFromReceipt[mode](t1.receipt); - t2.timepoint = await clockFromReceipt[mode](t2.receipt); - t3.timepoint = await clockFromReceipt[mode](t3.receipt); + t1.timepoint = await time.clockFromReceipt[mode](t1); + t2.timepoint = await time.clockFromReceipt[mode](t2); + t3.timepoint = await time.clockFromReceipt[mode](t3); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '80']); + expect(await this.token.numCheckpoints(this.other1)).to.equal(1); + expect(await this.token.checkpoints(this.other1, 0n)).to.be.deep.equal([t1.timepoint, 80n]); - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - t4.timepoint = await clockFromReceipt[mode](t4.receipt); + const t4 = await this.token.connect(this.holder).transfer(this.recipient, 20n); + t4.timepoint = await time.clockFromReceipt[mode](t4); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.timepoint.toString(), '100']); + expect(await this.token.numCheckpoints(this.other1)).to.equal(2n); + expect(await this.token.checkpoints(this.other1, 1n)).to.be.deep.equal([t4.timepoint, 100n]); }); }); describe('getPastVotes', function () { it('reverts if block number >= current block', async function () { const clock = await this.token.clock(); - await expectRevertCustomError(this.token.getPastVotes(other1, 5e10), 'ERC5805FutureLookup', [5e10, clock]); + await expect(this.token.getPastVotes(this.other1, 50_000_000_000n)) + .to.be.revertedWithCustomError(this.token, 'ERC5805FutureLookup') + .withArgs(50_000_000_000n, clock); }); it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(this.other1, 0n)).to.equal(0n); }); it('returns the latest block if >= last checkpoint block', async function () { - const { receipt } = await this.token.delegate(other1, { from: holder }); - const timepoint = await clockFromReceipt[mode](receipt); - await time.advanceBlock(); - await time.advanceBlock(); + const tx = await this.token.connect(this.holder).delegate(this.other1); + const timepoint = await time.clockFromReceipt[mode](tx); + await mine(2); - expect(await this.token.getPastVotes(other1, timepoint)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastVotes(other1, timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); + expect(await this.token.getPastVotes(this.other1, timepoint)).to.equal(supply); + expect(await this.token.getPastVotes(this.other1, timepoint + 1n)).to.equal(supply); }); it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const { receipt } = await this.token.delegate(other1, { from: holder }); - const timepoint = await clockFromReceipt[mode](receipt); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, timepoint - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); + await mine(); + const tx = await this.token.connect(this.holder).delegate(this.other1); + const timepoint = await time.clockFromReceipt[mode](tx); + await mine(2); + + expect(await this.token.getPastVotes(this.other1, timepoint - 1n)).to.equal(0n); + expect(await this.token.getPastVotes(this.other1, timepoint + 1n)).to.equal(supply); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 20, { from: other2 }); - await time.advanceBlock(); - await time.advanceBlock(); - - t1.timepoint = await clockFromReceipt[mode](t1.receipt); - t2.timepoint = await clockFromReceipt[mode](t2.receipt); - t3.timepoint = await clockFromReceipt[mode](t3.receipt); - t4.timepoint = await clockFromReceipt[mode](t4.receipt); - - expect(await this.token.getPastVotes(other1, t1.timepoint - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastVotes(other1, t1.timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastVotes(other1, t2.timepoint + 1)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastVotes(other1, t3.timepoint + 1)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastVotes(other1, t4.timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); + const t1 = await this.token.connect(this.holder).delegate(this.other1); + await mine(2); + const t2 = await this.token.connect(this.holder).transfer(this.other2, 10); + await mine(2); + const t3 = await this.token.connect(this.holder).transfer(this.other2, 10); + await mine(2); + const t4 = await this.token.connect(this.other2).transfer(this.holder, 20); + await mine(2); + + t1.timepoint = await time.clockFromReceipt[mode](t1); + t2.timepoint = await time.clockFromReceipt[mode](t2); + t3.timepoint = await time.clockFromReceipt[mode](t3); + t4.timepoint = await time.clockFromReceipt[mode](t4); + + expect(await this.token.getPastVotes(this.other1, t1.timepoint - 1n)).to.equal(0n); + expect(await this.token.getPastVotes(this.other1, t1.timepoint)).to.equal(supply); + expect(await this.token.getPastVotes(this.other1, t1.timepoint + 1n)).to.equal(supply); + expect(await this.token.getPastVotes(this.other1, t2.timepoint)).to.equal(supply - 10n); + expect(await this.token.getPastVotes(this.other1, t2.timepoint + 1n)).to.equal(supply - 10n); + expect(await this.token.getPastVotes(this.other1, t3.timepoint)).to.equal(supply - 20n); + expect(await this.token.getPastVotes(this.other1, t3.timepoint + 1n)).to.equal(supply - 20n); + expect(await this.token.getPastVotes(this.other1, t4.timepoint)).to.equal(supply); + expect(await this.token.getPastVotes(this.other1, t4.timepoint + 1n)).to.equal(supply); }); }); }); describe('getPastTotalSupply', function () { beforeEach(async function () { - await this.token.delegate(holder, { from: holder }); + await this.token.connect(this.holder).delegate(this.holder); }); it('reverts if block number >= current block', async function () { const clock = await this.token.clock(); - await expectRevertCustomError(this.token.getPastTotalSupply(5e10), 'ERC5805FutureLookup', [5e10, clock]); + await expect(this.token.getPastTotalSupply(50_000_000_000n)) + .to.be.revertedWithCustomError(this.token, 'ERC5805FutureLookup') + .withArgs(50_000_000_000n, clock); }); it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(0n)).to.equal(0n); }); it('returns the latest block if >= last checkpoint block', async function () { - const { receipt } = await this.token.$_mint(holder, supply); - const timepoint = await clockFromReceipt[mode](receipt); - await time.advanceBlock(); - await time.advanceBlock(); + const tx = await this.token.$_mint(this.holder, supply); + const timepoint = await time.clockFromReceipt[mode](tx); + await mine(2); - expect(await this.token.getPastTotalSupply(timepoint)).to.be.bignumber.equal(supply); - expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(timepoint)).to.equal(supply); + expect(await this.token.getPastTotalSupply(timepoint + 1n)).to.equal(supply); }); it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const { receipt } = await this.token.$_mint(holder, supply); - const timepoint = await clockFromReceipt[mode](receipt); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); + await mine(); + const tx = await this.token.$_mint(this.holder, supply); + const timepoint = await time.clockFromReceipt[mode](tx); + await mine(2); + + expect(await this.token.getPastTotalSupply(timepoint - 1n)).to.equal(0n); + expect(await this.token.getPastTotalSupply(timepoint + 1n)).to.equal(supply); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.$_mint(holder, supply); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.$_burn(holder, 10); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.$_burn(holder, 10); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.$_mint(holder, 20); - await time.advanceBlock(); - await time.advanceBlock(); - - t1.timepoint = await clockFromReceipt[mode](t1.receipt); - t2.timepoint = await clockFromReceipt[mode](t2.receipt); - t3.timepoint = await clockFromReceipt[mode](t3.receipt); - t4.timepoint = await clockFromReceipt[mode](t4.receipt); - - expect(await this.token.getPastTotalSupply(t1.timepoint - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); + const t1 = await this.token.$_mint(this.holder, supply); + await mine(2); + const t2 = await this.token.$_burn(this.holder, 10n); + await mine(2); + const t3 = await this.token.$_burn(this.holder, 10n); + await mine(2); + const t4 = await this.token.$_mint(this.holder, 20n); + await mine(2); + + t1.timepoint = await time.clockFromReceipt[mode](t1); + t2.timepoint = await time.clockFromReceipt[mode](t2); + t3.timepoint = await time.clockFromReceipt[mode](t3); + t4.timepoint = await time.clockFromReceipt[mode](t4); + + expect(await this.token.getPastTotalSupply(t1.timepoint - 1n)).to.equal(0n); + expect(await this.token.getPastTotalSupply(t1.timepoint)).to.equal(supply); + expect(await this.token.getPastTotalSupply(t1.timepoint + 1n)).to.equal(supply); + expect(await this.token.getPastTotalSupply(t2.timepoint)).to.equal(supply - 10n); + expect(await this.token.getPastTotalSupply(t2.timepoint + 1n)).to.equal(supply - 10n); + expect(await this.token.getPastTotalSupply(t3.timepoint)).to.equal(supply - 20n); + expect(await this.token.getPastTotalSupply(t3.timepoint + 1n)).to.equal(supply - 20n); + expect(await this.token.getPastTotalSupply(t4.timepoint)).to.equal(supply); + expect(await this.token.getPastTotalSupply(t4.timepoint + 1n)).to.equal(supply); }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index ba9a2a8cb6c..f52e9ca95c9 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -1,181 +1,192 @@ -/* eslint-disable */ - -const { expectEvent, time } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture, mine } = require('@nomicfoundation/hardhat-network-helpers'); -const { clock, clockFromReceipt } = require('../../../helpers/time'); +const { bigint: time } = require('../../../helpers/time'); const { shouldBehaveLikeVotes } = require('../../../governance/utils/Votes.behavior'); -const MODES = { - blocknumber: artifacts.require('$ERC721Votes'), +const TOKENS = [ + { Token: '$ERC721Votes', mode: 'blocknumber' }, // no timestamp mode for ERC721Votes yet -}; +]; + +const name = 'My Vote'; +const symbol = 'MTKN'; +const version = '1'; +const tokens = [ethers.parseEther('10000000'), 10n, 20n, 30n]; + +describe('ERC721Votes', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + // accounts is required by shouldBehaveLikeVotes + const accounts = await ethers.getSigners(); + const [holder, recipient, other1, other2] = accounts; -contract('ERC721Votes', function (accounts) { - const [account1, account2, other1, other2] = accounts; + const token = await ethers.deployContract(Token, [name, symbol, name, version]); - const name = 'My Vote'; - const symbol = 'MTKN'; - const version = '1'; - const tokens = ['10000000000000000000000000', '10', '20', '30'].map(n => web3.utils.toBN(n)); + return { accounts, holder, recipient, other1, other2, token }; + }; - for (const [mode, artifact] of Object.entries(MODES)) { describe(`vote with ${mode}`, function () { beforeEach(async function () { - this.votes = await artifact.new(name, symbol, name, version); + Object.assign(this, await loadFixture(fixture)); + this.votes = this.token; }); // includes ERC6372 behavior check - shouldBehaveLikeVotes(accounts, tokens, { mode, fungible: false }); + shouldBehaveLikeVotes(tokens, { mode, fungible: false }); describe('balanceOf', function () { beforeEach(async function () { - await this.votes.$_mint(account1, tokens[0]); - await this.votes.$_mint(account1, tokens[1]); - await this.votes.$_mint(account1, tokens[2]); - await this.votes.$_mint(account1, tokens[3]); + await this.votes.$_mint(this.holder, tokens[0]); + await this.votes.$_mint(this.holder, tokens[1]); + await this.votes.$_mint(this.holder, tokens[2]); + await this.votes.$_mint(this.holder, tokens[3]); }); it('grants to initial account', async function () { - expect(await this.votes.balanceOf(account1)).to.be.bignumber.equal('4'); + expect(await this.votes.balanceOf(this.holder)).to.equal(4n); }); }); describe('transfers', function () { beforeEach(async function () { - await this.votes.$_mint(account1, tokens[0]); + await this.votes.$_mint(this.holder, tokens[0]); }); it('no delegation', async function () { - const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); - expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: tokens[0] }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + await expect(this.votes.connect(this.holder).transferFrom(this.holder, this.recipient, tokens[0])) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.recipient.address, tokens[0]) + .to.not.emit(this.token, 'DelegateVotesChanged'); - this.account1Votes = '0'; - this.account2Votes = '0'; + this.holderVotes = 0n; + this.recipientVotes = 0n; }); it('sender delegation', async function () { - await this.votes.delegate(account1, { from: account1 }); - - const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); - expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: tokens[0] }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: account1, previousVotes: '1', newVotes: '0' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.account1Votes = '0'; - this.account2Votes = '0'; + await this.votes.connect(this.holder).delegate(this.holder); + + const tx = await this.votes.connect(this.holder).transferFrom(this.holder, this.recipient, tokens[0]); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.recipient.address, tokens[0]) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.holder.address, 1n, 0n); + + const { logs } = await tx.wait(); + const { index } = logs.find(event => event.fragment.name == 'DelegateVotesChanged'); + for (const event of logs.filter(event => event.fragment.name == 'Transfer')) { + expect(event.index).to.lt(index); + } + + this.holderVotes = 0n; + this.recipientVotes = 0n; }); it('receiver delegation', async function () { - await this.votes.delegate(account2, { from: account2 }); - - const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); - expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: tokens[0] }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: account2, previousVotes: '0', newVotes: '1' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.account1Votes = '0'; - this.account2Votes = '1'; + await this.votes.connect(this.recipient).delegate(this.recipient); + + const tx = await this.votes.connect(this.holder).transferFrom(this.holder, this.recipient, tokens[0]); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.recipient.address, tokens[0]) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.recipient.address, 0n, 1n); + + const { logs } = await tx.wait(); + const { index } = logs.find(event => event.fragment.name == 'DelegateVotesChanged'); + for (const event of logs.filter(event => event.fragment.name == 'Transfer')) { + expect(event.index).to.lt(index); + } + + this.holderVotes = 0n; + this.recipientVotes = 1n; }); it('full delegation', async function () { - await this.votes.delegate(account1, { from: account1 }); - await this.votes.delegate(account2, { from: account2 }); - - const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); - expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: tokens[0] }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: account1, previousVotes: '1', newVotes: '0' }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: account2, previousVotes: '0', newVotes: '1' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.account1Votes = '0'; - this.account2Votes = '1'; + await this.votes.connect(this.holder).delegate(this.holder); + await this.votes.connect(this.recipient).delegate(this.recipient); + + const tx = await this.votes.connect(this.holder).transferFrom(this.holder, this.recipient, tokens[0]); + await expect(tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.recipient.address, tokens[0]) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.holder.address, 1n, 0n) + .to.emit(this.token, 'DelegateVotesChanged') + .withArgs(this.recipient.address, 0n, 1n); + + const { logs } = await tx.wait(); + const { index } = logs.find(event => event.fragment.name == 'DelegateVotesChanged'); + for (const event of logs.filter(event => event.fragment.name == 'Transfer')) { + expect(event.index).to.lt(index); + } + + this.holderVotes = 0; + this.recipientVotes = 1n; }); it('returns the same total supply on transfers', async function () { - await this.votes.delegate(account1, { from: account1 }); + await this.votes.connect(this.holder).delegate(this.holder); - const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); - const timepoint = await clockFromReceipt[mode](receipt); + const tx = await this.votes.connect(this.holder).transferFrom(this.holder, this.recipient, tokens[0]); + const timepoint = await time.clockFromReceipt[mode](tx); - await time.advanceBlock(); - await time.advanceBlock(); + await mine(2); - expect(await this.votes.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('1'); - expect(await this.votes.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastTotalSupply(timepoint - 1n)).to.equal(1n); + expect(await this.votes.getPastTotalSupply(timepoint + 1n)).to.equal(1n); - this.account1Votes = '0'; - this.account2Votes = '0'; + this.holderVotes = 0n; + this.recipientVotes = 0n; }); it('generally returns the voting balance at the appropriate checkpoint', async function () { - await this.votes.$_mint(account1, tokens[1]); - await this.votes.$_mint(account1, tokens[2]); - await this.votes.$_mint(account1, tokens[3]); - - const total = await this.votes.balanceOf(account1); - - const t1 = await this.votes.delegate(other1, { from: account1 }); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.votes.transferFrom(account1, other2, tokens[0], { from: account1 }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.votes.transferFrom(account1, other2, tokens[2], { from: account1 }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.votes.transferFrom(other2, account1, tokens[2], { from: other2 }); - await time.advanceBlock(); - await time.advanceBlock(); - - t1.timepoint = await clockFromReceipt[mode](t1.receipt); - t2.timepoint = await clockFromReceipt[mode](t2.receipt); - t3.timepoint = await clockFromReceipt[mode](t3.receipt); - t4.timepoint = await clockFromReceipt[mode](t4.receipt); - - expect(await this.votes.getPastVotes(other1, t1.timepoint - 1)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal(total); - expect(await this.votes.getPastVotes(other1, t1.timepoint + 1)).to.be.bignumber.equal(total); - expect(await this.votes.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal('3'); - expect(await this.votes.getPastVotes(other1, t2.timepoint + 1)).to.be.bignumber.equal('3'); - expect(await this.votes.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal('2'); - expect(await this.votes.getPastVotes(other1, t3.timepoint + 1)).to.be.bignumber.equal('2'); - expect(await this.votes.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal('3'); - expect(await this.votes.getPastVotes(other1, t4.timepoint + 1)).to.be.bignumber.equal('3'); - - this.account1Votes = '0'; - this.account2Votes = '0'; + await this.votes.$_mint(this.holder, tokens[1]); + await this.votes.$_mint(this.holder, tokens[2]); + await this.votes.$_mint(this.holder, tokens[3]); + + const total = await this.votes.balanceOf(this.holder); + + const t1 = await this.votes.connect(this.holder).delegate(this.other1); + await mine(2); + const t2 = await this.votes.connect(this.holder).transferFrom(this.holder, this.other2, tokens[0]); + await mine(2); + const t3 = await this.votes.connect(this.holder).transferFrom(this.holder, this.other2, tokens[2]); + await mine(2); + const t4 = await this.votes.connect(this.other2).transferFrom(this.other2, this.holder, tokens[2]); + await mine(2); + + t1.timepoint = await time.clockFromReceipt[mode](t1); + t2.timepoint = await time.clockFromReceipt[mode](t2); + t3.timepoint = await time.clockFromReceipt[mode](t3); + t4.timepoint = await time.clockFromReceipt[mode](t4); + + expect(await this.votes.getPastVotes(this.other1, t1.timepoint - 1n)).to.equal(0n); + expect(await this.votes.getPastVotes(this.other1, t1.timepoint)).to.equal(total); + expect(await this.votes.getPastVotes(this.other1, t1.timepoint + 1n)).to.equal(total); + expect(await this.votes.getPastVotes(this.other1, t2.timepoint)).to.equal(3n); + expect(await this.votes.getPastVotes(this.other1, t2.timepoint + 1n)).to.equal(3n); + expect(await this.votes.getPastVotes(this.other1, t3.timepoint)).to.equal(2n); + expect(await this.votes.getPastVotes(this.other1, t3.timepoint + 1n)).to.equal(2n); + expect(await this.votes.getPastVotes(this.other1, t4.timepoint)).to.equal('3'); + expect(await this.votes.getPastVotes(this.other1, t4.timepoint + 1n)).to.equal(3n); + + this.holderVotes = 0n; + this.recipientVotes = 0n; }); afterEach(async function () { - expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(this.account1Votes); - expect(await this.votes.getVotes(account2)).to.be.bignumber.equal(this.account2Votes); + expect(await this.votes.getVotes(this.holder)).to.equal(this.holderVotes); + expect(await this.votes.getVotes(this.recipient)).to.equal(this.recipientVotes); // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" - const timepoint = await clock[mode](); - await time.advanceBlock(); - expect(await this.votes.getPastVotes(account1, timepoint)).to.be.bignumber.equal(this.account1Votes); - expect(await this.votes.getPastVotes(account2, timepoint)).to.be.bignumber.equal(this.account2Votes); + const timepoint = await time.clock[mode](); + await mine(); + expect(await this.votes.getPastVotes(this.holder, timepoint)).to.equal(this.holderVotes); + expect(await this.votes.getPastVotes(this.recipient, timepoint)).to.equal(this.recipientVotes); }); }); }); From 5bca2119ca634a0f7df4e2c3abf468e90c614119 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 19 Dec 2023 02:28:16 +0100 Subject: [PATCH 37/44] Migrate ERC165 tests (#4794) --- test/helpers/methods.js | 11 +- test/utils/introspection/ERC165.test.js | 11 +- .../utils/introspection/ERC165Checker.test.js | 219 +++++++----------- .../SupportsInterface.behavior.js | 26 +-- 4 files changed, 111 insertions(+), 156 deletions(-) diff --git a/test/helpers/methods.js b/test/helpers/methods.js index 94f01cff018..a4918972019 100644 --- a/test/helpers/methods.js +++ b/test/helpers/methods.js @@ -1,5 +1,14 @@ const { ethers } = require('hardhat'); +const selector = signature => ethers.FunctionFragment.from(signature).selector; + +const interfaceId = signatures => + ethers.toBeHex( + signatures.reduce((acc, signature) => acc ^ ethers.toBigInt(selector(signature)), 0n), + 4, + ); + module.exports = { - selector: signature => ethers.FunctionFragment.from(signature).selector, + selector, + interfaceId, }; diff --git a/test/utils/introspection/ERC165.test.js b/test/utils/introspection/ERC165.test.js index 6d531c16d40..d72791218b0 100644 --- a/test/utils/introspection/ERC165.test.js +++ b/test/utils/introspection/ERC165.test.js @@ -1,10 +1,17 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + const { shouldSupportInterfaces } = require('./SupportsInterface.behavior'); -const ERC165 = artifacts.require('$ERC165'); +async function fixture() { + return { + mock: await ethers.deployContract('$ERC165'), + }; +} contract('ERC165', function () { beforeEach(async function () { - this.mock = await ERC165.new(); + Object.assign(this, await loadFixture(fixture)); }); shouldSupportInterfaces(['ERC165']); diff --git a/test/utils/introspection/ERC165Checker.test.js b/test/utils/introspection/ERC165Checker.test.js index caa22012717..1bbe8a57130 100644 --- a/test/utils/introspection/ERC165Checker.test.js +++ b/test/utils/introspection/ERC165Checker.test.js @@ -1,13 +1,6 @@ -require('@openzeppelin/test-helpers'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); - -const ERC165Checker = artifacts.require('$ERC165Checker'); -const ERC165MissingData = artifacts.require('ERC165MissingData'); -const ERC165MaliciousData = artifacts.require('ERC165MaliciousData'); -const ERC165NotSupported = artifacts.require('ERC165NotSupported'); -const ERC165InterfacesSupported = artifacts.require('ERC165InterfacesSupported'); -const ERC165ReturnBombMock = artifacts.require('ERC165ReturnBombMock'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const DUMMY_ID = '0xdeadbeef'; const DUMMY_ID_2 = '0xcafebabe'; @@ -16,285 +9,237 @@ const DUMMY_UNSUPPORTED_ID = '0xbaddcafe'; const DUMMY_UNSUPPORTED_ID_2 = '0xbaadcafe'; const DUMMY_ACCOUNT = '0x1111111111111111111111111111111111111111'; -contract('ERC165Checker', function () { +async function fixture() { + return { mock: await ethers.deployContract('$ERC165Checker') }; +} + +describe('ERC165Checker', function () { beforeEach(async function () { - this.mock = await ERC165Checker.new(); + Object.assign(this, await loadFixture(fixture)); }); - context('ERC165 missing return data', function () { - beforeEach(async function () { - this.target = await ERC165MissingData.new(); + describe('ERC165 missing return data', function () { + before(async function () { + this.target = await ethers.deployContract('ERC165MissingData'); }); it('does not support ERC165', async function () { - const supported = await this.mock.$supportsERC165(this.target.address); - expect(supported).to.equal(false); + expect(await this.mock.$supportsERC165(this.target)).to.be.false; }); it('does not support mock interface via supportsInterface', async function () { - const supported = await this.mock.$supportsInterface(this.target.address, DUMMY_ID); - expect(supported).to.equal(false); + expect(await this.mock.$supportsInterface(this.target, DUMMY_ID)).to.be.false; }); it('does not support mock interface via supportsAllInterfaces', async function () { - const supported = await this.mock.$supportsAllInterfaces(this.target.address, [DUMMY_ID]); - expect(supported).to.equal(false); + expect(await this.mock.$supportsAllInterfaces(this.target, [DUMMY_ID])).to.be.false; }); it('does not support mock interface via getSupportedInterfaces', async function () { - const supported = await this.mock.$getSupportedInterfaces(this.target.address, [DUMMY_ID]); - expect(supported.length).to.equal(1); - expect(supported[0]).to.equal(false); + expect(await this.mock.$getSupportedInterfaces(this.target, [DUMMY_ID])).to.deep.equal([false]); }); it('does not support mock interface via supportsERC165InterfaceUnchecked', async function () { - const supported = await this.mock.$supportsERC165InterfaceUnchecked(this.target.address, DUMMY_ID); - expect(supported).to.equal(false); + expect(await this.mock.$supportsERC165InterfaceUnchecked(this.target, DUMMY_ID)).to.be.false; }); }); - context('ERC165 malicious return data', function () { + describe('ERC165 malicious return data', function () { beforeEach(async function () { - this.target = await ERC165MaliciousData.new(); + this.target = await ethers.deployContract('ERC165MaliciousData'); }); it('does not support ERC165', async function () { - const supported = await this.mock.$supportsERC165(this.target.address); - expect(supported).to.equal(false); + expect(await this.mock.$supportsERC165(this.target)).to.be.false; }); it('does not support mock interface via supportsInterface', async function () { - const supported = await this.mock.$supportsInterface(this.target.address, DUMMY_ID); - expect(supported).to.equal(false); + expect(await this.mock.$supportsInterface(this.target, DUMMY_ID)).to.be.false; }); it('does not support mock interface via supportsAllInterfaces', async function () { - const supported = await this.mock.$supportsAllInterfaces(this.target.address, [DUMMY_ID]); - expect(supported).to.equal(false); + expect(await this.mock.$supportsAllInterfaces(this.target, [DUMMY_ID])).to.be.false; }); it('does not support mock interface via getSupportedInterfaces', async function () { - const supported = await this.mock.$getSupportedInterfaces(this.target.address, [DUMMY_ID]); - expect(supported.length).to.equal(1); - expect(supported[0]).to.equal(false); + expect(await this.mock.$getSupportedInterfaces(this.target, [DUMMY_ID])).to.deep.equal([false]); }); it('does not support mock interface via supportsERC165InterfaceUnchecked', async function () { - const supported = await this.mock.$supportsERC165InterfaceUnchecked(this.target.address, DUMMY_ID); - expect(supported).to.equal(true); + expect(await this.mock.$supportsERC165InterfaceUnchecked(this.target, DUMMY_ID)).to.be.true; }); }); - context('ERC165 not supported', function () { + describe('ERC165 not supported', function () { beforeEach(async function () { - this.target = await ERC165NotSupported.new(); + this.target = await ethers.deployContract('ERC165NotSupported'); }); it('does not support ERC165', async function () { - const supported = await this.mock.$supportsERC165(this.target.address); - expect(supported).to.equal(false); + expect(await this.mock.$supportsERC165(this.target)).to.be.false; }); it('does not support mock interface via supportsInterface', async function () { - const supported = await this.mock.$supportsInterface(this.target.address, DUMMY_ID); - expect(supported).to.equal(false); + expect(await this.mock.$supportsInterface(this.target, DUMMY_ID)).to.be.false; }); it('does not support mock interface via supportsAllInterfaces', async function () { - const supported = await this.mock.$supportsAllInterfaces(this.target.address, [DUMMY_ID]); - expect(supported).to.equal(false); + expect(await this.mock.$supportsAllInterfaces(this.target, [DUMMY_ID])).to.be.false; }); it('does not support mock interface via getSupportedInterfaces', async function () { - const supported = await this.mock.$getSupportedInterfaces(this.target.address, [DUMMY_ID]); - expect(supported.length).to.equal(1); - expect(supported[0]).to.equal(false); + expect(await this.mock.$getSupportedInterfaces(this.target, [DUMMY_ID])).to.deep.equal([false]); }); it('does not support mock interface via supportsERC165InterfaceUnchecked', async function () { - const supported = await this.mock.$supportsERC165InterfaceUnchecked(this.target.address, DUMMY_ID); - expect(supported).to.equal(false); + expect(await this.mock.$supportsERC165InterfaceUnchecked(this.target, DUMMY_ID)).to.be.false; }); }); - context('ERC165 supported', function () { + describe('ERC165 supported', function () { beforeEach(async function () { - this.target = await ERC165InterfacesSupported.new([]); + this.target = await ethers.deployContract('ERC165InterfacesSupported', [[]]); }); it('supports ERC165', async function () { - const supported = await this.mock.$supportsERC165(this.target.address); - expect(supported).to.equal(true); + expect(await this.mock.$supportsERC165(this.target)).to.be.true; }); it('does not support mock interface via supportsInterface', async function () { - const supported = await this.mock.$supportsInterface(this.target.address, DUMMY_ID); - expect(supported).to.equal(false); + expect(await this.mock.$supportsInterface(this.target, DUMMY_ID)).to.be.false; }); it('does not support mock interface via supportsAllInterfaces', async function () { - const supported = await this.mock.$supportsAllInterfaces(this.target.address, [DUMMY_ID]); - expect(supported).to.equal(false); + expect(await this.mock.$supportsAllInterfaces(this.target, [DUMMY_ID])).to.be.false; }); it('does not support mock interface via getSupportedInterfaces', async function () { - const supported = await this.mock.$getSupportedInterfaces(this.target.address, [DUMMY_ID]); - expect(supported.length).to.equal(1); - expect(supported[0]).to.equal(false); + expect(await this.mock.$getSupportedInterfaces(this.target, [DUMMY_ID])).to.deep.equal([false]); }); it('does not support mock interface via supportsERC165InterfaceUnchecked', async function () { - const supported = await this.mock.$supportsERC165InterfaceUnchecked(this.target.address, DUMMY_ID); - expect(supported).to.equal(false); + expect(await this.mock.$supportsERC165InterfaceUnchecked(this.target, DUMMY_ID)).to.be.false; }); }); - context('ERC165 and single interface supported', function () { + describe('ERC165 and single interface supported', function () { beforeEach(async function () { - this.target = await ERC165InterfacesSupported.new([DUMMY_ID]); + this.target = await ethers.deployContract('ERC165InterfacesSupported', [[DUMMY_ID]]); }); it('supports ERC165', async function () { - const supported = await this.mock.$supportsERC165(this.target.address); - expect(supported).to.equal(true); + expect(await this.mock.$supportsERC165(this.target)).to.be.true; }); it('supports mock interface via supportsInterface', async function () { - const supported = await this.mock.$supportsInterface(this.target.address, DUMMY_ID); - expect(supported).to.equal(true); + expect(await this.mock.$supportsInterface(this.target, DUMMY_ID)).to.be.true; }); it('supports mock interface via supportsAllInterfaces', async function () { - const supported = await this.mock.$supportsAllInterfaces(this.target.address, [DUMMY_ID]); - expect(supported).to.equal(true); + expect(await this.mock.$supportsAllInterfaces(this.target, [DUMMY_ID])).to.be.true; }); it('supports mock interface via getSupportedInterfaces', async function () { - const supported = await this.mock.$getSupportedInterfaces(this.target.address, [DUMMY_ID]); - expect(supported.length).to.equal(1); - expect(supported[0]).to.equal(true); + expect(await this.mock.$getSupportedInterfaces(this.target, [DUMMY_ID])).to.deep.equal([true]); }); it('supports mock interface via supportsERC165InterfaceUnchecked', async function () { - const supported = await this.mock.$supportsERC165InterfaceUnchecked(this.target.address, DUMMY_ID); - expect(supported).to.equal(true); + expect(await this.mock.$supportsERC165InterfaceUnchecked(this.target, DUMMY_ID)).to.be.true; }); }); - context('ERC165 and many interfaces supported', function () { + describe('ERC165 and many interfaces supported', function () { + const supportedInterfaces = [DUMMY_ID, DUMMY_ID_2, DUMMY_ID_3]; beforeEach(async function () { - this.supportedInterfaces = [DUMMY_ID, DUMMY_ID_2, DUMMY_ID_3]; - this.target = await ERC165InterfacesSupported.new(this.supportedInterfaces); + this.target = await ethers.deployContract('ERC165InterfacesSupported', [supportedInterfaces]); }); it('supports ERC165', async function () { - const supported = await this.mock.$supportsERC165(this.target.address); - expect(supported).to.equal(true); + expect(await this.mock.$supportsERC165(this.target)).to.be.true; }); it('supports each interfaceId via supportsInterface', async function () { - for (const interfaceId of this.supportedInterfaces) { - const supported = await this.mock.$supportsInterface(this.target.address, interfaceId); - expect(supported).to.equal(true); + for (const interfaceId of supportedInterfaces) { + expect(await this.mock.$supportsInterface(this.target, interfaceId)).to.be.true; } }); it('supports all interfaceIds via supportsAllInterfaces', async function () { - const supported = await this.mock.$supportsAllInterfaces(this.target.address, this.supportedInterfaces); - expect(supported).to.equal(true); + expect(await this.mock.$supportsAllInterfaces(this.target, supportedInterfaces)).to.be.true; }); it('supports none of the interfaces queried via supportsAllInterfaces', async function () { const interfaceIdsToTest = [DUMMY_UNSUPPORTED_ID, DUMMY_UNSUPPORTED_ID_2]; - const supported = await this.mock.$supportsAllInterfaces(this.target.address, interfaceIdsToTest); - expect(supported).to.equal(false); + expect(await this.mock.$supportsAllInterfaces(this.target, interfaceIdsToTest)).to.be.false; }); it('supports not all of the interfaces queried via supportsAllInterfaces', async function () { - const interfaceIdsToTest = [...this.supportedInterfaces, DUMMY_UNSUPPORTED_ID]; - - const supported = await this.mock.$supportsAllInterfaces(this.target.address, interfaceIdsToTest); - expect(supported).to.equal(false); + const interfaceIdsToTest = [...supportedInterfaces, DUMMY_UNSUPPORTED_ID]; + expect(await this.mock.$supportsAllInterfaces(this.target, interfaceIdsToTest)).to.be.false; }); it('supports all interfaceIds via getSupportedInterfaces', async function () { - const supported = await this.mock.$getSupportedInterfaces(this.target.address, this.supportedInterfaces); - expect(supported.length).to.equal(3); - expect(supported[0]).to.equal(true); - expect(supported[1]).to.equal(true); - expect(supported[2]).to.equal(true); + expect(await this.mock.$getSupportedInterfaces(this.target, supportedInterfaces)).to.deep.equal( + supportedInterfaces.map(i => supportedInterfaces.includes(i)), + ); }); it('supports none of the interfaces queried via getSupportedInterfaces', async function () { const interfaceIdsToTest = [DUMMY_UNSUPPORTED_ID, DUMMY_UNSUPPORTED_ID_2]; - const supported = await this.mock.$getSupportedInterfaces(this.target.address, interfaceIdsToTest); - expect(supported.length).to.equal(2); - expect(supported[0]).to.equal(false); - expect(supported[1]).to.equal(false); + expect(await this.mock.$getSupportedInterfaces(this.target, interfaceIdsToTest)).to.deep.equal( + interfaceIdsToTest.map(i => supportedInterfaces.includes(i)), + ); }); it('supports not all of the interfaces queried via getSupportedInterfaces', async function () { - const interfaceIdsToTest = [...this.supportedInterfaces, DUMMY_UNSUPPORTED_ID]; + const interfaceIdsToTest = [...supportedInterfaces, DUMMY_UNSUPPORTED_ID]; - const supported = await this.mock.$getSupportedInterfaces(this.target.address, interfaceIdsToTest); - expect(supported.length).to.equal(4); - expect(supported[0]).to.equal(true); - expect(supported[1]).to.equal(true); - expect(supported[2]).to.equal(true); - expect(supported[3]).to.equal(false); + expect(await this.mock.$getSupportedInterfaces(this.target, interfaceIdsToTest)).to.deep.equal( + interfaceIdsToTest.map(i => supportedInterfaces.includes(i)), + ); }); it('supports each interfaceId via supportsERC165InterfaceUnchecked', async function () { - for (const interfaceId of this.supportedInterfaces) { - const supported = await this.mock.$supportsERC165InterfaceUnchecked(this.target.address, interfaceId); - expect(supported).to.equal(true); + for (const interfaceId of supportedInterfaces) { + expect(await this.mock.$supportsERC165InterfaceUnchecked(this.target, interfaceId)).to.be.true; } }); }); - context('account address does not support ERC165', function () { + describe('account address does not support ERC165', function () { it('does not support ERC165', async function () { - const supported = await this.mock.$supportsERC165(DUMMY_ACCOUNT); - expect(supported).to.equal(false); + expect(await this.mock.$supportsERC165(DUMMY_ACCOUNT)).to.be.false; }); it('does not support mock interface via supportsInterface', async function () { - const supported = await this.mock.$supportsInterface(DUMMY_ACCOUNT, DUMMY_ID); - expect(supported).to.equal(false); + expect(await this.mock.$supportsInterface(DUMMY_ACCOUNT, DUMMY_ID)).to.be.false; }); it('does not support mock interface via supportsAllInterfaces', async function () { - const supported = await this.mock.$supportsAllInterfaces(DUMMY_ACCOUNT, [DUMMY_ID]); - expect(supported).to.equal(false); + expect(await this.mock.$supportsAllInterfaces(DUMMY_ACCOUNT, [DUMMY_ID])).to.be.false; }); it('does not support mock interface via getSupportedInterfaces', async function () { - const supported = await this.mock.$getSupportedInterfaces(DUMMY_ACCOUNT, [DUMMY_ID]); - expect(supported.length).to.equal(1); - expect(supported[0]).to.equal(false); + expect(await this.mock.$getSupportedInterfaces(DUMMY_ACCOUNT, [DUMMY_ID])).to.deep.equal([false]); }); it('does not support mock interface via supportsERC165InterfaceUnchecked', async function () { - const supported = await this.mock.$supportsERC165InterfaceUnchecked(DUMMY_ACCOUNT, DUMMY_ID); - expect(supported).to.equal(false); + expect(await this.mock.$supportsERC165InterfaceUnchecked(DUMMY_ACCOUNT, DUMMY_ID)).to.be.false; }); }); it('Return bomb resistance', async function () { - this.target = await ERC165ReturnBombMock.new(); - - const tx1 = await this.mock.$supportsInterface.sendTransaction(this.target.address, DUMMY_ID); - expect(tx1.receipt.gasUsed).to.be.lessThan(120000); // 3*30k + 21k + some margin - - const tx2 = await this.mock.$getSupportedInterfaces.sendTransaction(this.target.address, [ - DUMMY_ID, - DUMMY_ID_2, - DUMMY_ID_3, - DUMMY_UNSUPPORTED_ID, - DUMMY_UNSUPPORTED_ID_2, - ]); - expect(tx2.receipt.gasUsed).to.be.lessThan(250000); // (2+5)*30k + 21k + some margin + this.target = await ethers.deployContract('ERC165ReturnBombMock'); + + const { gasUsed: gasUsed1 } = await this.mock.$supportsInterface.send(this.target, DUMMY_ID).then(tx => tx.wait()); + expect(gasUsed1).to.be.lessThan(120_000n); // 3*30k + 21k + some margin + + const { gasUsed: gasUsed2 } = await this.mock.$getSupportedInterfaces + .send(this.target, [DUMMY_ID, DUMMY_ID_2, DUMMY_ID_3, DUMMY_UNSUPPORTED_ID, DUMMY_UNSUPPORTED_ID_2]) + .then(tx => tx.wait()); + + expect(gasUsed2).to.be.lessThan(250_000n); // (2+5)*30k + 21k + some margin }); }); diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index 243dfb5d620..7a6df2c1445 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -1,6 +1,6 @@ -const { ethers } = require('ethers'); const { expect } = require('chai'); -const { selector } = require('../../helpers/methods'); +const { selector, interfaceId } = require('../../helpers/methods'); +const { mapValues } = require('../../helpers/iterate'); const INVALID_ID = '0xffffffff'; const SIGNATURES = { @@ -81,15 +81,7 @@ const SIGNATURES = { ERC2981: ['royaltyInfo(uint256,uint256)'], }; -const INTERFACE_IDS = Object.fromEntries( - Object.entries(SIGNATURES).map(([name, signatures]) => [ - name, - ethers.toBeHex( - signatures.reduce((id, fnSig) => id ^ BigInt(selector(fnSig)), 0n), - 4, - ), - ]), -); +const INTERFACE_IDS = mapValues(SIGNATURES, interfaceId); function shouldSupportInterfaces(interfaces = []) { describe('ERC165', function () { @@ -101,25 +93,25 @@ function shouldSupportInterfaces(interfaces = []) { it('uses less than 30k gas', async function () { for (const k of interfaces) { const interface = INTERFACE_IDS[k] ?? k; - expect(await this.contractUnderTest.supportsInterface.estimateGas(interface)).to.be.lte(30000); + expect(await this.contractUnderTest.supportsInterface.estimateGas(interface)).to.lte(30_000n); } }); it('returns true', async function () { for (const k of interfaces) { const interfaceId = INTERFACE_IDS[k] ?? k; - expect(await this.contractUnderTest.supportsInterface(interfaceId)).to.equal(true, `does not support ${k}`); + expect(await this.contractUnderTest.supportsInterface(interfaceId), `does not support ${k}`).to.be.true; } }); }); describe('when the interfaceId is not supported', function () { it('uses less than 30k', async function () { - expect(await this.contractUnderTest.supportsInterface.estimateGas(INVALID_ID)).to.be.lte(30000); + expect(await this.contractUnderTest.supportsInterface.estimateGas(INVALID_ID)).to.lte(30_000n); }); it('returns false', async function () { - expect(await this.contractUnderTest.supportsInterface(INVALID_ID)).to.be.equal(false, `supports ${INVALID_ID}`); + expect(await this.contractUnderTest.supportsInterface(INVALID_ID), `supports ${INVALID_ID}`).to.be.false; }); }); @@ -127,6 +119,8 @@ function shouldSupportInterfaces(interfaces = []) { for (const k of interfaces) { // skip interfaces for which we don't have a function list if (SIGNATURES[k] === undefined) continue; + + // Check the presence of each function in the contract's interface for (const fnSig of SIGNATURES[k]) { // TODO: Remove Truffle case when ethersjs migration is done if (this.contractUnderTest.abi) { @@ -137,7 +131,7 @@ function shouldSupportInterfaces(interfaces = []) { ); } - expect(!!this.contractUnderTest.interface.getFunction(fnSig), `did not find ${fnSig}`).to.be.true; + expect(this.contractUnderTest.interface.hasFunction(fnSig), `did not find ${fnSig}`).to.be.true; } } }); From 44965d7779f89cc97b8dbc6e473d3931ecd9e4b2 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 19 Dec 2023 10:00:16 +0100 Subject: [PATCH 38/44] Migrate SafeERC20.test.js (#4798) Co-authored-by: ernestognw --- test/token/ERC20/utils/SafeERC20.test.js | 206 +++++++++++------------ 1 file changed, 98 insertions(+), 108 deletions(-) diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index 4ff27f14d39..e710a324126 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -1,239 +1,229 @@ -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); - -const SafeERC20 = artifacts.require('$SafeERC20'); -const ERC20ReturnFalseMock = artifacts.require('$ERC20ReturnFalseMock'); -const ERC20ReturnTrueMock = artifacts.require('$ERC20'); // default implementation returns true -const ERC20NoReturnMock = artifacts.require('$ERC20NoReturnMock'); -const ERC20ForceApproveMock = artifacts.require('$ERC20ForceApproveMock'); - -const { expectRevertCustomError } = require('../../../helpers/customError'); +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const name = 'ERC20Mock'; const symbol = 'ERC20Mock'; -contract('SafeERC20', function (accounts) { - const [hasNoCode, receiver, spender] = accounts; +async function fixture() { + const [hasNoCode, owner, receiver, spender] = await ethers.getSigners(); + + const mock = await ethers.deployContract('$SafeERC20'); + const erc20ReturnFalseMock = await ethers.deployContract('$ERC20ReturnFalseMock', [name, symbol]); + const erc20ReturnTrueMock = await ethers.deployContract('$ERC20', [name, symbol]); // default implementation returns true + const erc20NoReturnMock = await ethers.deployContract('$ERC20NoReturnMock', [name, symbol]); + const erc20ForceApproveMock = await ethers.deployContract('$ERC20ForceApproveMock', [name, symbol]); + + return { + hasNoCode, + owner, + receiver, + spender, + mock, + erc20ReturnFalseMock, + erc20ReturnTrueMock, + erc20NoReturnMock, + erc20ForceApproveMock, + }; +} +describe('SafeERC20', function () { before(async function () { - this.mock = await SafeERC20.new(); + Object.assign(this, await loadFixture(fixture)); }); describe('with address that has no contract code', function () { beforeEach(async function () { - this.token = { address: hasNoCode }; + this.token = this.hasNoCode; }); it('reverts on transfer', async function () { - await expectRevertCustomError(this.mock.$safeTransfer(this.token.address, receiver, 0), 'AddressEmptyCode', [ - this.token.address, - ]); + await expect(this.mock.$safeTransfer(this.token, this.receiver, 0n)) + .to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode') + .withArgs(this.token.address); }); it('reverts on transferFrom', async function () { - await expectRevertCustomError( - this.mock.$safeTransferFrom(this.token.address, this.mock.address, receiver, 0), - 'AddressEmptyCode', - [this.token.address], - ); + await expect(this.mock.$safeTransferFrom(this.token, this.mock, this.receiver, 0n)) + .to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode') + .withArgs(this.token.address); }); it('reverts on increaseAllowance', async function () { // Call to 'token.allowance' does not return any data, resulting in a decoding error (revert without reason) - await expectRevert.unspecified(this.mock.$safeIncreaseAllowance(this.token.address, spender, 0)); + await expect(this.mock.$safeIncreaseAllowance(this.token, this.spender, 0n)).to.be.revertedWithoutReason(); }); it('reverts on decreaseAllowance', async function () { // Call to 'token.allowance' does not return any data, resulting in a decoding error (revert without reason) - await expectRevert.unspecified(this.mock.$safeDecreaseAllowance(this.token.address, spender, 0)); + await expect(this.mock.$safeDecreaseAllowance(this.token, this.spender, 0n)).to.be.revertedWithoutReason(); }); it('reverts on forceApprove', async function () { - await expectRevertCustomError(this.mock.$forceApprove(this.token.address, spender, 0), 'AddressEmptyCode', [ - this.token.address, - ]); + await expect(this.mock.$forceApprove(this.token, this.spender, 0n)) + .to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode') + .withArgs(this.token.address); }); }); describe('with token that returns false on all calls', function () { beforeEach(async function () { - this.token = await ERC20ReturnFalseMock.new(name, symbol); + this.token = this.erc20ReturnFalseMock; }); it('reverts on transfer', async function () { - await expectRevertCustomError( - this.mock.$safeTransfer(this.token.address, receiver, 0), - 'SafeERC20FailedOperation', - [this.token.address], - ); + await expect(this.mock.$safeTransfer(this.token, this.receiver, 0n)) + .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation') + .withArgs(this.token.target); }); it('reverts on transferFrom', async function () { - await expectRevertCustomError( - this.mock.$safeTransferFrom(this.token.address, this.mock.address, receiver, 0), - 'SafeERC20FailedOperation', - [this.token.address], - ); + await expect(this.mock.$safeTransferFrom(this.token, this.mock, this.receiver, 0n)) + .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation') + .withArgs(this.token.target); }); it('reverts on increaseAllowance', async function () { - await expectRevertCustomError( - this.mock.$safeIncreaseAllowance(this.token.address, spender, 0), - 'SafeERC20FailedOperation', - [this.token.address], - ); + await expect(this.mock.$safeIncreaseAllowance(this.token, this.spender, 0n)) + .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation') + .withArgs(this.token.target); }); it('reverts on decreaseAllowance', async function () { - await expectRevertCustomError( - this.mock.$safeDecreaseAllowance(this.token.address, spender, 0), - 'SafeERC20FailedOperation', - [this.token.address], - ); + await expect(this.mock.$safeDecreaseAllowance(this.token, this.spender, 0n)) + .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation') + .withArgs(this.token.target); }); it('reverts on forceApprove', async function () { - await expectRevertCustomError( - this.mock.$forceApprove(this.token.address, spender, 0), - 'SafeERC20FailedOperation', - [this.token.address], - ); + await expect(this.mock.$forceApprove(this.token, this.spender, 0n)) + .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation') + .withArgs(this.token.target); }); }); describe('with token that returns true on all calls', function () { beforeEach(async function () { - this.token = await ERC20ReturnTrueMock.new(name, symbol); + this.token = this.erc20ReturnTrueMock; }); - shouldOnlyRevertOnErrors(accounts); + shouldOnlyRevertOnErrors(); }); describe('with token that returns no boolean values', function () { beforeEach(async function () { - this.token = await ERC20NoReturnMock.new(name, symbol); + this.token = this.erc20NoReturnMock; }); - shouldOnlyRevertOnErrors(accounts); + shouldOnlyRevertOnErrors(); }); describe('with usdt approval beaviour', function () { - const spender = hasNoCode; - beforeEach(async function () { - this.token = await ERC20ForceApproveMock.new(name, symbol); + this.token = this.erc20ForceApproveMock; }); describe('with initial approval', function () { beforeEach(async function () { - await this.token.$_approve(this.mock.address, spender, 100); + await this.token.$_approve(this.mock, this.spender, 100n); }); it('safeIncreaseAllowance works', async function () { - await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10); - expect(this.token.allowance(this.mock.address, spender, 90)); + await this.mock.$safeIncreaseAllowance(this.token, this.spender, 10n); + expect(await this.token.allowance(this.mock, this.spender)).to.equal(110n); }); it('safeDecreaseAllowance works', async function () { - await this.mock.$safeDecreaseAllowance(this.token.address, spender, 10); - expect(this.token.allowance(this.mock.address, spender, 110)); + await this.mock.$safeDecreaseAllowance(this.token, this.spender, 10n); + expect(await this.token.allowance(this.mock, this.spender)).to.equal(90n); }); it('forceApprove works', async function () { - await this.mock.$forceApprove(this.token.address, spender, 200); - expect(this.token.allowance(this.mock.address, spender, 200)); + await this.mock.$forceApprove(this.token, this.spender, 200n); + expect(await this.token.allowance(this.mock, this.spender)).to.equal(200n); }); }); }); }); -function shouldOnlyRevertOnErrors([owner, receiver, spender]) { +function shouldOnlyRevertOnErrors() { describe('transfers', function () { beforeEach(async function () { - await this.token.$_mint(owner, 100); - await this.token.$_mint(this.mock.address, 100); - await this.token.approve(this.mock.address, constants.MAX_UINT256, { from: owner }); + await this.token.$_mint(this.owner, 100n); + await this.token.$_mint(this.mock, 100n); + await this.token.$_approve(this.owner, this.mock, ethers.MaxUint256); }); it("doesn't revert on transfer", async function () { - const { tx } = await this.mock.$safeTransfer(this.token.address, receiver, 10); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.mock.address, - to: receiver, - value: '10', - }); + await expect(this.mock.$safeTransfer(this.token, this.receiver, 10n)) + .to.emit(this.token, 'Transfer') + .withArgs(this.mock.target, this.receiver.address, 10n); }); it("doesn't revert on transferFrom", async function () { - const { tx } = await this.mock.$safeTransferFrom(this.token.address, owner, receiver, 10); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: owner, - to: receiver, - value: '10', - }); + await expect(this.mock.$safeTransferFrom(this.token, this.owner, this.receiver, 10n)) + .to.emit(this.token, 'Transfer') + .withArgs(this.owner.address, this.receiver.address, 10n); }); }); describe('approvals', function () { context('with zero allowance', function () { beforeEach(async function () { - await this.token.$_approve(this.mock.address, spender, 0); + await this.token.$_approve(this.mock, this.spender, 0n); }); it("doesn't revert when force approving a non-zero allowance", async function () { - await this.mock.$forceApprove(this.token.address, spender, 100); - expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('100'); + await this.mock.$forceApprove(this.token, this.spender, 100n); + expect(await this.token.allowance(this.mock, this.spender)).to.equal(100n); }); it("doesn't revert when force approving a zero allowance", async function () { - await this.mock.$forceApprove(this.token.address, spender, 0); - expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0'); + await this.mock.$forceApprove(this.token, this.spender, 0n); + expect(await this.token.allowance(this.mock, this.spender)).to.equal(0n); }); it("doesn't revert when increasing the allowance", async function () { - await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10); - expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('10'); + await this.mock.$safeIncreaseAllowance(this.token, this.spender, 10n); + expect(await this.token.allowance(this.mock, this.spender)).to.equal(10n); }); it('reverts when decreasing the allowance', async function () { - await expectRevertCustomError( - this.mock.$safeDecreaseAllowance(this.token.address, spender, 10), - 'SafeERC20FailedDecreaseAllowance', - [spender, 0, 10], - ); + await expect(this.mock.$safeDecreaseAllowance(this.token, this.spender, 10n)) + .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedDecreaseAllowance') + .withArgs(this.spender.address, 0n, 10n); }); }); context('with non-zero allowance', function () { beforeEach(async function () { - await this.token.$_approve(this.mock.address, spender, 100); + await this.token.$_approve(this.mock, this.spender, 100n); }); it("doesn't revert when force approving a non-zero allowance", async function () { - await this.mock.$forceApprove(this.token.address, spender, 20); - expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('20'); + await this.mock.$forceApprove(this.token, this.spender, 20n); + expect(await this.token.allowance(this.mock, this.spender)).to.equal(20n); }); it("doesn't revert when force approving a zero allowance", async function () { - await this.mock.$forceApprove(this.token.address, spender, 0); - expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0'); + await this.mock.$forceApprove(this.token, this.spender, 0n); + expect(await this.token.allowance(this.mock, this.spender)).to.equal(0n); }); it("doesn't revert when increasing the allowance", async function () { - await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10); - expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('110'); + await this.mock.$safeIncreaseAllowance(this.token, this.spender, 10n); + expect(await this.token.allowance(this.mock, this.spender)).to.equal(110n); }); it("doesn't revert when decreasing the allowance to a positive value", async function () { - await this.mock.$safeDecreaseAllowance(this.token.address, spender, 50); - expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('50'); + await this.mock.$safeDecreaseAllowance(this.token, this.spender, 50n); + expect(await this.token.allowance(this.mock, this.spender)).to.equal(50n); }); it('reverts when decreasing the allowance to a negative value', async function () { - await expectRevertCustomError( - this.mock.$safeDecreaseAllowance(this.token.address, spender, 200), - 'SafeERC20FailedDecreaseAllowance', - [spender, 100, 200], - ); + await expect(this.mock.$safeDecreaseAllowance(this.token, this.spender, 200n)) + .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedDecreaseAllowance') + .withArgs(this.spender.address, 100n, 200n); }); }); }); From f627500649547942300719372ebdbc2113d7c1a1 Mon Sep 17 00:00:00 2001 From: NiftyMike Date: Tue, 19 Dec 2023 08:14:25 -0600 Subject: [PATCH 39/44] Update SupportsInterface.behavior.js (#4674) Co-authored-by: ernestognw --- test/token/ERC1155/ERC1155.behavior.js | 2 +- test/utils/introspection/SupportsInterface.behavior.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/token/ERC1155/ERC1155.behavior.js b/test/token/ERC1155/ERC1155.behavior.js index 8df30a81460..9f76fae2948 100644 --- a/test/token/ERC1155/ERC1155.behavior.js +++ b/test/token/ERC1155/ERC1155.behavior.js @@ -896,7 +896,7 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); }); - shouldSupportInterfaces(['ERC165', 'ERC1155']); + shouldSupportInterfaces(['ERC165', 'ERC1155', 'ERC1155MetadataURI']); }); } diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index 7a6df2c1445..83b2645927c 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -26,6 +26,7 @@ const SIGNATURES = { 'safeTransferFrom(address,address,uint256,uint256,bytes)', 'safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)', ], + ERC1155MetadataURI: ['uri(uint256)'], ERC1155Receiver: [ 'onERC1155Received(address,address,uint256,uint256,bytes)', 'onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)', From f213a10522a7bd808561c5a4b17266065a199dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 19 Dec 2023 14:56:43 -0600 Subject: [PATCH 40/44] Remove Governor's guide ERC6372 disclaimer for Tally (#4801) --- docs/modules/ROOT/pages/governance.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 18c335ff400..fda51e6ff05 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -32,7 +32,7 @@ When using a timelock with your Governor contract, you can use either OpenZeppel https://www.tally.xyz[Tally] is a full-fledged application for user owned on-chain governance. It comprises a voting dashboard, proposal creation wizard, real time research and analysis, and educational content. -For all of these options, the Governor will be compatible with Tally: users will be able to create proposals, visualize voting power and advocates, navigate proposals, and cast votes. For proposal creation in particular, projects can also use Defender Admin as an alternative interface. +For all of these options, the Governor will be compatible with Tally: users will be able to create proposals, see voting periods and delays following xref:api:interfaces.adoc#IERC6372[IERC6372], visualize voting power and advocates, navigate proposals, and cast votes. For proposal creation in particular, projects can also use Defender Admin as an alternative interface. In the rest of this guide, we will focus on a fresh deploy of the vanilla OpenZeppelin Governor features without concern for compatibility with GovernorAlpha or Bravo. @@ -235,6 +235,6 @@ contract MyGovernor is Governor, GovernorCountingSimple, GovernorVotes, Governor === Disclaimer -Timestamp based voting is a recent feature that was formalized in ERC-6372 and ERC-5805, and introduced in v4.9. At the time this feature is released, governance tooling such as https://www.tally.xyz[Tally] does not support it yet. While support for timestamps should come soon, users can expect invalid reporting of deadlines & durations. This invalid reporting by offchain tools does not affect the onchain security and functionality of the governance contract. +Timestamp based voting is a recent feature that was formalized in ERC-6372 and ERC-5805, and introduced in v4.9. At the time this feature is released, some governance tooling may not support it yet. Users can expect invalid reporting of deadlines & durations if the tool is not able to interpret the ERC6372 clock. This invalid reporting by offchain tools does not affect the onchain security and functionality of the governance contract. Governors with timestamp support (v4.9 and above) are compatible with old tokens (before v4.9) and will operate in "block number" mode (which is the mode all old tokens operate on). On the other hand, old Governor instances (before v4.9) are not compatible with new tokens operating using timestamps. If you update your token code to use timestamps, make sure to also update your Governor code. From e70a0118ef10773457f670671baefad2c5ea610d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 21 Dec 2023 09:08:59 -0600 Subject: [PATCH 41/44] Remove changesets already included in CHANGELOG (#4805) --- .changeset/dull-ghosts-sip.md | 6 ------ .changeset/grumpy-poets-rush.md | 5 ----- .changeset/purple-squids-attend.md | 6 ------ .changeset/rude-weeks-beg.md | 5 ----- .changeset/strong-points-invent.md | 5 ----- .changeset/thirty-drinks-happen.md | 5 ----- 6 files changed, 32 deletions(-) delete mode 100644 .changeset/dull-ghosts-sip.md delete mode 100644 .changeset/grumpy-poets-rush.md delete mode 100644 .changeset/purple-squids-attend.md delete mode 100644 .changeset/rude-weeks-beg.md delete mode 100644 .changeset/strong-points-invent.md delete mode 100644 .changeset/thirty-drinks-happen.md diff --git a/.changeset/dull-ghosts-sip.md b/.changeset/dull-ghosts-sip.md deleted file mode 100644 index 6c362332ef6..00000000000 --- a/.changeset/dull-ghosts-sip.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`AccessManager`, `AccessManaged`, `GovernorTimelockAccess`: Ensure that calldata shorter than 4 bytes is not padded to 4 bytes. -pr: #4624 diff --git a/.changeset/grumpy-poets-rush.md b/.changeset/grumpy-poets-rush.md deleted file mode 100644 index e566a10fecf..00000000000 --- a/.changeset/grumpy-poets-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -Upgradeable Contracts: No longer transpile interfaces, libraries, and stateless contracts. diff --git a/.changeset/purple-squids-attend.md b/.changeset/purple-squids-attend.md deleted file mode 100644 index 7a13c7b93eb..00000000000 --- a/.changeset/purple-squids-attend.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`AccessManager`: Use named return parameters in functions that return multiple values. -pr: #4624 diff --git a/.changeset/rude-weeks-beg.md b/.changeset/rude-weeks-beg.md deleted file mode 100644 index 77fe423c64f..00000000000 --- a/.changeset/rude-weeks-beg.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`ERC2771Context` and `Context`: Introduce a `_contextPrefixLength()` getter, used to trim extra information appended to `msg.data`. diff --git a/.changeset/strong-points-invent.md b/.changeset/strong-points-invent.md deleted file mode 100644 index 980000c4245..00000000000 --- a/.changeset/strong-points-invent.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`Multicall`: Make aware of non-canonical context (i.e. `msg.sender` is not `_msgSender()`), allowing compatibility with `ERC2771Context`. diff --git a/.changeset/thirty-drinks-happen.md b/.changeset/thirty-drinks-happen.md deleted file mode 100644 index 85be9732ef4..00000000000 --- a/.changeset/thirty-drinks-happen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -`AccessManager`: Make `schedule` and `execute` more conservative when delay is 0. From be0572a8dc80dd7d2766c2a15f9eb5436ed3a445 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 21 Dec 2023 22:57:39 +0100 Subject: [PATCH 42/44] Migrate ERC1155 tests to ethers v6 (#4771) Co-authored-by: ernestognw --- .../generate/templates/EnumerableMap.opts.js | 5 +- .../generate/templates/EnumerableSet.opts.js | 4 +- test/access/Ownable.test.js | 2 +- test/access/manager/AccessManaged.test.js | 2 +- test/helpers/enums.js | 1 + test/token/ERC1155/ERC1155.behavior.js | 1119 ++++++++--------- test/token/ERC1155/ERC1155.test.js | 231 ++-- .../extensions/ERC1155Burnable.test.js | 67 +- .../extensions/ERC1155Pausable.test.js | 110 +- .../ERC1155/extensions/ERC1155Supply.test.js | 115 +- .../extensions/ERC1155URIStorage.test.js | 70 +- .../token/ERC1155/utils/ERC1155Holder.test.js | 82 +- test/token/ERC721/ERC721.behavior.js | 4 +- 13 files changed, 832 insertions(+), 980 deletions(-) diff --git a/scripts/generate/templates/EnumerableMap.opts.js b/scripts/generate/templates/EnumerableMap.opts.js index 699fa7b140e..7fef393a2b0 100644 --- a/scripts/generate/templates/EnumerableMap.opts.js +++ b/scripts/generate/templates/EnumerableMap.opts.js @@ -1,4 +1,7 @@ -const mapType = str => (str == 'uint256' ? 'Uint' : `${str.charAt(0).toUpperCase()}${str.slice(1)}`); +const { capitalize } = require('../../helpers'); + +const mapType = str => (str == 'uint256' ? 'Uint' : capitalize(str)); + const formatType = (keyType, valueType) => ({ name: `${mapType(keyType)}To${mapType(valueType)}Map`, keyType, diff --git a/scripts/generate/templates/EnumerableSet.opts.js b/scripts/generate/templates/EnumerableSet.opts.js index fb53724fe8f..739f0acdfe4 100644 --- a/scripts/generate/templates/EnumerableSet.opts.js +++ b/scripts/generate/templates/EnumerableSet.opts.js @@ -1,4 +1,6 @@ -const mapType = str => (str == 'uint256' ? 'Uint' : `${str.charAt(0).toUpperCase()}${str.slice(1)}`); +const { capitalize } = require('../../helpers'); + +const mapType = str => (str == 'uint256' ? 'Uint' : capitalize(str)); const formatType = type => ({ name: `${mapType(type)}Set`, diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index 568d52b6850..d565fc382ae 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -14,7 +14,7 @@ describe('Ownable', function () { }); it('emits ownership transfer events during construction', async function () { - await expect(await this.ownable.deploymentTransaction()) + await expect(this.ownable.deploymentTransaction()) .to.emit(this.ownable, 'OwnershipTransferred') .withArgs(ethers.ZeroAddress, this.owner.address); }); diff --git a/test/access/manager/AccessManaged.test.js b/test/access/manager/AccessManaged.test.js index f3a433ebd23..b468128ff6c 100644 --- a/test/access/manager/AccessManaged.test.js +++ b/test/access/manager/AccessManaged.test.js @@ -32,7 +32,7 @@ describe('AccessManaged', function () { }); it('sets authority and emits AuthorityUpdated event during construction', async function () { - await expect(await this.managed.deploymentTransaction()) + await expect(this.managed.deploymentTransaction()) .to.emit(this.managed, 'AuthorityUpdated') .withArgs(this.authority.target); }); diff --git a/test/helpers/enums.js b/test/helpers/enums.js index b75e73ba8dc..9a5d6b2632b 100644 --- a/test/helpers/enums.js +++ b/test/helpers/enums.js @@ -14,6 +14,7 @@ function createExport(Enum) { VoteType: Enum('Against', 'For', 'Abstain'), Rounding: Enum('Floor', 'Ceil', 'Trunc', 'Expand'), OperationState: Enum('Unset', 'Waiting', 'Ready', 'Done'), + RevertType: Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'), }; } diff --git a/test/token/ERC1155/ERC1155.behavior.js b/test/token/ERC1155/ERC1155.behavior.js index 9f76fae2948..4c81ea9d1ed 100644 --- a/test/token/ERC1155/ERC1155.behavior.js +++ b/test/token/ERC1155/ERC1155.behavior.js @@ -1,897 +1,798 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { ZERO_ADDRESS } = constants; - +const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); +const { + bigint: { RevertType }, +} = require('../../helpers/enums'); const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); -const { expectRevertCustomError } = require('../../helpers/customError'); -const { Enum } = require('../../helpers/enums'); - -const ERC1155ReceiverMock = artifacts.require('ERC1155ReceiverMock'); -const RevertType = Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'); -function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, multiTokenHolder, recipient, proxy]) { - const firstTokenId = new BN(1); - const secondTokenId = new BN(2); - const unknownTokenId = new BN(3); +function shouldBehaveLikeERC1155() { + const firstTokenId = 1n; + const secondTokenId = 2n; + const unknownTokenId = 3n; - const firstTokenValue = new BN(1000); - const secondTokenValue = new BN(2000); + const firstTokenValue = 1000n; + const secondTokenValue = 2000n; const RECEIVER_SINGLE_MAGIC_VALUE = '0xf23a6e61'; const RECEIVER_BATCH_MAGIC_VALUE = '0xbc197c81'; + beforeEach(async function () { + [this.recipient, this.proxy, this.alice, this.bruce] = this.otherAccounts; + }); + describe('like an ERC1155', function () { describe('balanceOf', function () { it('should return 0 when queried about the zero address', async function () { - expect(await this.token.balanceOf(ZERO_ADDRESS, firstTokenId)).to.be.bignumber.equal('0'); + expect(await this.token.balanceOf(ethers.ZeroAddress, firstTokenId)).to.equal(0n); }); - context("when accounts don't own tokens", function () { + describe("when accounts don't own tokens", function () { it('returns zero for given addresses', async function () { - expect(await this.token.balanceOf(firstTokenHolder, firstTokenId)).to.be.bignumber.equal('0'); - - expect(await this.token.balanceOf(secondTokenHolder, secondTokenId)).to.be.bignumber.equal('0'); - - expect(await this.token.balanceOf(firstTokenHolder, unknownTokenId)).to.be.bignumber.equal('0'); + expect(await this.token.balanceOf(this.alice, firstTokenId)).to.equal(0n); + expect(await this.token.balanceOf(this.bruce, secondTokenId)).to.equal(0n); + expect(await this.token.balanceOf(this.alice, unknownTokenId)).to.equal(0n); }); }); - context('when accounts own some tokens', function () { + describe('when accounts own some tokens', function () { beforeEach(async function () { - await this.token.$_mint(firstTokenHolder, firstTokenId, firstTokenValue, '0x', { - from: minter, - }); - await this.token.$_mint(secondTokenHolder, secondTokenId, secondTokenValue, '0x', { - from: minter, - }); + await this.token.$_mint(this.alice, firstTokenId, firstTokenValue, '0x'); + await this.token.$_mint(this.bruce, secondTokenId, secondTokenValue, '0x'); }); it('returns the amount of tokens owned by the given addresses', async function () { - expect(await this.token.balanceOf(firstTokenHolder, firstTokenId)).to.be.bignumber.equal(firstTokenValue); - - expect(await this.token.balanceOf(secondTokenHolder, secondTokenId)).to.be.bignumber.equal(secondTokenValue); - - expect(await this.token.balanceOf(firstTokenHolder, unknownTokenId)).to.be.bignumber.equal('0'); + expect(await this.token.balanceOf(this.alice, firstTokenId)).to.equal(firstTokenValue); + expect(await this.token.balanceOf(this.bruce, secondTokenId)).to.equal(secondTokenValue); + expect(await this.token.balanceOf(this.alice, unknownTokenId)).to.equal(0n); }); }); }); describe('balanceOfBatch', function () { it("reverts when input arrays don't match up", async function () { - const accounts1 = [firstTokenHolder, secondTokenHolder, firstTokenHolder, secondTokenHolder]; + const accounts1 = [this.alice, this.bruce, this.alice, this.bruce]; const ids1 = [firstTokenId, secondTokenId, unknownTokenId]; - await expectRevertCustomError(this.token.balanceOfBatch(accounts1, ids1), 'ERC1155InvalidArrayLength', [ - accounts1.length, - ids1.length, - ]); - const accounts2 = [firstTokenHolder, secondTokenHolder]; + await expect(this.token.balanceOfBatch(accounts1, ids1)) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidArrayLength') + .withArgs(ids1.length, accounts1.length); + + const accounts2 = [this.alice, this.bruce]; const ids2 = [firstTokenId, secondTokenId, unknownTokenId]; - await expectRevertCustomError(this.token.balanceOfBatch(accounts2, ids2), 'ERC1155InvalidArrayLength', [ - accounts2.length, - ids2.length, - ]); + await expect(this.token.balanceOfBatch(accounts2, ids2)) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidArrayLength') + .withArgs(ids2.length, accounts2.length); }); it('should return 0 as the balance when one of the addresses is the zero address', async function () { const result = await this.token.balanceOfBatch( - [firstTokenHolder, secondTokenHolder, ZERO_ADDRESS], + [this.alice, this.bruce, ethers.ZeroAddress], [firstTokenId, secondTokenId, unknownTokenId], ); - expect(result).to.be.an('array'); - expect(result[0]).to.be.a.bignumber.equal('0'); - expect(result[1]).to.be.a.bignumber.equal('0'); - expect(result[2]).to.be.a.bignumber.equal('0'); + expect(result).to.deep.equal([0n, 0n, 0n]); }); - context("when accounts don't own tokens", function () { + describe("when accounts don't own tokens", function () { it('returns zeros for each account', async function () { const result = await this.token.balanceOfBatch( - [firstTokenHolder, secondTokenHolder, firstTokenHolder], + [this.alice, this.bruce, this.alice], [firstTokenId, secondTokenId, unknownTokenId], ); - expect(result).to.be.an('array'); - expect(result[0]).to.be.a.bignumber.equal('0'); - expect(result[1]).to.be.a.bignumber.equal('0'); - expect(result[2]).to.be.a.bignumber.equal('0'); + expect(result).to.deep.equal([0n, 0n, 0n]); }); }); - context('when accounts own some tokens', function () { + describe('when accounts own some tokens', function () { beforeEach(async function () { - await this.token.$_mint(firstTokenHolder, firstTokenId, firstTokenValue, '0x', { - from: minter, - }); - await this.token.$_mint(secondTokenHolder, secondTokenId, secondTokenValue, '0x', { - from: minter, - }); + await this.token.$_mint(this.alice, firstTokenId, firstTokenValue, '0x'); + await this.token.$_mint(this.bruce, secondTokenId, secondTokenValue, '0x'); }); it('returns amounts owned by each account in order passed', async function () { const result = await this.token.balanceOfBatch( - [secondTokenHolder, firstTokenHolder, firstTokenHolder], + [this.bruce, this.alice, this.alice], [secondTokenId, firstTokenId, unknownTokenId], ); - expect(result).to.be.an('array'); - expect(result[0]).to.be.a.bignumber.equal(secondTokenValue); - expect(result[1]).to.be.a.bignumber.equal(firstTokenValue); - expect(result[2]).to.be.a.bignumber.equal('0'); + expect(result).to.deep.equal([secondTokenValue, firstTokenValue, 0n]); }); it('returns multiple times the balance of the same address when asked', async function () { const result = await this.token.balanceOfBatch( - [firstTokenHolder, secondTokenHolder, firstTokenHolder], + [this.alice, this.bruce, this.alice], [firstTokenId, secondTokenId, firstTokenId], ); - expect(result).to.be.an('array'); - expect(result[0]).to.be.a.bignumber.equal(result[2]); - expect(result[0]).to.be.a.bignumber.equal(firstTokenValue); - expect(result[1]).to.be.a.bignumber.equal(secondTokenValue); - expect(result[2]).to.be.a.bignumber.equal(firstTokenValue); + expect(result).to.deep.equal([firstTokenValue, secondTokenValue, firstTokenValue]); }); }); }); describe('setApprovalForAll', function () { - let receipt; beforeEach(async function () { - receipt = await this.token.setApprovalForAll(proxy, true, { from: multiTokenHolder }); + this.tx = await this.token.connect(this.holder).setApprovalForAll(this.proxy, true); }); it('sets approval status which can be queried via isApprovedForAll', async function () { - expect(await this.token.isApprovedForAll(multiTokenHolder, proxy)).to.be.equal(true); + expect(await this.token.isApprovedForAll(this.holder, this.proxy)).to.be.true; }); - it('emits an ApprovalForAll log', function () { - expectEvent(receipt, 'ApprovalForAll', { account: multiTokenHolder, operator: proxy, approved: true }); + it('emits an ApprovalForAll log', async function () { + await expect(this.tx) + .to.emit(this.token, 'ApprovalForAll') + .withArgs(this.holder.address, this.proxy.address, true); }); it('can unset approval for an operator', async function () { - await this.token.setApprovalForAll(proxy, false, { from: multiTokenHolder }); - expect(await this.token.isApprovedForAll(multiTokenHolder, proxy)).to.be.equal(false); + await this.token.connect(this.holder).setApprovalForAll(this.proxy, false); + expect(await this.token.isApprovedForAll(this.holder, this.proxy)).to.be.false; }); it('reverts if attempting to approve zero address as an operator', async function () { - await expectRevertCustomError( - this.token.setApprovalForAll(constants.ZERO_ADDRESS, true, { from: multiTokenHolder }), - 'ERC1155InvalidOperator', - [constants.ZERO_ADDRESS], - ); + await expect(this.token.connect(this.holder).setApprovalForAll(ethers.ZeroAddress, true)) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidOperator') + .withArgs(ethers.ZeroAddress); }); }); describe('safeTransferFrom', function () { beforeEach(async function () { - await this.token.$_mint(multiTokenHolder, firstTokenId, firstTokenValue, '0x', { - from: minter, - }); - await this.token.$_mint(multiTokenHolder, secondTokenId, secondTokenValue, '0x', { - from: minter, - }); + await this.token.$_mint(this.holder, firstTokenId, firstTokenValue, '0x'); + await this.token.$_mint(this.holder, secondTokenId, secondTokenValue, '0x'); }); it('reverts when transferring more than balance', async function () { - await expectRevertCustomError( - this.token.safeTransferFrom(multiTokenHolder, recipient, firstTokenId, firstTokenValue.addn(1), '0x', { - from: multiTokenHolder, - }), - 'ERC1155InsufficientBalance', - [multiTokenHolder, firstTokenValue, firstTokenValue.addn(1), firstTokenId], - ); + await expect( + this.token + .connect(this.holder) + .safeTransferFrom(this.holder, this.recipient, firstTokenId, firstTokenValue + 1n, '0x'), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1155InsufficientBalance') + .withArgs(this.holder.address, firstTokenValue, firstTokenValue + 1n, firstTokenId); }); it('reverts when transferring to zero address', async function () { - await expectRevertCustomError( - this.token.safeTransferFrom(multiTokenHolder, ZERO_ADDRESS, firstTokenId, firstTokenValue, '0x', { - from: multiTokenHolder, - }), - 'ERC1155InvalidReceiver', - [ZERO_ADDRESS], - ); + await expect( + this.token + .connect(this.holder) + .safeTransferFrom(this.holder, ethers.ZeroAddress, firstTokenId, firstTokenValue, '0x'), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver') + .withArgs(ethers.ZeroAddress); }); - function transferWasSuccessful({ operator, from, id, value }) { + function transferWasSuccessful() { it('debits transferred balance from sender', async function () { - const newBalance = await this.token.balanceOf(from, id); - expect(newBalance).to.be.a.bignumber.equal('0'); + expect(await this.token.balanceOf(this.args.from, this.args.id)).to.equal(0n); }); it('credits transferred balance to receiver', async function () { - const newBalance = await this.token.balanceOf(this.toWhom, id); - expect(newBalance).to.be.a.bignumber.equal(value); - }); - - it('emits a TransferSingle log', function () { - expectEvent(this.transferLogs, 'TransferSingle', { - operator, - from, - to: this.toWhom, - id, - value, - }); + expect(await this.token.balanceOf(this.args.to, this.args.id)).to.equal(this.args.value); + }); + + it('emits a TransferSingle log', async function () { + await expect(this.tx) + .to.emit(this.token, 'TransferSingle') + .withArgs( + this.args.operator.address ?? this.args.operator.target ?? this.args.operator, + this.args.from.address ?? this.args.from.target ?? this.args.from, + this.args.to.address ?? this.args.to.target ?? this.args.to, + this.args.id, + this.args.value, + ); }); } - context('when called by the multiTokenHolder', async function () { + describe('when called by the holder', async function () { beforeEach(async function () { - this.toWhom = recipient; - this.transferLogs = await this.token.safeTransferFrom( - multiTokenHolder, - recipient, - firstTokenId, - firstTokenValue, - '0x', - { - from: multiTokenHolder, - }, - ); - }); - - transferWasSuccessful.call(this, { - operator: multiTokenHolder, - from: multiTokenHolder, - id: firstTokenId, - value: firstTokenValue, + this.args = { + operator: this.holder, + from: this.holder, + to: this.recipient, + id: firstTokenId, + value: firstTokenValue, + data: '0x', + }; + this.tx = await this.token + .connect(this.args.operator) + .safeTransferFrom(this.args.from, this.args.to, this.args.id, this.args.value, this.args.data); }); - it('preserves existing balances which are not transferred by multiTokenHolder', async function () { - const balance1 = await this.token.balanceOf(multiTokenHolder, secondTokenId); - expect(balance1).to.be.a.bignumber.equal(secondTokenValue); + transferWasSuccessful(); - const balance2 = await this.token.balanceOf(recipient, secondTokenId); - expect(balance2).to.be.a.bignumber.equal('0'); + it('preserves existing balances which are not transferred by holder', async function () { + expect(await this.token.balanceOf(this.holder, secondTokenId)).to.equal(secondTokenValue); + expect(await this.token.balanceOf(this.recipient, secondTokenId)).to.equal(0n); }); }); - context('when called by an operator on behalf of the multiTokenHolder', function () { - context('when operator is not approved by multiTokenHolder', function () { + describe('when called by an operator on behalf of the holder', function () { + describe('when operator is not approved by holder', function () { beforeEach(async function () { - await this.token.setApprovalForAll(proxy, false, { from: multiTokenHolder }); + await this.token.connect(this.holder).setApprovalForAll(this.proxy, false); }); it('reverts', async function () { - await expectRevertCustomError( - this.token.safeTransferFrom(multiTokenHolder, recipient, firstTokenId, firstTokenValue, '0x', { - from: proxy, - }), - 'ERC1155MissingApprovalForAll', - [proxy, multiTokenHolder], - ); + await expect( + this.token + .connect(this.proxy) + .safeTransferFrom(this.holder, this.recipient, firstTokenId, firstTokenValue, '0x'), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1155MissingApprovalForAll') + .withArgs(this.proxy.address, this.holder.address); }); }); - context('when operator is approved by multiTokenHolder', function () { + describe('when operator is approved by holder', function () { beforeEach(async function () { - this.toWhom = recipient; - await this.token.setApprovalForAll(proxy, true, { from: multiTokenHolder }); - this.transferLogs = await this.token.safeTransferFrom( - multiTokenHolder, - recipient, - firstTokenId, - firstTokenValue, - '0x', - { - from: proxy, - }, - ); - }); + await this.token.connect(this.holder).setApprovalForAll(this.proxy, true); - transferWasSuccessful.call(this, { - operator: proxy, - from: multiTokenHolder, - id: firstTokenId, - value: firstTokenValue, + this.args = { + operator: this.proxy, + from: this.holder, + to: this.recipient, + id: firstTokenId, + value: firstTokenValue, + data: '0x', + }; + this.tx = await this.token + .connect(this.args.operator) + .safeTransferFrom(this.args.from, this.args.to, this.args.id, this.args.value, this.args.data); }); - it("preserves operator's balances not involved in the transfer", async function () { - const balance1 = await this.token.balanceOf(proxy, firstTokenId); - expect(balance1).to.be.a.bignumber.equal('0'); + transferWasSuccessful(); - const balance2 = await this.token.balanceOf(proxy, secondTokenId); - expect(balance2).to.be.a.bignumber.equal('0'); + it("preserves operator's balances not involved in the transfer", async function () { + expect(await this.token.balanceOf(this.proxy, firstTokenId)).to.equal(0n); + expect(await this.token.balanceOf(this.proxy, secondTokenId)).to.equal(0n); }); }); }); - context('when sending to a valid receiver', function () { + describe('when sending to a valid receiver', function () { beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( + this.receiver = await ethers.deployContract('$ERC1155ReceiverMock', [ RECEIVER_SINGLE_MAGIC_VALUE, RECEIVER_BATCH_MAGIC_VALUE, RevertType.None, - ); + ]); }); - context('without data', function () { + describe('without data', function () { beforeEach(async function () { - this.toWhom = this.receiver.address; - this.transferReceipt = await this.token.safeTransferFrom( - multiTokenHolder, - this.receiver.address, - firstTokenId, - firstTokenValue, - '0x', - { from: multiTokenHolder }, - ); - this.transferLogs = this.transferReceipt; + this.args = { + operator: this.holder, + from: this.holder, + to: this.receiver, + id: firstTokenId, + value: firstTokenValue, + data: '0x', + }; + this.tx = await this.token + .connect(this.args.operator) + .safeTransferFrom(this.args.from, this.args.to, this.args.id, this.args.value, this.args.data); }); - transferWasSuccessful.call(this, { - operator: multiTokenHolder, - from: multiTokenHolder, - id: firstTokenId, - value: firstTokenValue, - }); + transferWasSuccessful(); it('calls onERC1155Received', async function () { - await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'Received', { - operator: multiTokenHolder, - from: multiTokenHolder, - id: firstTokenId, - value: firstTokenValue, - data: null, - }); + await expect(this.tx) + .to.emit(this.receiver, 'Received') + .withArgs( + this.args.operator.address, + this.args.from.address, + this.args.id, + this.args.value, + this.args.data, + anyValue, + ); }); }); - context('with data', function () { - const data = '0xf00dd00d'; + describe('with data', function () { beforeEach(async function () { - this.toWhom = this.receiver.address; - this.transferReceipt = await this.token.safeTransferFrom( - multiTokenHolder, - this.receiver.address, - firstTokenId, - firstTokenValue, - data, - { from: multiTokenHolder }, - ); - this.transferLogs = this.transferReceipt; + this.args = { + operator: this.holder, + from: this.holder, + to: this.receiver, + id: firstTokenId, + value: firstTokenValue, + data: '0xf00dd00d', + }; + this.tx = await this.token + .connect(this.args.operator) + .safeTransferFrom(this.args.from, this.args.to, this.args.id, this.args.value, this.args.data); }); - transferWasSuccessful.call(this, { - operator: multiTokenHolder, - from: multiTokenHolder, - id: firstTokenId, - value: firstTokenValue, - }); + transferWasSuccessful(); it('calls onERC1155Received', async function () { - await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'Received', { - operator: multiTokenHolder, - from: multiTokenHolder, - id: firstTokenId, - value: firstTokenValue, - data, - }); + await expect(this.tx) + .to.emit(this.receiver, 'Received') + .withArgs( + this.args.operator.address, + this.args.from.address, + this.args.id, + this.args.value, + this.args.data, + anyValue, + ); }); }); }); - context('to a receiver contract returning unexpected value', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new('0x00c0ffee', RECEIVER_BATCH_MAGIC_VALUE, RevertType.None); - }); - + describe('to a receiver contract returning unexpected value', function () { it('reverts', async function () { - await expectRevertCustomError( - this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstTokenValue, '0x', { - from: multiTokenHolder, - }), - 'ERC1155InvalidReceiver', - [this.receiver.address], - ); + const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [ + '0x00c0ffee', + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.None, + ]); + + await expect( + this.token + .connect(this.holder) + .safeTransferFrom(this.holder, receiver, firstTokenId, firstTokenValue, '0x'), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver') + .withArgs(receiver.target); }); }); - context('to a receiver contract that reverts', function () { - context('with a revert string', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( + describe('to a receiver contract that reverts', function () { + describe('with a revert string', function () { + it('reverts', async function () { + const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [ RECEIVER_SINGLE_MAGIC_VALUE, RECEIVER_BATCH_MAGIC_VALUE, RevertType.RevertWithMessage, - ); - }); + ]); - it('reverts', async function () { - await expectRevert( - this.token.safeTransferFrom( - multiTokenHolder, - this.receiver.address, - firstTokenId, - firstTokenValue, - '0x', - { - from: multiTokenHolder, - }, - ), - 'ERC1155ReceiverMock: reverting on receive', - ); + await expect( + this.token + .connect(this.holder) + .safeTransferFrom(this.holder, receiver, firstTokenId, firstTokenValue, '0x'), + ).to.be.revertedWith('ERC1155ReceiverMock: reverting on receive'); }); }); - context('without a revert string', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( + describe('without a revert string', function () { + it('reverts', async function () { + const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [ RECEIVER_SINGLE_MAGIC_VALUE, RECEIVER_BATCH_MAGIC_VALUE, RevertType.RevertWithoutMessage, - ); - }); + ]); - it('reverts', async function () { - await expectRevertCustomError( - this.token.safeTransferFrom( - multiTokenHolder, - this.receiver.address, - firstTokenId, - firstTokenValue, - '0x', - { - from: multiTokenHolder, - }, - ), - 'ERC1155InvalidReceiver', - [this.receiver.address], - ); + await expect( + this.token + .connect(this.holder) + .safeTransferFrom(this.holder, receiver, firstTokenId, firstTokenValue, '0x'), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver') + .withArgs(receiver.target); }); }); - context('with a custom error', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( + describe('with a custom error', function () { + it('reverts', async function () { + const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [ RECEIVER_SINGLE_MAGIC_VALUE, RECEIVER_BATCH_MAGIC_VALUE, RevertType.RevertWithCustomError, - ); - }); + ]); - it('reverts', async function () { - await expectRevertCustomError( - this.token.safeTransferFrom( - multiTokenHolder, - this.receiver.address, - firstTokenId, - firstTokenValue, - '0x', - { - from: multiTokenHolder, - }, - ), - 'CustomError', - [RECEIVER_SINGLE_MAGIC_VALUE], - ); + await expect( + this.token + .connect(this.holder) + .safeTransferFrom(this.holder, receiver, firstTokenId, firstTokenValue, '0x'), + ) + .to.be.revertedWithCustomError(receiver, 'CustomError') + .withArgs(RECEIVER_SINGLE_MAGIC_VALUE); }); }); - context('with a panic', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( + describe('with a panic', function () { + it('reverts', async function () { + const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [ RECEIVER_SINGLE_MAGIC_VALUE, RECEIVER_BATCH_MAGIC_VALUE, RevertType.Panic, - ); - }); + ]); - it('reverts', async function () { - await expectRevert.unspecified( - this.token.safeTransferFrom( - multiTokenHolder, - this.receiver.address, - firstTokenId, - firstTokenValue, - '0x', - { - from: multiTokenHolder, - }, - ), - ); + await expect( + this.token + .connect(this.holder) + .safeTransferFrom(this.holder, receiver, firstTokenId, firstTokenValue, '0x'), + ).to.be.revertedWithPanic(); }); }); }); - context('to a contract that does not implement the required function', function () { + describe('to a contract that does not implement the required function', function () { it('reverts', async function () { - const invalidReceiver = this.token; - await expectRevert.unspecified( - this.token.safeTransferFrom( - multiTokenHolder, - invalidReceiver.address, - firstTokenId, - firstTokenValue, - '0x', - { - from: multiTokenHolder, - }, - ), - ); + const invalidReceiver = this.token.target; + + await expect( + this.token + .connect(this.holder) + .safeTransferFrom(this.holder, invalidReceiver, firstTokenId, firstTokenValue, '0x'), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver') + .withArgs(invalidReceiver); }); }); }); describe('safeBatchTransferFrom', function () { beforeEach(async function () { - await this.token.$_mint(multiTokenHolder, firstTokenId, firstTokenValue, '0x', { - from: minter, - }); - await this.token.$_mint(multiTokenHolder, secondTokenId, secondTokenValue, '0x', { - from: minter, - }); + await this.token.$_mint(this.holder, firstTokenId, firstTokenValue, '0x'); + await this.token.$_mint(this.holder, secondTokenId, secondTokenValue, '0x'); }); it('reverts when transferring value more than any of balances', async function () { - await expectRevertCustomError( - this.token.safeBatchTransferFrom( - multiTokenHolder, - recipient, - [firstTokenId, secondTokenId], - [firstTokenValue, secondTokenValue.addn(1)], - '0x', - { from: multiTokenHolder }, - ), - 'ERC1155InsufficientBalance', - [multiTokenHolder, secondTokenValue, secondTokenValue.addn(1), secondTokenId], - ); + await expect( + this.token + .connect(this.holder) + .safeBatchTransferFrom( + this.holder, + this.recipient, + [firstTokenId, secondTokenId], + [firstTokenValue, secondTokenValue + 1n], + '0x', + ), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1155InsufficientBalance') + .withArgs(this.holder.address, secondTokenValue, secondTokenValue + 1n, secondTokenId); }); it("reverts when ids array length doesn't match values array length", async function () { const ids1 = [firstTokenId]; const tokenValues1 = [firstTokenValue, secondTokenValue]; - await expectRevertCustomError( - this.token.safeBatchTransferFrom(multiTokenHolder, recipient, ids1, tokenValues1, '0x', { - from: multiTokenHolder, - }), - 'ERC1155InvalidArrayLength', - [ids1.length, tokenValues1.length], - ); + await expect( + this.token.connect(this.holder).safeBatchTransferFrom(this.holder, this.recipient, ids1, tokenValues1, '0x'), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidArrayLength') + .withArgs(ids1.length, tokenValues1.length); const ids2 = [firstTokenId, secondTokenId]; const tokenValues2 = [firstTokenValue]; - await expectRevertCustomError( - this.token.safeBatchTransferFrom(multiTokenHolder, recipient, ids2, tokenValues2, '0x', { - from: multiTokenHolder, - }), - 'ERC1155InvalidArrayLength', - [ids2.length, tokenValues2.length], - ); + + await expect( + this.token.connect(this.holder).safeBatchTransferFrom(this.holder, this.recipient, ids2, tokenValues2, '0x'), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidArrayLength') + .withArgs(ids2.length, tokenValues2.length); }); it('reverts when transferring to zero address', async function () { - await expectRevertCustomError( - this.token.safeBatchTransferFrom( - multiTokenHolder, - ZERO_ADDRESS, - [firstTokenId, secondTokenId], - [firstTokenValue, secondTokenValue], - '0x', - { from: multiTokenHolder }, - ), - 'ERC1155InvalidReceiver', - [ZERO_ADDRESS], - ); + await expect( + this.token + .connect(this.holder) + .safeBatchTransferFrom( + this.holder, + ethers.ZeroAddress, + [firstTokenId, secondTokenId], + [firstTokenValue, secondTokenValue], + '0x', + ), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver') + .withArgs(ethers.ZeroAddress); }); it('reverts when transferring from zero address', async function () { - await expectRevertCustomError( - this.token.$_safeBatchTransferFrom(ZERO_ADDRESS, multiTokenHolder, [firstTokenId], [firstTokenValue], '0x'), - 'ERC1155InvalidSender', - [ZERO_ADDRESS], - ); + await expect( + this.token.$_safeBatchTransferFrom(ethers.ZeroAddress, this.holder, [firstTokenId], [firstTokenValue], '0x'), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidSender') + .withArgs(ethers.ZeroAddress); }); - function batchTransferWasSuccessful({ operator, from, ids, values }) { + function batchTransferWasSuccessful() { it('debits transferred balances from sender', async function () { - const newBalances = await this.token.balanceOfBatch(new Array(ids.length).fill(from), ids); - for (const newBalance of newBalances) { - expect(newBalance).to.be.a.bignumber.equal('0'); - } + const newBalances = await this.token.balanceOfBatch( + this.args.ids.map(() => this.args.from), + this.args.ids, + ); + expect(newBalances).to.deep.equal(this.args.ids.map(() => 0n)); }); it('credits transferred balances to receiver', async function () { - const newBalances = await this.token.balanceOfBatch(new Array(ids.length).fill(this.toWhom), ids); - for (let i = 0; i < newBalances.length; i++) { - expect(newBalances[i]).to.be.a.bignumber.equal(values[i]); - } - }); - - it('emits a TransferBatch log', function () { - expectEvent(this.transferLogs, 'TransferBatch', { - operator, - from, - to: this.toWhom, - // ids, - // values, - }); + const newBalances = await this.token.balanceOfBatch( + this.args.ids.map(() => this.args.to), + this.args.ids, + ); + expect(newBalances).to.deep.equal(this.args.values); + }); + + it('emits a TransferBatch log', async function () { + await expect(this.tx) + .to.emit(this.token, 'TransferBatch') + .withArgs( + this.args.operator.address ?? this.args.operator.target ?? this.args.operator, + this.args.from.address ?? this.args.from.target ?? this.args.from, + this.args.to.address ?? this.args.to.target ?? this.args.to, + this.args.ids, + this.args.values, + ); }); } - context('when called by the multiTokenHolder', async function () { + describe('when called by the holder', async function () { beforeEach(async function () { - this.toWhom = recipient; - this.transferLogs = await this.token.safeBatchTransferFrom( - multiTokenHolder, - recipient, - [firstTokenId, secondTokenId], - [firstTokenValue, secondTokenValue], - '0x', - { from: multiTokenHolder }, - ); + this.args = { + operator: this.holder, + from: this.holder, + to: this.recipient, + ids: [firstTokenId, secondTokenId], + values: [firstTokenValue, secondTokenValue], + data: '0x', + }; + this.tx = await this.token + .connect(this.args.operator) + .safeBatchTransferFrom(this.args.from, this.args.to, this.args.ids, this.args.values, this.args.data); }); - batchTransferWasSuccessful.call(this, { - operator: multiTokenHolder, - from: multiTokenHolder, - ids: [firstTokenId, secondTokenId], - values: [firstTokenValue, secondTokenValue], - }); + batchTransferWasSuccessful(); }); - context('when called by an operator on behalf of the multiTokenHolder', function () { - context('when operator is not approved by multiTokenHolder', function () { + describe('when called by an operator on behalf of the holder', function () { + describe('when operator is not approved by holder', function () { beforeEach(async function () { - await this.token.setApprovalForAll(proxy, false, { from: multiTokenHolder }); + await this.token.connect(this.holder).setApprovalForAll(this.proxy, false); }); it('reverts', async function () { - await expectRevertCustomError( - this.token.safeBatchTransferFrom( - multiTokenHolder, - recipient, - [firstTokenId, secondTokenId], - [firstTokenValue, secondTokenValue], - '0x', - { from: proxy }, - ), - 'ERC1155MissingApprovalForAll', - [proxy, multiTokenHolder], - ); - }); - }); - - context('when operator is approved by multiTokenHolder', function () { + await expect( + this.token + .connect(this.proxy) + .safeBatchTransferFrom( + this.holder, + this.recipient, + [firstTokenId, secondTokenId], + [firstTokenValue, secondTokenValue], + '0x', + ), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1155MissingApprovalForAll') + .withArgs(this.proxy.address, this.holder.address); + }); + }); + + describe('when operator is approved by holder', function () { beforeEach(async function () { - this.toWhom = recipient; - await this.token.setApprovalForAll(proxy, true, { from: multiTokenHolder }); - this.transferLogs = await this.token.safeBatchTransferFrom( - multiTokenHolder, - recipient, - [firstTokenId, secondTokenId], - [firstTokenValue, secondTokenValue], - '0x', - { from: proxy }, - ); - }); + await this.token.connect(this.holder).setApprovalForAll(this.proxy, true); - batchTransferWasSuccessful.call(this, { - operator: proxy, - from: multiTokenHolder, - ids: [firstTokenId, secondTokenId], - values: [firstTokenValue, secondTokenValue], + this.args = { + operator: this.proxy, + from: this.holder, + to: this.recipient, + ids: [firstTokenId, secondTokenId], + values: [firstTokenValue, secondTokenValue], + data: '0x', + }; + this.tx = await this.token + .connect(this.args.operator) + .safeBatchTransferFrom(this.args.from, this.args.to, this.args.ids, this.args.values, this.args.data); }); + batchTransferWasSuccessful(); + it("preserves operator's balances not involved in the transfer", async function () { - const balance1 = await this.token.balanceOf(proxy, firstTokenId); - expect(balance1).to.be.a.bignumber.equal('0'); - const balance2 = await this.token.balanceOf(proxy, secondTokenId); - expect(balance2).to.be.a.bignumber.equal('0'); + expect(await this.token.balanceOf(this.proxy, firstTokenId)).to.equal(0n); + expect(await this.token.balanceOf(this.proxy, secondTokenId)).to.equal(0n); }); }); }); - context('when sending to a valid receiver', function () { + describe('when sending to a valid receiver', function () { beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( + this.receiver = await ethers.deployContract('$ERC1155ReceiverMock', [ RECEIVER_SINGLE_MAGIC_VALUE, RECEIVER_BATCH_MAGIC_VALUE, RevertType.None, - ); + ]); }); - context('without data', function () { + describe('without data', function () { beforeEach(async function () { - this.toWhom = this.receiver.address; - this.transferReceipt = await this.token.safeBatchTransferFrom( - multiTokenHolder, - this.receiver.address, - [firstTokenId, secondTokenId], - [firstTokenValue, secondTokenValue], - '0x', - { from: multiTokenHolder }, - ); - this.transferLogs = this.transferReceipt; + this.args = { + operator: this.holder, + from: this.holder, + to: this.receiver, + ids: [firstTokenId, secondTokenId], + values: [firstTokenValue, secondTokenValue], + data: '0x', + }; + this.tx = await this.token + .connect(this.args.operator) + .safeBatchTransferFrom(this.args.from, this.args.to, this.args.ids, this.args.values, this.args.data); }); - batchTransferWasSuccessful.call(this, { - operator: multiTokenHolder, - from: multiTokenHolder, - ids: [firstTokenId, secondTokenId], - values: [firstTokenValue, secondTokenValue], - }); + batchTransferWasSuccessful(); it('calls onERC1155BatchReceived', async function () { - await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'BatchReceived', { - operator: multiTokenHolder, - from: multiTokenHolder, - // ids: [firstTokenId, secondTokenId], - // values: [firstTokenValue, secondTokenValue], - data: null, - }); + await expect(this.tx) + .to.emit(this.receiver, 'BatchReceived') + .withArgs( + this.holder.address, + this.holder.address, + this.args.ids, + this.args.values, + this.args.data, + anyValue, + ); }); }); - context('with data', function () { - const data = '0xf00dd00d'; + describe('with data', function () { beforeEach(async function () { - this.toWhom = this.receiver.address; - this.transferReceipt = await this.token.safeBatchTransferFrom( - multiTokenHolder, - this.receiver.address, - [firstTokenId, secondTokenId], - [firstTokenValue, secondTokenValue], - data, - { from: multiTokenHolder }, - ); - this.transferLogs = this.transferReceipt; + this.args = { + operator: this.holder, + from: this.holder, + to: this.receiver, + ids: [firstTokenId, secondTokenId], + values: [firstTokenValue, secondTokenValue], + data: '0xf00dd00d', + }; + this.tx = await this.token + .connect(this.args.operator) + .safeBatchTransferFrom(this.args.from, this.args.to, this.args.ids, this.args.values, this.args.data); }); - batchTransferWasSuccessful.call(this, { - operator: multiTokenHolder, - from: multiTokenHolder, - ids: [firstTokenId, secondTokenId], - values: [firstTokenValue, secondTokenValue], - }); + batchTransferWasSuccessful(); it('calls onERC1155Received', async function () { - await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'BatchReceived', { - operator: multiTokenHolder, - from: multiTokenHolder, - // ids: [firstTokenId, secondTokenId], - // values: [firstTokenValue, secondTokenValue], - data, - }); + await expect(this.tx) + .to.emit(this.receiver, 'BatchReceived') + .withArgs( + this.holder.address, + this.holder.address, + this.args.ids, + this.args.values, + this.args.data, + anyValue, + ); }); }); }); - context('to a receiver contract returning unexpected value', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( + describe('to a receiver contract returning unexpected value', function () { + it('reverts', async function () { + const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [ RECEIVER_SINGLE_MAGIC_VALUE, RECEIVER_SINGLE_MAGIC_VALUE, RevertType.None, - ); - }); - - it('reverts', async function () { - await expectRevertCustomError( - this.token.safeBatchTransferFrom( - multiTokenHolder, - this.receiver.address, - [firstTokenId, secondTokenId], - [firstTokenValue, secondTokenValue], - '0x', - { from: multiTokenHolder }, - ), - 'ERC1155InvalidReceiver', - [this.receiver.address], - ); + ]); + + await expect( + this.token + .connect(this.holder) + .safeBatchTransferFrom( + this.holder, + receiver, + [firstTokenId, secondTokenId], + [firstTokenValue, secondTokenValue], + '0x', + ), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver') + .withArgs(receiver.target); }); }); - context('to a receiver contract that reverts', function () { - context('with a revert string', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( + describe('to a receiver contract that reverts', function () { + describe('with a revert string', function () { + it('reverts', async function () { + const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [ RECEIVER_SINGLE_MAGIC_VALUE, RECEIVER_BATCH_MAGIC_VALUE, RevertType.RevertWithMessage, - ); - }); + ]); - it('reverts', async function () { - await expectRevert( - this.token.safeBatchTransferFrom( - multiTokenHolder, - this.receiver.address, - [firstTokenId, secondTokenId], - [firstTokenValue, secondTokenValue], - '0x', - { from: multiTokenHolder }, - ), - 'ERC1155ReceiverMock: reverting on batch receive', - ); + await expect( + this.token + .connect(this.holder) + .safeBatchTransferFrom( + this.holder, + receiver, + [firstTokenId, secondTokenId], + [firstTokenValue, secondTokenValue], + '0x', + ), + ).to.be.revertedWith('ERC1155ReceiverMock: reverting on batch receive'); }); }); - context('without a revert string', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( + describe('without a revert string', function () { + it('reverts', async function () { + const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [ RECEIVER_SINGLE_MAGIC_VALUE, RECEIVER_BATCH_MAGIC_VALUE, RevertType.RevertWithoutMessage, - ); - }); + ]); - it('reverts', async function () { - await expectRevertCustomError( - this.token.safeBatchTransferFrom( - multiTokenHolder, - this.receiver.address, - [firstTokenId, secondTokenId], - [firstTokenValue, secondTokenValue], - '0x', - { from: multiTokenHolder }, - ), - 'ERC1155InvalidReceiver', - [this.receiver.address], - ); + await expect( + this.token + .connect(this.holder) + .safeBatchTransferFrom( + this.holder, + receiver, + [firstTokenId, secondTokenId], + [firstTokenValue, secondTokenValue], + '0x', + ), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver') + .withArgs(receiver.target); }); }); - context('with a custom error', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( + describe('with a custom error', function () { + it('reverts', async function () { + const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [ RECEIVER_SINGLE_MAGIC_VALUE, RECEIVER_BATCH_MAGIC_VALUE, RevertType.RevertWithCustomError, - ); - }); + ]); - it('reverts', async function () { - await expectRevertCustomError( - this.token.safeBatchTransferFrom( - multiTokenHolder, - this.receiver.address, - [firstTokenId, secondTokenId], - [firstTokenValue, secondTokenValue], - '0x', - { from: multiTokenHolder }, - ), - 'CustomError', - [RECEIVER_SINGLE_MAGIC_VALUE], - ); + await expect( + this.token + .connect(this.holder) + .safeBatchTransferFrom( + this.holder, + receiver, + [firstTokenId, secondTokenId], + [firstTokenValue, secondTokenValue], + '0x', + ), + ) + .to.be.revertedWithCustomError(receiver, 'CustomError') + .withArgs(RECEIVER_SINGLE_MAGIC_VALUE); }); }); - context('with a panic', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( + describe('with a panic', function () { + it('reverts', async function () { + const receiver = await ethers.deployContract('$ERC1155ReceiverMock', [ RECEIVER_SINGLE_MAGIC_VALUE, RECEIVER_BATCH_MAGIC_VALUE, RevertType.Panic, - ); - }); + ]); - it('reverts', async function () { - await expectRevert.unspecified( - this.token.safeBatchTransferFrom( - multiTokenHolder, - this.receiver.address, - [firstTokenId, secondTokenId], - [firstTokenValue, secondTokenValue], - '0x', - { from: multiTokenHolder }, - ), - ); + await expect( + this.token + .connect(this.holder) + .safeBatchTransferFrom( + this.holder, + receiver, + [firstTokenId, secondTokenId], + [firstTokenValue, secondTokenValue], + '0x', + ), + ).to.be.revertedWithPanic(); }); }); }); - context('to a contract that does not implement the required function', function () { + describe('to a contract that does not implement the required function', function () { it('reverts', async function () { - const invalidReceiver = this.token; - await expectRevert.unspecified( - this.token.safeBatchTransferFrom( - multiTokenHolder, - invalidReceiver.address, - [firstTokenId, secondTokenId], - [firstTokenValue, secondTokenValue], - '0x', - { from: multiTokenHolder }, - ), - ); + const invalidReceiver = this.token.target; + + await expect( + this.token + .connect(this.holder) + .safeBatchTransferFrom( + this.holder, + invalidReceiver, + [firstTokenId, secondTokenId], + [firstTokenValue, secondTokenValue], + '0x', + ), + ) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver') + .withArgs(invalidReceiver); }); }); }); diff --git a/test/token/ERC1155/ERC1155.test.js b/test/token/ERC1155/ERC1155.test.js index 58d747a4b1e..c469dd84584 100644 --- a/test/token/ERC1155/ERC1155.test.js +++ b/test/token/ERC1155/ERC1155.test.js @@ -1,251 +1,212 @@ -const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS } = constants; - +const { ethers } = require('hardhat'); const { expect } = require('chai'); - -const { expectRevertCustomError } = require('../../helpers/customError'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { zip } = require('../../helpers/iterate'); const { shouldBehaveLikeERC1155 } = require('./ERC1155.behavior'); -const ERC1155Mock = artifacts.require('$ERC1155'); -contract('ERC1155', function (accounts) { - const [operator, tokenHolder, tokenBatchHolder, ...otherAccounts] = accounts; +const initialURI = 'https://token-cdn-domain/{id}.json'; - const initialURI = 'https://token-cdn-domain/{id}.json'; +async function fixture() { + const [operator, holder, ...otherAccounts] = await ethers.getSigners(); + const token = await ethers.deployContract('$ERC1155', [initialURI]); + return { token, operator, holder, otherAccounts }; +} +describe('ERC1155', function () { beforeEach(async function () { - this.token = await ERC1155Mock.new(initialURI); + Object.assign(this, await loadFixture(fixture)); }); - shouldBehaveLikeERC1155(otherAccounts); + shouldBehaveLikeERC1155(); describe('internal functions', function () { - const tokenId = new BN(1990); - const mintValue = new BN(9001); - const burnValue = new BN(3000); + const tokenId = 1990n; + const mintValue = 9001n; + const burnValue = 3000n; - const tokenBatchIds = [new BN(2000), new BN(2010), new BN(2020)]; - const mintValues = [new BN(5000), new BN(10000), new BN(42195)]; - const burnValues = [new BN(5000), new BN(9001), new BN(195)]; + const tokenBatchIds = [2000n, 2010n, 2020n]; + const mintValues = [5000n, 10000n, 42195n]; + const burnValues = [5000n, 9001n, 195n]; const data = '0x12345678'; describe('_mint', function () { it('reverts with a zero destination address', async function () { - await expectRevertCustomError( - this.token.$_mint(ZERO_ADDRESS, tokenId, mintValue, data), - 'ERC1155InvalidReceiver', - [ZERO_ADDRESS], - ); + await expect(this.token.$_mint(ethers.ZeroAddress, tokenId, mintValue, data)) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver') + .withArgs(ethers.ZeroAddress); }); - context('with minted tokens', function () { + describe('with minted tokens', function () { beforeEach(async function () { - this.receipt = await this.token.$_mint(tokenHolder, tokenId, mintValue, data, { from: operator }); + this.tx = await this.token.connect(this.operator).$_mint(this.holder, tokenId, mintValue, data); }); - it('emits a TransferSingle event', function () { - expectEvent(this.receipt, 'TransferSingle', { - operator, - from: ZERO_ADDRESS, - to: tokenHolder, - id: tokenId, - value: mintValue, - }); + it('emits a TransferSingle event', async function () { + await expect(this.tx) + .to.emit(this.token, 'TransferSingle') + .withArgs(this.operator.address, ethers.ZeroAddress, this.holder.address, tokenId, mintValue); }); it('credits the minted token value', async function () { - expect(await this.token.balanceOf(tokenHolder, tokenId)).to.be.bignumber.equal(mintValue); + expect(await this.token.balanceOf(this.holder, tokenId)).to.equal(mintValue); }); }); }); describe('_mintBatch', function () { it('reverts with a zero destination address', async function () { - await expectRevertCustomError( - this.token.$_mintBatch(ZERO_ADDRESS, tokenBatchIds, mintValues, data), - 'ERC1155InvalidReceiver', - [ZERO_ADDRESS], - ); + await expect(this.token.$_mintBatch(ethers.ZeroAddress, tokenBatchIds, mintValues, data)) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidReceiver') + .withArgs(ethers.ZeroAddress); }); it('reverts if length of inputs do not match', async function () { - await expectRevertCustomError( - this.token.$_mintBatch(tokenBatchHolder, tokenBatchIds, mintValues.slice(1), data), - 'ERC1155InvalidArrayLength', - [tokenBatchIds.length, mintValues.length - 1], - ); - - await expectRevertCustomError( - this.token.$_mintBatch(tokenBatchHolder, tokenBatchIds.slice(1), mintValues, data), - 'ERC1155InvalidArrayLength', - [tokenBatchIds.length - 1, mintValues.length], - ); + await expect(this.token.$_mintBatch(this.holder, tokenBatchIds, mintValues.slice(1), data)) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidArrayLength') + .withArgs(tokenBatchIds.length, mintValues.length - 1); + + await expect(this.token.$_mintBatch(this.holder, tokenBatchIds.slice(1), mintValues, data)) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidArrayLength') + .withArgs(tokenBatchIds.length - 1, mintValues.length); }); - context('with minted batch of tokens', function () { + describe('with minted batch of tokens', function () { beforeEach(async function () { - this.receipt = await this.token.$_mintBatch(tokenBatchHolder, tokenBatchIds, mintValues, data, { - from: operator, - }); + this.tx = await this.token.connect(this.operator).$_mintBatch(this.holder, tokenBatchIds, mintValues, data); }); - it('emits a TransferBatch event', function () { - expectEvent(this.receipt, 'TransferBatch', { - operator, - from: ZERO_ADDRESS, - to: tokenBatchHolder, - }); + it('emits a TransferBatch event', async function () { + await expect(this.tx) + .to.emit(this.token, 'TransferBatch') + .withArgs(this.operator.address, ethers.ZeroAddress, this.holder.address, tokenBatchIds, mintValues); }); it('credits the minted batch of tokens', async function () { const holderBatchBalances = await this.token.balanceOfBatch( - new Array(tokenBatchIds.length).fill(tokenBatchHolder), + tokenBatchIds.map(() => this.holder), tokenBatchIds, ); - for (let i = 0; i < holderBatchBalances.length; i++) { - expect(holderBatchBalances[i]).to.be.bignumber.equal(mintValues[i]); - } + expect(holderBatchBalances).to.deep.equal(mintValues); }); }); }); describe('_burn', function () { it("reverts when burning the zero account's tokens", async function () { - await expectRevertCustomError(this.token.$_burn(ZERO_ADDRESS, tokenId, mintValue), 'ERC1155InvalidSender', [ - ZERO_ADDRESS, - ]); + await expect(this.token.$_burn(ethers.ZeroAddress, tokenId, mintValue)) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidSender') + .withArgs(ethers.ZeroAddress); }); it('reverts when burning a non-existent token id', async function () { - await expectRevertCustomError( - this.token.$_burn(tokenHolder, tokenId, mintValue), - 'ERC1155InsufficientBalance', - [tokenHolder, 0, mintValue, tokenId], - ); + await expect(this.token.$_burn(this.holder, tokenId, mintValue)) + .to.be.revertedWithCustomError(this.token, 'ERC1155InsufficientBalance') + .withArgs(this.holder.address, 0, mintValue, tokenId); }); it('reverts when burning more than available tokens', async function () { - await this.token.$_mint(tokenHolder, tokenId, mintValue, data, { from: operator }); + await this.token.connect(this.operator).$_mint(this.holder, tokenId, mintValue, data); - await expectRevertCustomError( - this.token.$_burn(tokenHolder, tokenId, mintValue.addn(1)), - 'ERC1155InsufficientBalance', - [tokenHolder, mintValue, mintValue.addn(1), tokenId], - ); + await expect(this.token.$_burn(this.holder, tokenId, mintValue + 1n)) + .to.be.revertedWithCustomError(this.token, 'ERC1155InsufficientBalance') + .withArgs(this.holder.address, mintValue, mintValue + 1n, tokenId); }); - context('with minted-then-burnt tokens', function () { + describe('with minted-then-burnt tokens', function () { beforeEach(async function () { - await this.token.$_mint(tokenHolder, tokenId, mintValue, data); - this.receipt = await this.token.$_burn(tokenHolder, tokenId, burnValue, { from: operator }); + await this.token.$_mint(this.holder, tokenId, mintValue, data); + this.tx = await this.token.connect(this.operator).$_burn(this.holder, tokenId, burnValue); }); - it('emits a TransferSingle event', function () { - expectEvent(this.receipt, 'TransferSingle', { - operator, - from: tokenHolder, - to: ZERO_ADDRESS, - id: tokenId, - value: burnValue, - }); + it('emits a TransferSingle event', async function () { + await expect(this.tx) + .to.emit(this.token, 'TransferSingle') + .withArgs(this.operator.address, this.holder.address, ethers.ZeroAddress, tokenId, burnValue); }); it('accounts for both minting and burning', async function () { - expect(await this.token.balanceOf(tokenHolder, tokenId)).to.be.bignumber.equal(mintValue.sub(burnValue)); + expect(await this.token.balanceOf(this.holder, tokenId)).to.equal(mintValue - burnValue); }); }); }); describe('_burnBatch', function () { it("reverts when burning the zero account's tokens", async function () { - await expectRevertCustomError( - this.token.$_burnBatch(ZERO_ADDRESS, tokenBatchIds, burnValues), - 'ERC1155InvalidSender', - [ZERO_ADDRESS], - ); + await expect(this.token.$_burnBatch(ethers.ZeroAddress, tokenBatchIds, burnValues)) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidSender') + .withArgs(ethers.ZeroAddress); }); it('reverts if length of inputs do not match', async function () { - await expectRevertCustomError( - this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds, burnValues.slice(1)), - 'ERC1155InvalidArrayLength', - [tokenBatchIds.length, burnValues.length - 1], - ); - - await expectRevertCustomError( - this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds.slice(1), burnValues), - 'ERC1155InvalidArrayLength', - [tokenBatchIds.length - 1, burnValues.length], - ); + await expect(this.token.$_burnBatch(this.holder, tokenBatchIds, burnValues.slice(1))) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidArrayLength') + .withArgs(tokenBatchIds.length, burnValues.length - 1); + + await expect(this.token.$_burnBatch(this.holder, tokenBatchIds.slice(1), burnValues)) + .to.be.revertedWithCustomError(this.token, 'ERC1155InvalidArrayLength') + .withArgs(tokenBatchIds.length - 1, burnValues.length); }); it('reverts when burning a non-existent token id', async function () { - await expectRevertCustomError( - this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds, burnValues), - 'ERC1155InsufficientBalance', - [tokenBatchHolder, 0, tokenBatchIds[0], burnValues[0]], - ); + await expect(this.token.$_burnBatch(this.holder, tokenBatchIds, burnValues)) + .to.be.revertedWithCustomError(this.token, 'ERC1155InsufficientBalance') + .withArgs(this.holder.address, 0, burnValues[0], tokenBatchIds[0]); }); - context('with minted-then-burnt tokens', function () { + describe('with minted-then-burnt tokens', function () { beforeEach(async function () { - await this.token.$_mintBatch(tokenBatchHolder, tokenBatchIds, mintValues, data); - this.receipt = await this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds, burnValues, { from: operator }); + await this.token.$_mintBatch(this.holder, tokenBatchIds, mintValues, data); + this.tx = await this.token.connect(this.operator).$_burnBatch(this.holder, tokenBatchIds, burnValues); }); - it('emits a TransferBatch event', function () { - expectEvent(this.receipt, 'TransferBatch', { - operator, - from: tokenBatchHolder, - to: ZERO_ADDRESS, - // ids: tokenBatchIds, - // values: burnValues, - }); + it('emits a TransferBatch event', async function () { + await expect(this.tx) + .to.emit(this.token, 'TransferBatch') + .withArgs(this.operator.address, this.holder.address, ethers.ZeroAddress, tokenBatchIds, burnValues); }); it('accounts for both minting and burning', async function () { const holderBatchBalances = await this.token.balanceOfBatch( - new Array(tokenBatchIds.length).fill(tokenBatchHolder), + tokenBatchIds.map(() => this.holder), tokenBatchIds, ); - for (let i = 0; i < holderBatchBalances.length; i++) { - expect(holderBatchBalances[i]).to.be.bignumber.equal(mintValues[i].sub(burnValues[i])); - } + expect(holderBatchBalances).to.deep.equal( + zip(mintValues, burnValues).map(([mintValue, burnValue]) => mintValue - burnValue), + ); }); }); }); }); describe('ERC1155MetadataURI', function () { - const firstTokenID = new BN('42'); - const secondTokenID = new BN('1337'); + const firstTokenID = 42n; + const secondTokenID = 1337n; it('emits no URI event in constructor', async function () { - await expectEvent.notEmitted.inConstruction(this.token, 'URI'); + await expect(this.token.deploymentTransaction()).to.not.emit(this.token, 'URI'); }); it('sets the initial URI for all token types', async function () { - expect(await this.token.uri(firstTokenID)).to.be.equal(initialURI); - expect(await this.token.uri(secondTokenID)).to.be.equal(initialURI); + expect(await this.token.uri(firstTokenID)).to.equal(initialURI); + expect(await this.token.uri(secondTokenID)).to.equal(initialURI); }); describe('_setURI', function () { const newURI = 'https://token-cdn-domain/{locale}/{id}.json'; it('emits no URI event', async function () { - const receipt = await this.token.$_setURI(newURI); - - expectEvent.notEmitted(receipt, 'URI'); + await expect(this.token.$_setURI(newURI)).to.not.emit(this.token, 'URI'); }); it('sets the new URI for all token types', async function () { await this.token.$_setURI(newURI); - expect(await this.token.uri(firstTokenID)).to.be.equal(newURI); - expect(await this.token.uri(secondTokenID)).to.be.equal(newURI); + expect(await this.token.uri(firstTokenID)).to.equal(newURI); + expect(await this.token.uri(secondTokenID)).to.equal(newURI); }); }); }); diff --git a/test/token/ERC1155/extensions/ERC1155Burnable.test.js b/test/token/ERC1155/extensions/ERC1155Burnable.test.js index fc94db0527d..6a75dc2234c 100644 --- a/test/token/ERC1155/extensions/ERC1155Burnable.test.js +++ b/test/token/ERC1155/extensions/ERC1155Burnable.test.js @@ -1,71 +1,66 @@ -const { BN } = require('@openzeppelin/test-helpers'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { expectRevertCustomError } = require('../../../helpers/customError'); - -const ERC1155Burnable = artifacts.require('$ERC1155Burnable'); +const ids = [42n, 1137n]; +const values = [3000n, 9902n]; -contract('ERC1155Burnable', function (accounts) { - const [holder, operator, other] = accounts; +async function fixture() { + const [holder, operator, other] = await ethers.getSigners(); - const uri = 'https://token.com'; + const token = await ethers.deployContract('$ERC1155Burnable', ['https://token-cdn-domain/{id}.json']); + await token.$_mint(holder, ids[0], values[0], '0x'); + await token.$_mint(holder, ids[1], values[1], '0x'); - const tokenIds = [new BN('42'), new BN('1137')]; - const values = [new BN('3000'), new BN('9902')]; + return { token, holder, operator, other }; +} +describe('ERC1155Burnable', function () { beforeEach(async function () { - this.token = await ERC1155Burnable.new(uri); - - await this.token.$_mint(holder, tokenIds[0], values[0], '0x'); - await this.token.$_mint(holder, tokenIds[1], values[1], '0x'); + Object.assign(this, await loadFixture(fixture)); }); describe('burn', function () { it('holder can burn their tokens', async function () { - await this.token.burn(holder, tokenIds[0], values[0].subn(1), { from: holder }); + await this.token.connect(this.holder).burn(this.holder, ids[0], values[0] - 1n); - expect(await this.token.balanceOf(holder, tokenIds[0])).to.be.bignumber.equal('1'); + expect(await this.token.balanceOf(this.holder, ids[0])).to.equal(1n); }); it("approved operators can burn the holder's tokens", async function () { - await this.token.setApprovalForAll(operator, true, { from: holder }); - await this.token.burn(holder, tokenIds[0], values[0].subn(1), { from: operator }); + await this.token.connect(this.holder).setApprovalForAll(this.operator, true); + await this.token.connect(this.operator).burn(this.holder, ids[0], values[0] - 1n); - expect(await this.token.balanceOf(holder, tokenIds[0])).to.be.bignumber.equal('1'); + expect(await this.token.balanceOf(this.holder, ids[0])).to.equal(1n); }); it("unapproved accounts cannot burn the holder's tokens", async function () { - await expectRevertCustomError( - this.token.burn(holder, tokenIds[0], values[0].subn(1), { from: other }), - 'ERC1155MissingApprovalForAll', - [other, holder], - ); + await expect(this.token.connect(this.other).burn(this.holder, ids[0], values[0] - 1n)) + .to.be.revertedWithCustomError(this.token, 'ERC1155MissingApprovalForAll') + .withArgs(this.other.address, this.holder.address); }); }); describe('burnBatch', function () { it('holder can burn their tokens', async function () { - await this.token.burnBatch(holder, tokenIds, [values[0].subn(1), values[1].subn(2)], { from: holder }); + await this.token.connect(this.holder).burnBatch(this.holder, ids, [values[0] - 1n, values[1] - 2n]); - expect(await this.token.balanceOf(holder, tokenIds[0])).to.be.bignumber.equal('1'); - expect(await this.token.balanceOf(holder, tokenIds[1])).to.be.bignumber.equal('2'); + expect(await this.token.balanceOf(this.holder, ids[0])).to.equal(1n); + expect(await this.token.balanceOf(this.holder, ids[1])).to.equal(2n); }); it("approved operators can burn the holder's tokens", async function () { - await this.token.setApprovalForAll(operator, true, { from: holder }); - await this.token.burnBatch(holder, tokenIds, [values[0].subn(1), values[1].subn(2)], { from: operator }); + await this.token.connect(this.holder).setApprovalForAll(this.operator, true); + await this.token.connect(this.operator).burnBatch(this.holder, ids, [values[0] - 1n, values[1] - 2n]); - expect(await this.token.balanceOf(holder, tokenIds[0])).to.be.bignumber.equal('1'); - expect(await this.token.balanceOf(holder, tokenIds[1])).to.be.bignumber.equal('2'); + expect(await this.token.balanceOf(this.holder, ids[0])).to.equal(1n); + expect(await this.token.balanceOf(this.holder, ids[1])).to.equal(2n); }); it("unapproved accounts cannot burn the holder's tokens", async function () { - await expectRevertCustomError( - this.token.burnBatch(holder, tokenIds, [values[0].subn(1), values[1].subn(2)], { from: other }), - 'ERC1155MissingApprovalForAll', - [other, holder], - ); + await expect(this.token.connect(this.other).burnBatch(this.holder, ids, [values[0] - 1n, values[1] - 2n])) + .to.be.revertedWithCustomError(this.token, 'ERC1155MissingApprovalForAll') + .withArgs(this.other.address, this.holder.address); }); }); }); diff --git a/test/token/ERC1155/extensions/ERC1155Pausable.test.js b/test/token/ERC1155/extensions/ERC1155Pausable.test.js index 248ea568424..7038180bbd6 100644 --- a/test/token/ERC1155/extensions/ERC1155Pausable.test.js +++ b/test/token/ERC1155/extensions/ERC1155Pausable.test.js @@ -1,112 +1,104 @@ -const { BN } = require('@openzeppelin/test-helpers'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { expectRevertCustomError } = require('../../../helpers/customError'); - -const ERC1155Pausable = artifacts.require('$ERC1155Pausable'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -contract('ERC1155Pausable', function (accounts) { - const [holder, operator, receiver, other] = accounts; +async function fixture() { + const [holder, operator, receiver, other] = await ethers.getSigners(); + const token = await ethers.deployContract('$ERC1155Pausable', ['https://token-cdn-domain/{id}.json']); + return { token, holder, operator, receiver, other }; +} - const uri = 'https://token.com'; +contract('ERC1155Pausable', function () { + const firstTokenId = 37n; + const firstTokenValue = 42n; + const secondTokenId = 19842n; + const secondTokenValue = 23n; beforeEach(async function () { - this.token = await ERC1155Pausable.new(uri); + Object.assign(this, await loadFixture(fixture)); }); context('when token is paused', function () { - const firstTokenId = new BN('37'); - const firstTokenValue = new BN('42'); - - const secondTokenId = new BN('19842'); - const secondTokenValue = new BN('23'); - beforeEach(async function () { - await this.token.setApprovalForAll(operator, true, { from: holder }); - await this.token.$_mint(holder, firstTokenId, firstTokenValue, '0x'); - + await this.token.connect(this.holder).setApprovalForAll(this.operator, true); + await this.token.$_mint(this.holder, firstTokenId, firstTokenValue, '0x'); await this.token.$_pause(); }); it('reverts when trying to safeTransferFrom from holder', async function () { - await expectRevertCustomError( - this.token.safeTransferFrom(holder, receiver, firstTokenId, firstTokenValue, '0x', { from: holder }), - 'EnforcedPause', - [], - ); + await expect( + this.token + .connect(this.holder) + .safeTransferFrom(this.holder, this.receiver, firstTokenId, firstTokenValue, '0x'), + ).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); it('reverts when trying to safeTransferFrom from operator', async function () { - await expectRevertCustomError( - this.token.safeTransferFrom(holder, receiver, firstTokenId, firstTokenValue, '0x', { from: operator }), - 'EnforcedPause', - [], - ); + await expect( + this.token + .connect(this.operator) + .safeTransferFrom(this.holder, this.receiver, firstTokenId, firstTokenValue, '0x'), + ).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); it('reverts when trying to safeBatchTransferFrom from holder', async function () { - await expectRevertCustomError( - this.token.safeBatchTransferFrom(holder, receiver, [firstTokenId], [firstTokenValue], '0x', { from: holder }), - 'EnforcedPause', - [], - ); + await expect( + this.token + .connect(this.holder) + .safeBatchTransferFrom(this.holder, this.receiver, [firstTokenId], [firstTokenValue], '0x'), + ).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); it('reverts when trying to safeBatchTransferFrom from operator', async function () { - await expectRevertCustomError( - this.token.safeBatchTransferFrom(holder, receiver, [firstTokenId], [firstTokenValue], '0x', { - from: operator, - }), - 'EnforcedPause', - [], - ); + await expect( + this.token + .connect(this.operator) + .safeBatchTransferFrom(this.holder, this.receiver, [firstTokenId], [firstTokenValue], '0x'), + ).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); it('reverts when trying to mint', async function () { - await expectRevertCustomError( - this.token.$_mint(holder, secondTokenId, secondTokenValue, '0x'), + await expect(this.token.$_mint(this.holder, secondTokenId, secondTokenValue, '0x')).to.be.revertedWithCustomError( + this.token, 'EnforcedPause', - [], ); }); it('reverts when trying to mintBatch', async function () { - await expectRevertCustomError( - this.token.$_mintBatch(holder, [secondTokenId], [secondTokenValue], '0x'), - 'EnforcedPause', - [], - ); + await expect( + this.token.$_mintBatch(this.holder, [secondTokenId], [secondTokenValue], '0x'), + ).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); it('reverts when trying to burn', async function () { - await expectRevertCustomError(this.token.$_burn(holder, firstTokenId, firstTokenValue), 'EnforcedPause', []); + await expect(this.token.$_burn(this.holder, firstTokenId, firstTokenValue)).to.be.revertedWithCustomError( + this.token, + 'EnforcedPause', + ); }); it('reverts when trying to burnBatch', async function () { - await expectRevertCustomError( - this.token.$_burnBatch(holder, [firstTokenId], [firstTokenValue]), - 'EnforcedPause', - [], - ); + await expect( + this.token.$_burnBatch(this.holder, [firstTokenId], [firstTokenValue]), + ).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); }); describe('setApprovalForAll', function () { it('approves an operator', async function () { - await this.token.setApprovalForAll(other, true, { from: holder }); - expect(await this.token.isApprovedForAll(holder, other)).to.equal(true); + await this.token.connect(this.holder).setApprovalForAll(this.other, true); + expect(await this.token.isApprovedForAll(this.holder, this.other)).to.be.true; }); }); describe('balanceOf', function () { it('returns the token value owned by the given address', async function () { - const balance = await this.token.balanceOf(holder, firstTokenId); - expect(balance).to.be.bignumber.equal(firstTokenValue); + expect(await this.token.balanceOf(this.holder, firstTokenId)).to.equal(firstTokenValue); }); }); describe('isApprovedForAll', function () { it('returns the approval of the operator', async function () { - expect(await this.token.isApprovedForAll(holder, operator)).to.equal(true); + expect(await this.token.isApprovedForAll(this.holder, this.operator)).to.be.true; }); }); }); diff --git a/test/token/ERC1155/extensions/ERC1155Supply.test.js b/test/token/ERC1155/extensions/ERC1155Supply.test.js index bf86920f62f..72736d8362a 100644 --- a/test/token/ERC1155/extensions/ERC1155Supply.test.js +++ b/test/token/ERC1155/extensions/ERC1155Supply.test.js @@ -1,116 +1,119 @@ -const { BN, constants } = require('@openzeppelin/test-helpers'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { ZERO_ADDRESS } = constants; - -const ERC1155Supply = artifacts.require('$ERC1155Supply'); - -contract('ERC1155Supply', function (accounts) { - const [holder] = accounts; - - const uri = 'https://token.com'; - - const firstTokenId = new BN('37'); - const firstTokenValue = new BN('42'); +async function fixture() { + const [holder] = await ethers.getSigners(); + const token = await ethers.deployContract('$ERC1155Supply', ['https://token-cdn-domain/{id}.json']); + return { token, holder }; +} - const secondTokenId = new BN('19842'); - const secondTokenValue = new BN('23'); +describe('ERC1155Supply', function () { + const firstTokenId = 37n; + const firstTokenValue = 42n; + const secondTokenId = 19842n; + const secondTokenValue = 23n; beforeEach(async function () { - this.token = await ERC1155Supply.new(uri); + Object.assign(this, await loadFixture(fixture)); }); - context('before mint', function () { + describe('before mint', function () { it('exist', async function () { - expect(await this.token.exists(firstTokenId)).to.be.equal(false); + expect(await this.token.exists(firstTokenId)).to.be.false; }); it('totalSupply', async function () { - expect(await this.token.methods['totalSupply(uint256)'](firstTokenId)).to.be.bignumber.equal('0'); - expect(await this.token.methods['totalSupply()']()).to.be.bignumber.equal('0'); + expect(await this.token.totalSupply(ethers.Typed.uint256(firstTokenId))).to.equal(0n); + expect(await this.token.totalSupply()).to.equal(0n); }); }); - context('after mint', function () { - context('single', function () { + describe('after mint', function () { + describe('single', function () { beforeEach(async function () { - await this.token.$_mint(holder, firstTokenId, firstTokenValue, '0x'); + await this.token.$_mint(this.holder, firstTokenId, firstTokenValue, '0x'); }); it('exist', async function () { - expect(await this.token.exists(firstTokenId)).to.be.equal(true); + expect(await this.token.exists(firstTokenId)).to.be.true; }); it('totalSupply', async function () { - expect(await this.token.methods['totalSupply(uint256)'](firstTokenId)).to.be.bignumber.equal(firstTokenValue); - expect(await this.token.methods['totalSupply()']()).to.be.bignumber.equal(firstTokenValue); + expect(await this.token.totalSupply(ethers.Typed.uint256(firstTokenId))).to.equal(firstTokenValue); + expect(await this.token.totalSupply()).to.equal(firstTokenValue); }); }); - context('batch', function () { + describe('batch', function () { beforeEach(async function () { - await this.token.$_mintBatch(holder, [firstTokenId, secondTokenId], [firstTokenValue, secondTokenValue], '0x'); + await this.token.$_mintBatch( + this.holder, + [firstTokenId, secondTokenId], + [firstTokenValue, secondTokenValue], + '0x', + ); }); it('exist', async function () { - expect(await this.token.exists(firstTokenId)).to.be.equal(true); - expect(await this.token.exists(secondTokenId)).to.be.equal(true); + expect(await this.token.exists(firstTokenId)).to.be.true; + expect(await this.token.exists(secondTokenId)).to.be.true; }); it('totalSupply', async function () { - expect(await this.token.methods['totalSupply(uint256)'](firstTokenId)).to.be.bignumber.equal(firstTokenValue); - expect(await this.token.methods['totalSupply(uint256)'](secondTokenId)).to.be.bignumber.equal(secondTokenValue); - expect(await this.token.methods['totalSupply()']()).to.be.bignumber.equal( - firstTokenValue.add(secondTokenValue), - ); + expect(await this.token.totalSupply(ethers.Typed.uint256(firstTokenId))).to.equal(firstTokenValue); + expect(await this.token.totalSupply(ethers.Typed.uint256(secondTokenId))).to.equal(secondTokenValue); + expect(await this.token.totalSupply()).to.equal(firstTokenValue + secondTokenValue); }); }); }); - context('after burn', function () { - context('single', function () { + describe('after burn', function () { + describe('single', function () { beforeEach(async function () { - await this.token.$_mint(holder, firstTokenId, firstTokenValue, '0x'); - await this.token.$_burn(holder, firstTokenId, firstTokenValue); + await this.token.$_mint(this.holder, firstTokenId, firstTokenValue, '0x'); + await this.token.$_burn(this.holder, firstTokenId, firstTokenValue); }); it('exist', async function () { - expect(await this.token.exists(firstTokenId)).to.be.equal(false); + expect(await this.token.exists(firstTokenId)).to.be.false; }); it('totalSupply', async function () { - expect(await this.token.methods['totalSupply(uint256)'](firstTokenId)).to.be.bignumber.equal('0'); - expect(await this.token.methods['totalSupply()']()).to.be.bignumber.equal('0'); + expect(await this.token.totalSupply(ethers.Typed.uint256(firstTokenId))).to.equal(0n); + expect(await this.token.totalSupply()).to.equal(0n); }); }); - context('batch', function () { + describe('batch', function () { beforeEach(async function () { - await this.token.$_mintBatch(holder, [firstTokenId, secondTokenId], [firstTokenValue, secondTokenValue], '0x'); - await this.token.$_burnBatch(holder, [firstTokenId, secondTokenId], [firstTokenValue, secondTokenValue]); + await this.token.$_mintBatch( + this.holder, + [firstTokenId, secondTokenId], + [firstTokenValue, secondTokenValue], + '0x', + ); + await this.token.$_burnBatch(this.holder, [firstTokenId, secondTokenId], [firstTokenValue, secondTokenValue]); }); it('exist', async function () { - expect(await this.token.exists(firstTokenId)).to.be.equal(false); - expect(await this.token.exists(secondTokenId)).to.be.equal(false); + expect(await this.token.exists(firstTokenId)).to.be.false; + expect(await this.token.exists(secondTokenId)).to.be.false; }); it('totalSupply', async function () { - expect(await this.token.methods['totalSupply(uint256)'](firstTokenId)).to.be.bignumber.equal('0'); - expect(await this.token.methods['totalSupply(uint256)'](secondTokenId)).to.be.bignumber.equal('0'); - expect(await this.token.methods['totalSupply()']()).to.be.bignumber.equal('0'); + expect(await this.token.totalSupply(ethers.Typed.uint256(firstTokenId))).to.equal(0n); + expect(await this.token.totalSupply(ethers.Typed.uint256(secondTokenId))).to.equal(0n); + expect(await this.token.totalSupply()).to.equal(0n); }); }); }); - context('other', function () { + describe('other', function () { it('supply unaffected by no-op', async function () { - this.token.safeTransferFrom(ZERO_ADDRESS, ZERO_ADDRESS, firstTokenId, firstTokenValue, '0x', { - from: ZERO_ADDRESS, - }); - expect(await this.token.methods['totalSupply(uint256)'](firstTokenId)).to.be.bignumber.equal('0'); - expect(await this.token.methods['totalSupply()']()).to.be.bignumber.equal('0'); + this.token.safeTransferFrom(ethers.ZeroAddress, ethers.ZeroAddress, firstTokenId, firstTokenValue, '0x'); + expect(await this.token.totalSupply(ethers.Typed.uint256(firstTokenId))).to.equal(0n); + expect(await this.token.totalSupply()).to.equal(0n); }); }); }); diff --git a/test/token/ERC1155/extensions/ERC1155URIStorage.test.js b/test/token/ERC1155/extensions/ERC1155URIStorage.test.js index 58ac67bc655..a0d9b570488 100644 --- a/test/token/ERC1155/extensions/ERC1155URIStorage.test.js +++ b/test/token/ERC1155/extensions/ERC1155URIStorage.test.js @@ -1,66 +1,70 @@ -const { BN, expectEvent } = require('@openzeppelin/test-helpers'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { artifacts } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const ERC1155URIStorage = artifacts.require('$ERC1155URIStorage'); +const erc1155Uri = 'https://token.com/nfts/'; +const baseUri = 'https://token.com/'; +const tokenId = 1n; +const value = 3000n; -contract(['ERC1155URIStorage'], function (accounts) { - const [holder] = accounts; +describe('ERC1155URIStorage', function () { + describe('with base uri set', function () { + async function fixture() { + const [holder] = await ethers.getSigners(); - const erc1155Uri = 'https://token.com/nfts/'; - const baseUri = 'https://token.com/'; + const token = await ethers.deployContract('$ERC1155URIStorage', [erc1155Uri]); + await token.$_setBaseURI(baseUri); + await token.$_mint(holder, tokenId, value, '0x'); - const tokenId = new BN('1'); - const value = new BN('3000'); + return { token, holder }; + } - describe('with base uri set', function () { beforeEach(async function () { - this.token = await ERC1155URIStorage.new(erc1155Uri); - await this.token.$_setBaseURI(baseUri); - - await this.token.$_mint(holder, tokenId, value, '0x'); + Object.assign(this, await loadFixture(fixture)); }); it('can request the token uri, returning the erc1155 uri if no token uri was set', async function () { - const receivedTokenUri = await this.token.uri(tokenId); - - expect(receivedTokenUri).to.be.equal(erc1155Uri); + expect(await this.token.uri(tokenId)).to.equal(erc1155Uri); }); it('can request the token uri, returning the concatenated uri if a token uri was set', async function () { const tokenUri = '1234/'; - const receipt = await this.token.$_setURI(tokenId, tokenUri); + const expectedUri = `${baseUri}${tokenUri}`; - const receivedTokenUri = await this.token.uri(tokenId); + await expect(this.token.$_setURI(ethers.Typed.uint256(tokenId), tokenUri)) + .to.emit(this.token, 'URI') + .withArgs(expectedUri, tokenId); - const expectedUri = `${baseUri}${tokenUri}`; - expect(receivedTokenUri).to.be.equal(expectedUri); - expectEvent(receipt, 'URI', { value: expectedUri, id: tokenId }); + expect(await this.token.uri(tokenId)).to.equal(expectedUri); }); }); describe('with base uri set to the empty string', function () { - beforeEach(async function () { - this.token = await ERC1155URIStorage.new(''); + async function fixture() { + const [holder] = await ethers.getSigners(); + + const token = await ethers.deployContract('$ERC1155URIStorage', ['']); + await token.$_mint(holder, tokenId, value, '0x'); - await this.token.$_mint(holder, tokenId, value, '0x'); + return { token, holder }; + } + + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); }); it('can request the token uri, returning an empty string if no token uri was set', async function () { - const receivedTokenUri = await this.token.uri(tokenId); - - expect(receivedTokenUri).to.be.equal(''); + expect(await this.token.uri(tokenId)).to.equal(''); }); it('can request the token uri, returning the token uri if a token uri was set', async function () { const tokenUri = 'ipfs://1234/'; - const receipt = await this.token.$_setURI(tokenId, tokenUri); - const receivedTokenUri = await this.token.uri(tokenId); + await expect(this.token.$_setURI(ethers.Typed.uint256(tokenId), tokenUri)) + .to.emit(this.token, 'URI') + .withArgs(tokenUri, tokenId); - expect(receivedTokenUri).to.be.equal(tokenUri); - expectEvent(receipt, 'URI', { value: tokenUri, id: tokenId }); + expect(await this.token.uri(tokenId)).to.equal(tokenUri); }); }); }); diff --git a/test/token/ERC1155/utils/ERC1155Holder.test.js b/test/token/ERC1155/utils/ERC1155Holder.test.js index ee818eae862..52705fc4542 100644 --- a/test/token/ERC1155/utils/ERC1155Holder.test.js +++ b/test/token/ERC1155/utils/ERC1155Holder.test.js @@ -1,64 +1,56 @@ -const { BN } = require('@openzeppelin/test-helpers'); - -const ERC1155Holder = artifacts.require('$ERC1155Holder'); -const ERC1155 = artifacts.require('$ERC1155'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior'); -contract('ERC1155Holder', function (accounts) { - const [creator] = accounts; - const uri = 'https://token-cdn-domain/{id}.json'; - const multiTokenIds = [new BN(1), new BN(2), new BN(3)]; - const multiTokenValues = [new BN(1000), new BN(2000), new BN(3000)]; - const transferData = '0x12345678'; +const ids = [1n, 2n, 3n]; +const values = [1000n, 2000n, 3000n]; +const data = '0x12345678'; + +async function fixture() { + const [owner] = await ethers.getSigners(); + + const token = await ethers.deployContract('$ERC1155', ['https://token-cdn-domain/{id}.json']); + const mock = await ethers.deployContract('$ERC1155Holder'); + + await token.$_mintBatch(owner, ids, values, '0x'); + return { owner, token, mock }; +} + +describe('ERC1155Holder', function () { beforeEach(async function () { - this.multiToken = await ERC1155.new(uri); - this.holder = await ERC1155Holder.new(); - await this.multiToken.$_mintBatch(creator, multiTokenIds, multiTokenValues, '0x'); + Object.assign(this, await loadFixture(fixture)); }); shouldSupportInterfaces(['ERC165', 'ERC1155Receiver']); it('receives ERC1155 tokens from a single ID', async function () { - await this.multiToken.safeTransferFrom( - creator, - this.holder.address, - multiTokenIds[0], - multiTokenValues[0], - transferData, - { from: creator }, - ); + await this.token.connect(this.owner).safeTransferFrom(this.owner, this.mock, ids[0], values[0], data); - expect(await this.multiToken.balanceOf(this.holder.address, multiTokenIds[0])).to.be.bignumber.equal( - multiTokenValues[0], - ); + expect(await this.token.balanceOf(this.mock, ids[0])).to.equal(values[0]); - for (let i = 1; i < multiTokenIds.length; i++) { - expect(await this.multiToken.balanceOf(this.holder.address, multiTokenIds[i])).to.be.bignumber.equal(new BN(0)); + for (let i = 1; i < ids.length; i++) { + expect(await this.token.balanceOf(this.mock, ids[i])).to.equal(0n); } }); it('receives ERC1155 tokens from a multiple IDs', async function () { - for (let i = 0; i < multiTokenIds.length; i++) { - expect(await this.multiToken.balanceOf(this.holder.address, multiTokenIds[i])).to.be.bignumber.equal(new BN(0)); - } - - await this.multiToken.safeBatchTransferFrom( - creator, - this.holder.address, - multiTokenIds, - multiTokenValues, - transferData, - { from: creator }, - ); - - for (let i = 0; i < multiTokenIds.length; i++) { - expect(await this.multiToken.balanceOf(this.holder.address, multiTokenIds[i])).to.be.bignumber.equal( - multiTokenValues[i], - ); - } + expect( + await this.token.balanceOfBatch( + ids.map(() => this.mock), + ids, + ), + ).to.deep.equal(ids.map(() => 0n)); + + await this.token.connect(this.owner).safeBatchTransferFrom(this.owner, this.mock, ids, values, data); + + expect( + await this.token.balanceOfBatch( + ids.map(() => this.mock), + ids, + ), + ).to.deep.equal(values); }); }); diff --git a/test/token/ERC721/ERC721.behavior.js b/test/token/ERC721/ERC721.behavior.js index 32d67d90d98..ff441f5e61b 100644 --- a/test/token/ERC721/ERC721.behavior.js +++ b/test/token/ERC721/ERC721.behavior.js @@ -5,11 +5,9 @@ const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); const { - bigint: { Enum }, + bigint: { RevertType }, } = require('../../helpers/enums'); -const RevertType = Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'); - const firstTokenId = 5042n; const secondTokenId = 79217n; const nonExistentTokenId = 13n; From 015ef69287ba794cba0551db124495d9dd977ad9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 22 Dec 2023 20:50:25 +0100 Subject: [PATCH 43/44] Refactor time helper and remove custom error helper. (#4803) Co-authored-by: ernestognw --- test/access/AccessControl.behavior.js | 54 ++++++++------- test/access/manager/AccessManaged.test.js | 12 ++-- .../access/manager/AccessManager.predicate.js | 13 ++-- test/access/manager/AccessManager.test.js | 65 +++++++++---------- test/access/manager/AuthorityUtils.test.js | 2 +- test/finance/VestingWallet.behavior.js | 6 +- test/finance/VestingWallet.test.js | 3 +- test/governance/TimelockController.test.js | 32 ++++----- .../extensions/GovernorTimelockAccess.test.js | 7 +- .../GovernorTimelockControl.test.js | 2 +- test/governance/utils/Votes.behavior.js | 2 +- test/governance/utils/Votes.test.js | 2 +- test/helpers/access-manager.js | 15 ++--- test/helpers/constants.js | 10 +-- test/helpers/customError.js | 45 ------------- test/helpers/governance.js | 8 +-- test/helpers/namespaced-storage.js | 14 ++-- test/helpers/time.js | 48 +++++++++----- test/proxy/utils/Initializable.test.js | 4 +- test/token/ERC1155/ERC1155.behavior.js | 1 + test/token/ERC1155/ERC1155.test.js | 2 +- .../ERC20/extensions/ERC20Burnable.test.js | 1 + test/utils/math/Math.test.js | 1 + test/utils/math/SafeCast.test.js | 1 + test/utils/math/SignedMath.test.js | 1 + test/utils/structs/BitMap.test.js | 2 +- test/utils/structs/Checkpoints.test.js | 2 +- test/utils/structs/DoubleEndedQueue.test.js | 2 +- test/utils/structs/EnumerableMap.behavior.js | 2 +- test/utils/structs/EnumerableMap.test.js | 1 + test/utils/structs/EnumerableSet.test.js | 1 + test/utils/types/Time.test.js | 6 +- 32 files changed, 158 insertions(+), 209 deletions(-) delete mode 100644 test/helpers/customError.js diff --git a/test/access/AccessControl.behavior.js b/test/access/AccessControl.behavior.js index 836ca2d6af2..e9027cd65e3 100644 --- a/test/access/AccessControl.behavior.js +++ b/test/access/AccessControl.behavior.js @@ -1,5 +1,6 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); + const { bigint: time } = require('../helpers/time'); const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); @@ -279,8 +280,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { await this.mock.connect(this.defaultAdmin).beginDefaultAdminTransfer(this.newDefaultAdmin); // Wait for acceptance - const acceptSchedule = (await time.clock.timestamp()) + this.delay; - await time.forward.timestamp(acceptSchedule + 1n, false); + await time.increaseBy.timestamp(this.delay + 1n, false); await this.mock.connect(this.newDefaultAdmin).acceptDefaultAdminTransfer(); const value = await this.mock[getter](); @@ -309,7 +309,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { it(`returns pending admin and schedule ${tag} it passes if not accepted`, async function () { // Wait until schedule + fromSchedule const { schedule: firstSchedule } = await this.mock.pendingDefaultAdmin(); - await time.forward.timestamp(firstSchedule + fromSchedule); + await time.increaseTo.timestamp(firstSchedule + fromSchedule); const { newAdmin, schedule } = await this.mock.pendingDefaultAdmin(); expect(newAdmin).to.equal(this.newDefaultAdmin.address); @@ -320,7 +320,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { it('returns 0 after schedule passes and the transfer was accepted', async function () { // Wait after schedule const { schedule: firstSchedule } = await this.mock.pendingDefaultAdmin(); - await time.forward.timestamp(firstSchedule + 1n, false); + await time.increaseTo.timestamp(firstSchedule + 1n, false); // Accepts await this.mock.connect(this.newDefaultAdmin).acceptDefaultAdminTransfer(); @@ -352,7 +352,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { it(`returns ${delayTag} delay ${tag} delay schedule passes`, async function () { // Wait until schedule + fromSchedule const { schedule } = await this.mock.pendingDefaultAdminDelay(); - await time.forward.timestamp(schedule + fromSchedule); + await time.increaseTo.timestamp(schedule + fromSchedule); const currentDelay = await this.mock.defaultAdminDelay(); expect(currentDelay).to.equal(expectNew ? newDelay : this.delay); @@ -383,7 +383,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { it(`returns ${delayTag} delay ${tag} delay schedule passes`, async function () { // Wait until schedule + fromSchedule const { schedule: firstSchedule } = await this.mock.pendingDefaultAdminDelay(); - await time.forward.timestamp(firstSchedule + fromSchedule); + await time.increaseTo.timestamp(firstSchedule + fromSchedule); const { newDelay, schedule } = await this.mock.pendingDefaultAdminDelay(); expect(newDelay).to.equal(expectedDelay); @@ -437,7 +437,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { const nextBlockTimestamp = (await time.clock.timestamp()) + 1n; const acceptSchedule = nextBlockTimestamp + this.delay; - await time.forward.timestamp(nextBlockTimestamp, false); // set timestamp but don't mine the block yet + await time.increaseTo.timestamp(nextBlockTimestamp, false); // set timestamp but don't mine the block yet await expect(this.mock.connect(this.defaultAdmin).beginDefaultAdminTransfer(this.newDefaultAdmin)) .to.emit(this.mock, 'DefaultAdminTransferScheduled') .withArgs(this.newDefaultAdmin.address, acceptSchedule); @@ -461,7 +461,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { ]) { it(`should be able to begin a transfer again ${tag} acceptSchedule passes`, async function () { // Wait until schedule + fromSchedule - await time.forward.timestamp(this.acceptSchedule + fromSchedule, false); + await time.increaseTo.timestamp(this.acceptSchedule + fromSchedule, false); // defaultAdmin changes its mind and begin again to another address await expect(this.mock.connect(this.defaultAdmin).beginDefaultAdminTransfer(this.other)).to.emit( @@ -477,7 +477,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { it('should not emit a cancellation event if the new default admin accepted', async function () { // Wait until the acceptSchedule has passed - await time.forward.timestamp(this.acceptSchedule + 1n, false); + await time.increaseTo.timestamp(this.acceptSchedule + 1n, false); // Accept and restart await this.mock.connect(this.newDefaultAdmin).acceptDefaultAdminTransfer(); @@ -506,7 +506,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { } delay and apply it to next default admin transfer schedule ${schedulePassed} effectSchedule passed`, async function () { // Wait until the expected fromSchedule time const nextBlockTimestamp = this.effectSchedule + fromSchedule; - await time.forward.timestamp(nextBlockTimestamp, false); + await time.increaseTo.timestamp(nextBlockTimestamp, false); // Start the new default admin transfer and get its schedule const expectedDelay = expectNewDelay ? newDelay : this.delay; @@ -531,7 +531,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { }); it('should revert if caller is not pending default admin', async function () { - await time.forward.timestamp(this.acceptSchedule + 1n, false); + await time.increaseTo.timestamp(this.acceptSchedule + 1n, false); await expect(this.mock.connect(this.other).acceptDefaultAdminTransfer()) .to.be.revertedWithCustomError(this.mock, 'AccessControlInvalidDefaultAdmin') .withArgs(this.other.address); @@ -539,7 +539,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { describe('when caller is pending default admin and delay has passed', function () { beforeEach(async function () { - await time.forward.timestamp(this.acceptSchedule + 1n, false); + await time.increaseTo.timestamp(this.acceptSchedule + 1n, false); }); it('accepts a transfer and changes default admin', async function () { @@ -568,7 +568,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { [0n, 'equal'], ]) { it(`should revert if block.timestamp is ${tag} to schedule`, async function () { - await time.forward.timestamp(this.acceptSchedule + fromSchedule, false); + await time.increaseTo.timestamp(this.acceptSchedule + fromSchedule, false); expect(this.mock.connect(this.newDefaultAdmin).acceptDefaultAdminTransfer()) .to.be.revertedWithCustomError(this.mock, 'AccessControlEnforcedDefaultAdminDelay') .withArgs(this.acceptSchedule); @@ -597,7 +597,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { ]) { it(`resets pending default admin and schedule ${tag} transfer schedule passes`, async function () { // Advance until passed delay - await time.forward.timestamp(this.acceptSchedule + fromSchedule, false); + await time.increaseTo.timestamp(this.acceptSchedule + fromSchedule, false); await expect(this.mock.connect(this.defaultAdmin).cancelDefaultAdminTransfer()).to.emit( this.mock, @@ -614,7 +614,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { await this.mock.connect(this.defaultAdmin).cancelDefaultAdminTransfer(); // Advance until passed delay - await time.forward.timestamp(this.acceptSchedule + 1n, false); + await time.increaseTo.timestamp(this.acceptSchedule + 1n, false); // Previous pending default admin should not be able to accept after cancellation. await expect(this.mock.connect(this.newDefaultAdmin).acceptDefaultAdminTransfer()) @@ -641,19 +641,17 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { beforeEach(async function () { await this.mock.connect(this.defaultAdmin).beginDefaultAdminTransfer(ethers.ZeroAddress); this.expectedSchedule = (await time.clock.timestamp()) + this.delay; - this.delayNotPassed = this.expectedSchedule; - this.delayPassed = this.expectedSchedule + 1n; }); it('reverts if caller is not default admin', async function () { - await time.forward.timestamp(this.delayPassed, false); + await time.increaseBy.timestamp(this.delay + 1n, false); await expect( this.mock.connect(this.defaultAdmin).renounceRole(DEFAULT_ADMIN_ROLE, this.other), ).to.be.revertedWithCustomError(this.mock, 'AccessControlBadConfirmation'); }); it("renouncing the admin role when not an admin doesn't affect the schedule", async function () { - await time.forward.timestamp(this.delayPassed, false); + await time.increaseBy.timestamp(this.delay + 1n, false); await this.mock.connect(this.other).renounceRole(DEFAULT_ADMIN_ROLE, this.other); const { newAdmin, schedule } = await this.mock.pendingDefaultAdmin(); @@ -662,7 +660,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { }); it('keeps defaultAdmin consistent with hasRole if another non-defaultAdmin user renounces the DEFAULT_ADMIN_ROLE', async function () { - await time.forward.timestamp(this.delayPassed, false); + await time.increaseBy.timestamp(this.delay + 1n, false); // This passes because it's a noop await this.mock.connect(this.other).renounceRole(DEFAULT_ADMIN_ROLE, this.other); @@ -672,7 +670,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { }); it('renounces role', async function () { - await time.forward.timestamp(this.delayPassed, false); + await time.increaseBy.timestamp(this.delay + 1n, false); await expect(this.mock.connect(this.defaultAdmin).renounceRole(DEFAULT_ADMIN_ROLE, this.defaultAdmin)) .to.emit(this.mock, 'RoleRevoked') .withArgs(DEFAULT_ADMIN_ROLE, this.defaultAdmin.address, this.defaultAdmin.address); @@ -687,7 +685,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { }); it('allows to recover access using the internal _grantRole', async function () { - await time.forward.timestamp(this.delayPassed, false); + await time.increaseBy.timestamp(this.delay + 1n, false); await this.mock.connect(this.defaultAdmin).renounceRole(DEFAULT_ADMIN_ROLE, this.defaultAdmin); await expect(this.mock.connect(this.defaultAdmin).$_grantRole(DEFAULT_ADMIN_ROLE, this.other)) @@ -701,7 +699,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { [0n, 'equal'], ]) { it(`reverts if block.timestamp is ${tag} to schedule`, async function () { - await time.forward.timestamp(this.delayNotPassed + fromSchedule, false); + await time.increaseBy.timestamp(this.delay + fromSchedule, false); await expect(this.mock.connect(this.defaultAdmin).renounceRole(DEFAULT_ADMIN_ROLE, this.defaultAdmin)) .to.be.revertedWithCustomError(this.mock, 'AccessControlEnforcedDefaultAdminDelay') .withArgs(this.expectedSchedule); @@ -736,7 +734,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { const nextBlockTimestamp = (await time.clock.timestamp()) + 1n; const effectSchedule = nextBlockTimestamp + changeDelay; - await time.forward.timestamp(nextBlockTimestamp, false); + await time.increaseTo.timestamp(nextBlockTimestamp, false); // Begins the change await expect(this.mock.connect(this.defaultAdmin).changeDefaultAdminDelay(this.newDefaultAdminDelay)) @@ -765,7 +763,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { // Wait until schedule + fromSchedule const { schedule: firstSchedule } = await this.mock.pendingDefaultAdminDelay(); const nextBlockTimestamp = firstSchedule + fromSchedule; - await time.forward.timestamp(nextBlockTimestamp, false); + await time.increaseTo.timestamp(nextBlockTimestamp, false); // Calculate expected values const anotherNewDefaultAdminDelay = this.newDefaultAdminDelay + time.duration.hours(2); @@ -788,7 +786,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { it(`should ${emit} a cancellation event ${tag} the delay schedule passes`, async function () { // Wait until schedule + fromSchedule const { schedule: firstSchedule } = await this.mock.pendingDefaultAdminDelay(); - await time.forward.timestamp(firstSchedule + fromSchedule, false); + await time.increaseTo.timestamp(firstSchedule + fromSchedule, false); // Default admin changes its mind and begins another delay change const anotherNewDefaultAdminDelay = this.newDefaultAdminDelay + time.duration.hours(2); @@ -830,7 +828,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { it(`resets pending delay and schedule ${tag} delay change schedule passes`, async function () { // Wait until schedule + fromSchedule const { schedule: firstSchedule } = await this.mock.pendingDefaultAdminDelay(); - await time.forward.timestamp(firstSchedule + fromSchedule, false); + await time.increaseTo.timestamp(firstSchedule + fromSchedule, false); await this.mock.connect(this.defaultAdmin).rollbackDefaultAdminDelay(); @@ -843,7 +841,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules() { it(`should ${emit} a cancellation event ${tag} the delay schedule passes`, async function () { // Wait until schedule + fromSchedule const { schedule: firstSchedule } = await this.mock.pendingDefaultAdminDelay(); - await time.forward.timestamp(firstSchedule + fromSchedule, false); + await time.increaseTo.timestamp(firstSchedule + fromSchedule, false); const expected = expect(this.mock.connect(this.defaultAdmin).rollbackDefaultAdminDelay()); if (passed) { diff --git a/test/access/manager/AccessManaged.test.js b/test/access/manager/AccessManaged.test.js index b468128ff6c..8af07a7ddf6 100644 --- a/test/access/manager/AccessManaged.test.js +++ b/test/access/manager/AccessManaged.test.js @@ -1,7 +1,8 @@ -const { bigint: time } = require('../../helpers/time'); +const { ethers } = require('hardhat'); + const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { impersonate } = require('../../helpers/account'); -const { ethers } = require('hardhat'); +const { bigint: time } = require('../../helpers/time'); async function fixture() { const [admin, roleMember, other] = await ethers.getSigners(); @@ -84,14 +85,13 @@ describe('AccessManaged', function () { const calldata = this.managed.interface.encodeFunctionData(fn, []); // Schedule - const timestamp = await time.clock.timestamp(); - const scheduledAt = timestamp + 1n; + const scheduledAt = (await time.clock.timestamp()) + 1n; const when = scheduledAt + delay; - await time.forward.timestamp(scheduledAt, false); + await time.increaseTo.timestamp(scheduledAt, false); await this.authority.connect(this.roleMember).schedule(this.managed, calldata, when); // Set execution date - await time.forward.timestamp(when, false); + await time.increaseTo.timestamp(when, false); // Shouldn't revert await this.managed.connect(this.roleMember)[this.selector](); diff --git a/test/access/manager/AccessManager.predicate.js b/test/access/manager/AccessManager.predicate.js index 048437e035f..5bf40a3d7fa 100644 --- a/test/access/manager/AccessManager.predicate.js +++ b/test/access/manager/AccessManager.predicate.js @@ -1,8 +1,9 @@ +const { ethers } = require('hardhat'); const { setStorageAt } = require('@nomicfoundation/hardhat-network-helpers'); + const { EXECUTION_ID_STORAGE_SLOT, EXPIRATION, prepareOperation } = require('../../helpers/access-manager'); const { impersonate } = require('../../helpers/account'); const { bigint: time } = require('../../helpers/time'); -const { ethers } = require('hardhat'); // ============ COMMON PREDICATES ============ @@ -146,7 +147,7 @@ function testAsDelay(type, { before, after }) { describe(`when ${type} delay has not taken effect yet`, function () { beforeEach(`set next block timestamp before ${type} takes effect`, async function () { - await time.forward.timestamp(this.delayEffect - 1n, !!before.mineDelay); + await time.increaseTo.timestamp(this.delayEffect - 1n, !!before.mineDelay); }); before(); @@ -154,7 +155,7 @@ function testAsDelay(type, { before, after }) { describe(`when ${type} delay has taken effect`, function () { beforeEach(`set next block timestamp when ${type} takes effect`, async function () { - await time.forward.timestamp(this.delayEffect, !!after.mineDelay); + await time.increaseTo.timestamp(this.delayEffect, !!after.mineDelay); }); after(); @@ -187,7 +188,7 @@ function testAsSchedulableOperation({ scheduled: { before, after, expired }, not beforeEach('set next block time before operation is ready', async function () { this.scheduledAt = await time.clock.timestamp(); const schedule = await this.manager.getSchedule(this.operationId); - await time.forward.timestamp(schedule - 1n, !!before.mineDelay); + await time.increaseTo.timestamp(schedule - 1n, !!before.mineDelay); }); before(); @@ -197,7 +198,7 @@ function testAsSchedulableOperation({ scheduled: { before, after, expired }, not beforeEach('set next block time when operation is ready for execution', async function () { this.scheduledAt = await time.clock.timestamp(); const schedule = await this.manager.getSchedule(this.operationId); - await time.forward.timestamp(schedule, !!after.mineDelay); + await time.increaseTo.timestamp(schedule, !!after.mineDelay); }); after(); @@ -207,7 +208,7 @@ function testAsSchedulableOperation({ scheduled: { before, after, expired }, not beforeEach('set next block time when operation expired', async function () { this.scheduledAt = await time.clock.timestamp(); const schedule = await this.manager.getSchedule(this.operationId); - await time.forward.timestamp(schedule + EXPIRATION, !!expired.mineDelay); + await time.increaseTo.timestamp(schedule + EXPIRATION, !!expired.mineDelay); }); expired(); diff --git a/test/access/manager/AccessManager.test.js b/test/access/manager/AccessManager.test.js index b26a246af47..ed16dd007e6 100644 --- a/test/access/manager/AccessManager.test.js +++ b/test/access/manager/AccessManager.test.js @@ -1,5 +1,12 @@ -const { ethers, expect } = require('hardhat'); +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture, getStorageAt } = require('@nomicfoundation/hardhat-network-helpers'); + +const { impersonate } = require('../../helpers/account'); +const { MAX_UINT48 } = require('../../helpers/constants'); +const { bigint: time } = require('../../helpers/time'); const { selector } = require('../../helpers/methods'); + const { buildBaseRoles, formatAccess, @@ -26,17 +33,7 @@ const { testAsGetAccess, } = require('./AccessManager.predicate'); -const { - time: { increase }, - getStorageAt, - loadFixture, -} = require('@nomicfoundation/hardhat-network-helpers'); -const { MAX_UINT48 } = require('../../helpers/constants'); -const { impersonate } = require('../../helpers/account'); -const { bigint: time } = require('../../helpers/time'); -const { ZeroAddress: ZERO_ADDRESS, Wallet, toBeHex, id } = require('ethers'); - -const { address: someAddress } = Wallet.createRandom(); +const { address: someAddress } = ethers.Wallet.createRandom(); async function fixture() { const [admin, roleAdmin, roleGuardian, member, user, other] = await ethers.getSigners(); @@ -118,7 +115,7 @@ contract('AccessManager', function () { it('rejects zero address for initialAdmin', async function () { await expect(ethers.deployContract('$AccessManager', [ethers.ZeroAddress])) .to.be.revertedWithCustomError(this.manager, 'AccessManagerInvalidInitialAdmin') - .withArgs(ZERO_ADDRESS); + .withArgs(ethers.ZeroAddress); }); it('initializes setup roles correctly', async function () { @@ -759,7 +756,7 @@ contract('AccessManager', function () { describe('when is not scheduled', function () { it('returns default 0', async function () { - expect(await this.manager.getNonce(id('operation'))).to.equal(0n); + expect(await this.manager.getNonce(ethers.id('operation'))).to.equal(0n); }); }); }); @@ -912,7 +909,7 @@ contract('AccessManager', function () { beforeEach('sets old delay', async function () { this.role = this.roles.SOME; await this.manager.$_setGrantDelay(this.role.id, oldDelay); - await increase(MINSETBACK); + await time.increaseBy.timestamp(MINSETBACK); expect(await this.manager.getRoleGrantDelay(this.role.id)).to.equal(oldDelay); }); @@ -924,7 +921,7 @@ contract('AccessManager', function () { .withArgs(this.role.id, newDelay, setGrantDelayAt + MINSETBACK); expect(await this.manager.getRoleGrantDelay(this.role.id)).to.equal(oldDelay); - await increase(MINSETBACK); + await time.increaseBy.timestamp(MINSETBACK); expect(await this.manager.getRoleGrantDelay(this.role.id)).to.equal(newDelay); }); }); @@ -935,7 +932,7 @@ contract('AccessManager', function () { beforeEach('sets old delay', async function () { this.role = this.roles.SOME; await this.manager.$_setGrantDelay(this.role.id, oldDelay); - await increase(MINSETBACK); + await time.increaseBy.timestamp(MINSETBACK); expect(await this.manager.getRoleGrantDelay(this.role.id)).to.equal(oldDelay); }); @@ -950,7 +947,7 @@ contract('AccessManager', function () { .withArgs(this.role.id, newDelay, setGrantDelayAt + MINSETBACK); expect(await this.manager.getRoleGrantDelay(this.role.id)).to.equal(oldDelay); - await increase(MINSETBACK); + await time.increaseBy.timestamp(MINSETBACK); expect(await this.manager.getRoleGrantDelay(this.role.id)).to.equal(newDelay); }); }); @@ -973,7 +970,7 @@ contract('AccessManager', function () { .withArgs(this.role.id, newDelay, setGrantDelayAt + setback); expect(await this.manager.getRoleGrantDelay(this.role.id)).to.equal(oldDelay); - await increase(setback); + await time.increaseBy.timestamp(setback); expect(await this.manager.getRoleGrantDelay(this.role.id)).to.equal(newDelay); }); }); @@ -998,7 +995,7 @@ contract('AccessManager', function () { beforeEach('sets old delay', async function () { await this.manager.$_setTargetAdminDelay(target, oldDelay); - await increase(MINSETBACK); + await time.increaseBy.timestamp(MINSETBACK); expect(await this.manager.getTargetAdminDelay(target)).to.equal(oldDelay); }); @@ -1010,7 +1007,7 @@ contract('AccessManager', function () { .withArgs(target, newDelay, setTargetAdminDelayAt + MINSETBACK); expect(await this.manager.getTargetAdminDelay(target)).to.equal(oldDelay); - await increase(MINSETBACK); + await time.increaseBy.timestamp(MINSETBACK); expect(await this.manager.getTargetAdminDelay(target)).to.equal(newDelay); }); }); @@ -1021,7 +1018,7 @@ contract('AccessManager', function () { beforeEach('sets old delay', async function () { await this.manager.$_setTargetAdminDelay(target, oldDelay); - await increase(MINSETBACK); + await time.increaseBy.timestamp(MINSETBACK); expect(await this.manager.getTargetAdminDelay(target)).to.equal(oldDelay); }); @@ -1036,7 +1033,7 @@ contract('AccessManager', function () { .withArgs(target, newDelay, setTargetAdminDelayAt + MINSETBACK); expect(await this.manager.getTargetAdminDelay(target)).to.equal(oldDelay); - await increase(MINSETBACK); + await time.increaseBy.timestamp(MINSETBACK); expect(await this.manager.getTargetAdminDelay(target)).to.equal(newDelay); }); }); @@ -1059,7 +1056,7 @@ contract('AccessManager', function () { .withArgs(target, newDelay, setTargetAdminDelayAt + setback); expect(await this.manager.getTargetAdminDelay(target)).to.equal(oldDelay); - await increase(setback); + await time.increaseBy.timestamp(setback); expect(await this.manager.getTargetAdminDelay(target)).to.equal(newDelay); }); }); @@ -1205,7 +1202,7 @@ contract('AccessManager', function () { // Delay granting this.grantDelay = time.duration.weeks(2); await this.manager.$_setGrantDelay(ANOTHER_ROLE, this.grantDelay); - await increase(MINSETBACK); + await time.increaseBy.timestamp(MINSETBACK); // Grant role this.executionDelay = time.duration.days(3); @@ -1279,7 +1276,7 @@ contract('AccessManager', function () { // Delay granting this.grantDelay = 0; await this.manager.$_setGrantDelay(ANOTHER_ROLE, this.grantDelay); - await increase(MINSETBACK); + await time.increaseBy.timestamp(MINSETBACK); }); it('immediately grants the role to the user', async function () { @@ -1326,7 +1323,7 @@ contract('AccessManager', function () { // Delay granting const grantDelay = time.duration.weeks(2); await this.manager.$_setGrantDelay(ANOTHER_ROLE, grantDelay); - await increase(MINSETBACK); + await time.increaseBy.timestamp(MINSETBACK); }); describe('when increasing the execution delay', function () { @@ -1443,7 +1440,7 @@ contract('AccessManager', function () { // Delay granting const grantDelay = 0; await this.manager.$_setGrantDelay(ANOTHER_ROLE, grantDelay); - await increase(MINSETBACK); + await time.increaseBy.timestamp(MINSETBACK); }); describe('when increasing the execution delay', function () { @@ -1942,7 +1939,7 @@ contract('AccessManager', function () { expect(expectedOperationId).to.equal(op1.operationId); // Consume - await increase(this.delay); + await time.increaseBy.timestamp(this.delay); await this.manager.$_consumeScheduledOp(expectedOperationId); // Check nonce @@ -2166,7 +2163,7 @@ contract('AccessManager', function () { delay, }); await schedule(); - await increase(delay); + await time.increaseBy.timestamp(delay); await expect(this.manager.connect(this.caller).execute(this.target, this.calldata)) .to.emit(this.manager, 'OperationExecuted') .withArgs(operationId, 1n); @@ -2191,7 +2188,7 @@ contract('AccessManager', function () { // remove the execution delay await this.manager.$_grantRole(this.role.id, this.caller, 0, 0); - await increase(delay); + await time.increaseBy.timestamp(delay); await expect(this.manager.connect(this.caller).execute(this.target, this.calldata)) .to.emit(this.manager, 'OperationExecuted') .withArgs(operationId, 1n); @@ -2217,7 +2214,7 @@ contract('AccessManager', function () { delay, }); await schedule(); - await increase(delay); + await time.increaseBy.timestamp(delay); await this.manager.connect(this.caller).execute(this.target, this.calldata); await expect(this.manager.connect(this.caller).execute(this.target, this.calldata)) .to.be.revertedWithCustomError(this.manager, 'AccessManagerNotScheduled') @@ -2241,7 +2238,7 @@ contract('AccessManager', function () { describe('when caller is not consuming scheduled operation', function () { beforeEach('set consuming false', async function () { - await this.target.setIsConsumingScheduledOp(false, toBeHex(CONSUMING_SCHEDULE_STORAGE_SLOT, 32)); + await this.target.setIsConsumingScheduledOp(false, ethers.toBeHex(CONSUMING_SCHEDULE_STORAGE_SLOT, 32)); }); it('reverts as AccessManagerUnauthorizedConsume', async function () { @@ -2253,7 +2250,7 @@ contract('AccessManager', function () { describe('when caller is consuming scheduled operation', function () { beforeEach('set consuming true', async function () { - await this.target.setIsConsumingScheduledOp(true, toBeHex(CONSUMING_SCHEDULE_STORAGE_SLOT, 32)); + await this.target.setIsConsumingScheduledOp(true, ethers.toBeHex(CONSUMING_SCHEDULE_STORAGE_SLOT, 32)); }); testAsSchedulableOperation({ diff --git a/test/access/manager/AuthorityUtils.test.js b/test/access/manager/AuthorityUtils.test.js index 6c353f20609..c17220541da 100644 --- a/test/access/manager/AuthorityUtils.test.js +++ b/test/access/manager/AuthorityUtils.test.js @@ -1,5 +1,5 @@ -const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); async function fixture() { const [user, other] = await ethers.getSigners(); diff --git a/test/finance/VestingWallet.behavior.js b/test/finance/VestingWallet.behavior.js index d4863bc52d5..53c8c565c71 100644 --- a/test/finance/VestingWallet.behavior.js +++ b/test/finance/VestingWallet.behavior.js @@ -4,7 +4,7 @@ const { bigint: time } = require('../helpers/time'); function shouldBehaveLikeVesting() { it('check vesting schedule', async function () { for (const timestamp of this.schedule) { - await time.forward.timestamp(timestamp); + await time.increaseTo.timestamp(timestamp); const vesting = this.vestingFn(timestamp); expect(await this.mock.vestedAmount(...this.args, timestamp)).to.be.equal(vesting); @@ -24,7 +24,7 @@ function shouldBehaveLikeVesting() { } for (const timestamp of this.schedule) { - await time.forward.timestamp(timestamp, false); + await time.increaseTo.timestamp(timestamp, false); const vested = this.vestingFn(timestamp); const tx = await this.mock.release(...this.args); @@ -39,7 +39,7 @@ function shouldBehaveLikeVesting() { const { args, error } = await this.setupFailure(); for (const timestamp of this.schedule) { - await time.forward.timestamp(timestamp); + await time.increaseTo.timestamp(timestamp); await expect(this.mock.release(...args)).to.be.revertedWithCustomError(...error); } diff --git a/test/finance/VestingWallet.test.js b/test/finance/VestingWallet.test.js index fa60faea723..843918fee10 100644 --- a/test/finance/VestingWallet.test.js +++ b/test/finance/VestingWallet.test.js @@ -1,8 +1,9 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { bigint: time } = require('../helpers/time'); + const { min } = require('../helpers/math'); +const { bigint: time } = require('../helpers/time'); const { shouldBehaveLikeVesting } = require('./VestingWallet.behavior'); diff --git a/test/governance/TimelockController.test.js b/test/governance/TimelockController.test.js index 9d3f5188b41..709104743e0 100644 --- a/test/governance/TimelockController.test.js +++ b/test/governance/TimelockController.test.js @@ -329,7 +329,7 @@ describe('TimelockController', function () { it('revert if execution comes too early 2/2', async function () { // -1 is too tight, test sometime fails - await this.mock.getTimestamp(this.operation.id).then(clock => time.forward.timestamp(clock - 5n)); + await this.mock.getTimestamp(this.operation.id).then(clock => time.increaseTo.timestamp(clock - 5n)); await expect( this.mock @@ -348,7 +348,7 @@ describe('TimelockController', function () { describe('on time', function () { beforeEach(async function () { - await this.mock.getTimestamp(this.operation.id).then(clock => time.forward.timestamp(clock)); + await this.mock.getTimestamp(this.operation.id).then(time.increaseTo.timestamp); }); it('executor can reveal', async function () { @@ -407,7 +407,7 @@ describe('TimelockController', function () { ); // Advance on time to make the operation executable - await this.mock.getTimestamp(reentrantOperation.id).then(clock => time.forward.timestamp(clock)); + await this.mock.getTimestamp(reentrantOperation.id).then(time.increaseTo.timestamp); // Grant executor role to the reentrant contract await this.mock.connect(this.admin).grantRole(EXECUTOR_ROLE, reentrant); @@ -667,7 +667,7 @@ describe('TimelockController', function () { it('revert if execution comes too early 2/2', async function () { // -1 is to tight, test sometime fails - await this.mock.getTimestamp(this.operation.id).then(clock => time.forward.timestamp(clock - 5n)); + await this.mock.getTimestamp(this.operation.id).then(clock => time.increaseTo.timestamp(clock - 5n)); await expect( this.mock @@ -686,7 +686,7 @@ describe('TimelockController', function () { describe('on time', function () { beforeEach(async function () { - await this.mock.getTimestamp(this.operation.id).then(clock => time.forward.timestamp(clock)); + await this.mock.getTimestamp(this.operation.id).then(time.increaseTo.timestamp); }); it('executor can reveal', async function () { @@ -800,7 +800,7 @@ describe('TimelockController', function () { ); // Advance on time to make the operation executable - await this.mock.getTimestamp(reentrantBatchOperation.id).then(clock => time.forward.timestamp(clock)); + await this.mock.getTimestamp(reentrantBatchOperation.id).then(time.increaseTo.timestamp); // Grant executor role to the reentrant contract await this.mock.connect(this.admin).grantRole(EXECUTOR_ROLE, reentrant); @@ -883,7 +883,7 @@ describe('TimelockController', function () { MINDELAY, ); - await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp); await expect( this.mock @@ -965,7 +965,7 @@ describe('TimelockController', function () { .connect(this.proposer) .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); - await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp); await expect( this.mock @@ -1016,7 +1016,7 @@ describe('TimelockController', function () { MINDELAY, ); - await this.mock.getTimestamp(this.operation2.id).then(clock => time.forward.timestamp(clock)); + await this.mock.getTimestamp(this.operation2.id).then(time.increaseTo.timestamp); }); it('cannot execute before dependency', async function () { @@ -1073,7 +1073,7 @@ describe('TimelockController', function () { .connect(this.proposer) .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); - await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp); await this.mock .connect(this.executor) @@ -1095,7 +1095,7 @@ describe('TimelockController', function () { .connect(this.proposer) .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); - await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp); await expect( this.mock @@ -1117,7 +1117,7 @@ describe('TimelockController', function () { .connect(this.proposer) .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); - await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp); // Targeted function reverts with a panic code (0x1) + the timelock bubble the panic code await expect( @@ -1140,7 +1140,7 @@ describe('TimelockController', function () { .connect(this.proposer) .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); - await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp); await expect( this.mock @@ -1164,7 +1164,7 @@ describe('TimelockController', function () { .connect(this.proposer) .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); - await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp); expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n); @@ -1192,7 +1192,7 @@ describe('TimelockController', function () { .connect(this.proposer) .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); - await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp); expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n); @@ -1220,7 +1220,7 @@ describe('TimelockController', function () { .connect(this.proposer) .schedule(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, MINDELAY); - await this.mock.getTimestamp(operation.id).then(clock => time.forward.timestamp(clock)); + await this.mock.getTimestamp(operation.id).then(time.increaseTo.timestamp); expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); expect(await ethers.provider.getBalance(this.callreceivermock)).to.equal(0n); diff --git a/test/governance/extensions/GovernorTimelockAccess.test.js b/test/governance/extensions/GovernorTimelockAccess.test.js index 2f16d1b991e..a984a7f5e3f 100644 --- a/test/governance/extensions/GovernorTimelockAccess.test.js +++ b/test/governance/extensions/GovernorTimelockAccess.test.js @@ -595,7 +595,7 @@ describe('GovernorTimelockAccess', function () { .to.emit(this.mock, 'ProposalCanceled') .withArgs(original.currentProposal.id); - await time.clock.timestamp().then(clock => time.forward.timestamp(max(clock + 1n, eta))); + await time.clock.timestamp().then(clock => time.increaseTo.timestamp(max(clock + 1n, eta))); await expect(original.execute()) .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') @@ -621,7 +621,7 @@ describe('GovernorTimelockAccess', function () { .to.emit(this.mock, 'ProposalCanceled') .withArgs(this.proposal.id); - await time.clock.timestamp().then(clock => time.forward.timestamp(max(clock + 1n, eta))); + await time.clock.timestamp().then(clock => time.increaseTo.timestamp(max(clock + 1n, eta))); await expect(this.helper.execute()) .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') @@ -639,14 +639,11 @@ describe('GovernorTimelockAccess', function () { await this.helper.waitForSnapshot(); await this.helper.connect(this.voter1).vote({ support: Enums.VoteType.For }); await this.helper.waitForDeadline(); - // await this.helper.queue(); - // const eta = await this.mock.proposalEta(this.proposal.id); await expect(this.helper.cancel('internal')) .to.emit(this.mock, 'ProposalCanceled') .withArgs(this.proposal.id); - // await time.forward.timestamp(eta); await expect(this.helper.execute()) .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') .withArgs( diff --git a/test/governance/extensions/GovernorTimelockControl.test.js b/test/governance/extensions/GovernorTimelockControl.test.js index 9f6bceb5baa..71c2b188c20 100644 --- a/test/governance/extensions/GovernorTimelockControl.test.js +++ b/test/governance/extensions/GovernorTimelockControl.test.js @@ -374,7 +374,7 @@ describe('GovernorTimelockControl', function () { await this.timelock.connect(this.owner).schedule(...call, delay); - await time.clock.timestamp().then(clock => time.forward.timestamp(clock + delay)); + await time.increaseBy.timestamp(delay); // Error bubbled up from Governor await expect(this.timelock.connect(this.owner).execute(...call)).to.be.revertedWithCustomError( diff --git a/test/governance/utils/Votes.behavior.js b/test/governance/utils/Votes.behavior.js index a08f184c8ac..be3a87db58a 100644 --- a/test/governance/utils/Votes.behavior.js +++ b/test/governance/utils/Votes.behavior.js @@ -2,8 +2,8 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { mine } = require('@nomicfoundation/hardhat-network-helpers'); -const { bigint: time } = require('../../helpers/time'); const { getDomain, Delegation } = require('../../helpers/eip712'); +const { bigint: time } = require('../../helpers/time'); const { shouldBehaveLikeERC6372 } = require('./ERC6372.behavior'); diff --git a/test/governance/utils/Votes.test.js b/test/governance/utils/Votes.test.js index dda5e5c8251..c133609bc7b 100644 --- a/test/governance/utils/Votes.test.js +++ b/test/governance/utils/Votes.test.js @@ -2,9 +2,9 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { bigint: time } = require('../../helpers/time'); const { sum } = require('../../helpers/math'); const { zip } = require('../../helpers/iterate'); +const { bigint: time } = require('../../helpers/time'); const { shouldBehaveLikeVotes } = require('./Votes.behavior'); diff --git a/test/helpers/access-manager.js b/test/helpers/access-manager.js index 5f55dd51876..e08b48d4e73 100644 --- a/test/helpers/access-manager.js +++ b/test/helpers/access-manager.js @@ -1,9 +1,7 @@ -const { - bigint: { MAX_UINT64 }, -} = require('./constants'); +const { ethers } = require('hardhat'); +const { MAX_UINT64 } = require('./constants'); const { namespaceSlot } = require('./namespaced-storage'); const { bigint: time } = require('./time'); -const { keccak256, AbiCoder } = require('ethers'); function buildBaseRoles() { const roles = { @@ -54,9 +52,8 @@ const CONSUMING_SCHEDULE_STORAGE_SLOT = namespaceSlot('AccessManaged', 0n); * @requires this.{manager, caller, target, calldata} */ async function prepareOperation(manager, { caller, target, calldata, delay }) { - const timestamp = await time.clock.timestamp(); - const scheduledAt = timestamp + 1n; - await time.forward.timestamp(scheduledAt, false); // Fix next block timestamp for predictability + const scheduledAt = (await time.clock.timestamp()) + 1n; + await time.increaseTo.timestamp(scheduledAt, false); // Fix next block timestamp for predictability return { schedule: () => manager.connect(caller).schedule(target, calldata, scheduledAt + delay), @@ -68,8 +65,8 @@ async function prepareOperation(manager, { caller, target, calldata, delay }) { const lazyGetAddress = addressable => addressable.address ?? addressable.target ?? addressable; const hashOperation = (caller, target, data) => - keccak256( - AbiCoder.defaultAbiCoder().encode( + ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( ['address', 'address', 'bytes'], [lazyGetAddress(caller), lazyGetAddress(target), data], ), diff --git a/test/helpers/constants.js b/test/helpers/constants.js index 17937bee108..4dfda5eabae 100644 --- a/test/helpers/constants.js +++ b/test/helpers/constants.js @@ -1,12 +1,4 @@ -// TODO: deprecate the old version in favor of this one -const bigint = { +module.exports = { MAX_UINT48: 2n ** 48n - 1n, MAX_UINT64: 2n ** 64n - 1n, }; - -// TODO: remove toString() when bigint are supported -module.exports = { - MAX_UINT48: bigint.MAX_UINT48.toString(), - MAX_UINT64: bigint.MAX_UINT64.toString(), - bigint, -}; diff --git a/test/helpers/customError.js b/test/helpers/customError.js deleted file mode 100644 index acc3214eb80..00000000000 --- a/test/helpers/customError.js +++ /dev/null @@ -1,45 +0,0 @@ -// DEPRECATED: replace with hardhat-toolbox chai matchers. - -const { expect } = require('chai'); - -/** Revert handler that supports custom errors. */ -async function expectRevertCustomError(promise, expectedErrorName, args) { - if (!Array.isArray(args)) { - expect.fail('Expected 3rd array parameter for error arguments'); - } - - await promise.then( - () => expect.fail("Expected promise to throw but it didn't"), - ({ message }) => { - // The revert message for custom errors looks like: - // VM Exception while processing transaction: - // reverted with custom error 'InvalidAccountNonce("0x70997970C51812dc3A010C7d01b50e0d17dc79C8", 0)' - - // Attempt to parse as a custom error - const match = message.match(/custom error '(?\w+)\((?.*)\)'/); - if (!match) { - expect.fail(`Could not parse as custom error. ${message}`); - } - // Extract the error name and parameters - const errorName = match.groups.name; - const argMatches = [...match.groups.args.matchAll(/-?\w+/g)]; - - // Assert error name - expect(errorName).to.be.equal( - expectedErrorName, - `Unexpected custom error name (with found args: [${argMatches.map(([a]) => a)}])`, - ); - - // Coerce to string for comparison since `arg` can be either a number or hex. - const sanitizedExpected = args.map(arg => arg.toString().toLowerCase()); - const sanitizedActual = argMatches.map(([arg]) => arg.toString().toLowerCase()); - - // Assert argument equality - expect(sanitizedActual).to.have.members(sanitizedExpected, `Unexpected ${errorName} arguments`); - }, - ); -} - -module.exports = { - expectRevertCustomError, -}; diff --git a/test/helpers/governance.js b/test/helpers/governance.js index c2e79461a16..0efb3da5c5c 100644 --- a/test/helpers/governance.js +++ b/test/helpers/governance.js @@ -1,7 +1,7 @@ const { ethers } = require('hardhat'); -const { forward } = require('./time'); const { ProposalState } = require('./enums'); const { unique } = require('./iterate'); +const time = require('./time'); const timelockSalt = (address, descriptionHash) => ethers.toBeHex((ethers.toBigInt(address) << 96n) ^ ethers.toBigInt(descriptionHash), 32); @@ -131,17 +131,17 @@ class GovernorHelper { /// Clock helpers async waitForSnapshot(offset = 0n) { const timepoint = await this.governor.proposalSnapshot(this.id); - return forward[this.mode](timepoint + offset); + return time.increaseTo[this.mode](timepoint + offset); } async waitForDeadline(offset = 0n) { const timepoint = await this.governor.proposalDeadline(this.id); - return forward[this.mode](timepoint + offset); + return time.increaseTo[this.mode](timepoint + offset); } async waitForEta(offset = 0n) { const timestamp = await this.governor.proposalEta(this.id); - return forward.timestamp(timestamp + offset); + return time.increaseTo.timestamp(timestamp + offset); } /// Other helpers diff --git a/test/helpers/namespaced-storage.js b/test/helpers/namespaced-storage.js index 9fa70411363..eccec3b52c0 100644 --- a/test/helpers/namespaced-storage.js +++ b/test/helpers/namespaced-storage.js @@ -1,21 +1,15 @@ -const { keccak256, id, toBeHex, MaxUint256 } = require('ethers'); -const { artifacts } = require('hardhat'); +const { ethers, artifacts } = require('hardhat'); +const { erc7201slot } = require('./erc1967'); function namespaceId(contractName) { return `openzeppelin.storage.${contractName}`; } -function namespaceLocation(value) { - const hashIdBN = BigInt(id(value)) - 1n; // keccak256(id) - 1 - const mask = MaxUint256 - 0xffn; // ~0xff - return BigInt(keccak256(toBeHex(hashIdBN, 32))) & mask; -} - function namespaceSlot(contractName, offset) { try { // Try to get the artifact paths, will throw if it doesn't exist artifacts._getArtifactPathSync(`${contractName}Upgradeable`); - return offset + namespaceLocation(namespaceId(contractName)); + return offset + ethers.toBigInt(erc7201slot(namespaceId(contractName))); } catch (_) { return offset; } @@ -23,6 +17,6 @@ function namespaceSlot(contractName, offset) { module.exports = { namespaceSlot, - namespaceLocation, + namespaceLocation: erc7201slot, namespaceId, }; diff --git a/test/helpers/time.js b/test/helpers/time.js index 5f85b69158a..db72f206367 100644 --- a/test/helpers/time.js +++ b/test/helpers/time.js @@ -1,27 +1,39 @@ const { ethers } = require('hardhat'); -const { time, mineUpTo } = require('@nomicfoundation/hardhat-network-helpers'); +const { time, mine, mineUpTo } = require('@nomicfoundation/hardhat-network-helpers'); const { mapValues } = require('./iterate'); +const clock = { + blocknumber: () => time.latestBlock(), + timestamp: () => time.latest(), +}; +const clockFromReceipt = { + blocknumber: receipt => Promise.resolve(receipt.blockNumber), + timestamp: receipt => ethers.provider.getBlock(receipt.blockNumber).then(block => block.timestamp), +}; +const increaseBy = { + blockNumber: mine, + timestamp: (delay, mine = true) => + time.latest().then(clock => increaseTo.timestamp(clock + ethers.toNumber(delay), mine)), +}; +const increaseTo = { + blocknumber: mineUpTo, + timestamp: (to, mine = true) => (mine ? time.increaseTo(to) : time.setNextBlockTimestamp(to)), +}; +const duration = time.duration; + module.exports = { - clock: { - blocknumber: () => time.latestBlock(), - timestamp: () => time.latest(), - }, - clockFromReceipt: { - blocknumber: receipt => Promise.resolve(receipt.blockNumber), - timestamp: receipt => ethers.provider.getBlock(receipt.blockNumber).then(block => block.timestamp), - }, - forward: { - blocknumber: mineUpTo, - timestamp: (to, mine = true) => (mine ? time.increaseTo(to) : time.setNextBlockTimestamp(to)), - }, - duration: time.duration, + clock, + clockFromReceipt, + increaseBy, + increaseTo, + duration, }; // TODO: deprecate the old version in favor of this one module.exports.bigint = { - clock: mapValues(module.exports.clock, fn => () => fn().then(ethers.toBigInt)), - clockFromReceipt: mapValues(module.exports.clockFromReceipt, fn => receipt => fn(receipt).then(ethers.toBigInt)), - forward: module.exports.forward, - duration: mapValues(module.exports.duration, fn => n => ethers.toBigInt(fn(ethers.toNumber(n)))), + clock: mapValues(clock, fn => () => fn().then(ethers.toBigInt)), + clockFromReceipt: mapValues(clockFromReceipt, fn => receipt => fn(receipt).then(ethers.toBigInt)), + increaseBy: increaseBy, + increaseTo: increaseTo, + duration: mapValues(duration, fn => n => ethers.toBigInt(fn(ethers.toNumber(n)))), }; diff --git a/test/proxy/utils/Initializable.test.js b/test/proxy/utils/Initializable.test.js index bc26e6b60a5..6bf213f0d9d 100644 --- a/test/proxy/utils/Initializable.test.js +++ b/test/proxy/utils/Initializable.test.js @@ -1,8 +1,6 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); -const { - bigint: { MAX_UINT64 }, -} = require('../../helpers/constants'); +const { MAX_UINT64 } = require('../../helpers/constants'); describe('Initializable', function () { describe('basic testing without inheritance', function () { diff --git a/test/token/ERC1155/ERC1155.behavior.js b/test/token/ERC1155/ERC1155.behavior.js index 4c81ea9d1ed..9ae706f8496 100644 --- a/test/token/ERC1155/ERC1155.behavior.js +++ b/test/token/ERC1155/ERC1155.behavior.js @@ -1,6 +1,7 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); + const { bigint: { RevertType }, } = require('../../helpers/enums'); diff --git a/test/token/ERC1155/ERC1155.test.js b/test/token/ERC1155/ERC1155.test.js index c469dd84584..486d1aec9ec 100644 --- a/test/token/ERC1155/ERC1155.test.js +++ b/test/token/ERC1155/ERC1155.test.js @@ -1,8 +1,8 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { zip } = require('../../helpers/iterate'); +const { zip } = require('../../helpers/iterate'); const { shouldBehaveLikeERC1155 } = require('./ERC1155.behavior'); const initialURI = 'https://token-cdn-domain/{id}.json'; diff --git a/test/token/ERC20/extensions/ERC20Burnable.test.js b/test/token/ERC20/extensions/ERC20Burnable.test.js index 8253acbf15f..9fa02e44f3e 100644 --- a/test/token/ERC20/extensions/ERC20Burnable.test.js +++ b/test/token/ERC20/extensions/ERC20Burnable.test.js @@ -1,4 +1,5 @@ const { ethers } = require('hardhat'); +const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const name = 'My Token'; diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 6e4fa3b9c5a..dda94f8d361 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -2,6 +2,7 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); + const { min, max } = require('../../helpers/math'); const { bigint: { Rounding }, diff --git a/test/utils/math/SafeCast.test.js b/test/utils/math/SafeCast.test.js index dd04f75ba1a..a69e75c9960 100644 --- a/test/utils/math/SafeCast.test.js +++ b/test/utils/math/SafeCast.test.js @@ -1,6 +1,7 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + const { range } = require('../../../scripts/helpers'); async function fixture() { diff --git a/test/utils/math/SignedMath.test.js b/test/utils/math/SignedMath.test.js index 253e7235752..51aa5d8fba6 100644 --- a/test/utils/math/SignedMath.test.js +++ b/test/utils/math/SignedMath.test.js @@ -1,6 +1,7 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + const { min, max } = require('../../helpers/math'); async function testCommutative(fn, lhs, rhs, expected, ...extra) { diff --git a/test/utils/structs/BitMap.test.js b/test/utils/structs/BitMap.test.js index 133f1f734b3..a7685414e5b 100644 --- a/test/utils/structs/BitMap.test.js +++ b/test/utils/structs/BitMap.test.js @@ -1,5 +1,5 @@ -const { expect } = require('chai'); const { ethers } = require('hardhat'); +const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); async function fixture() { diff --git a/test/utils/structs/Checkpoints.test.js b/test/utils/structs/Checkpoints.test.js index c5b9e65a05e..2c15e082d48 100644 --- a/test/utils/structs/Checkpoints.test.js +++ b/test/utils/structs/Checkpoints.test.js @@ -1,5 +1,5 @@ -const { expect } = require('chai'); const { ethers } = require('hardhat'); +const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { VALUE_SIZES } = require('../../../scripts/generate/templates/Checkpoints.opts.js'); diff --git a/test/utils/structs/DoubleEndedQueue.test.js b/test/utils/structs/DoubleEndedQueue.test.js index 92d9f530c1e..1f6e782b5bd 100644 --- a/test/utils/structs/DoubleEndedQueue.test.js +++ b/test/utils/structs/DoubleEndedQueue.test.js @@ -1,5 +1,5 @@ -const { expect } = require('chai'); const { ethers } = require('hardhat'); +const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); async function fixture() { diff --git a/test/utils/structs/EnumerableMap.behavior.js b/test/utils/structs/EnumerableMap.behavior.js index 9c675c62d5c..39e74a68e80 100644 --- a/test/utils/structs/EnumerableMap.behavior.js +++ b/test/utils/structs/EnumerableMap.behavior.js @@ -1,5 +1,5 @@ -const { expect } = require('chai'); const { ethers } = require('hardhat'); +const { expect } = require('chai'); const zip = (array1, array2) => array1.map((item, index) => [item, array2[index]]); diff --git a/test/utils/structs/EnumerableMap.test.js b/test/utils/structs/EnumerableMap.test.js index 183a8c812d5..6df9871aecc 100644 --- a/test/utils/structs/EnumerableMap.test.js +++ b/test/utils/structs/EnumerableMap.test.js @@ -1,5 +1,6 @@ const { ethers } = require('hardhat'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + const { mapValues } = require('../../helpers/iterate'); const { randomArray, generators } = require('../../helpers/random'); const { TYPES, formatType } = require('../../../scripts/generate/templates/EnumerableMap.opts'); diff --git a/test/utils/structs/EnumerableSet.test.js b/test/utils/structs/EnumerableSet.test.js index 4345dfe7d14..db6c5a4536f 100644 --- a/test/utils/structs/EnumerableSet.test.js +++ b/test/utils/structs/EnumerableSet.test.js @@ -1,5 +1,6 @@ const { ethers } = require('hardhat'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + const { mapValues } = require('../../helpers/iterate'); const { randomArray, generators } = require('../../helpers/random'); const { TYPES } = require('../../../scripts/generate/templates/EnumerableSet.opts'); diff --git a/test/utils/types/Time.test.js b/test/utils/types/Time.test.js index c55a769f985..171a8452638 100644 --- a/test/utils/types/Time.test.js +++ b/test/utils/types/Time.test.js @@ -1,12 +1,12 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { - bigint: { clock }, -} = require('../../helpers/time'); const { product } = require('../../helpers/iterate'); const { max } = require('../../helpers/math'); +const { + bigint: { clock }, +} = require('../../helpers/time'); const MAX_UINT32 = 1n << (32n - 1n); const MAX_UINT48 = 1n << (48n - 1n); From abcf9dd8b78ca81ac0c3571a6ce9831235ff1b4c Mon Sep 17 00:00:00 2001 From: cairo <101215230+cairoeth@users.noreply.github.com> Date: Fri, 22 Dec 2023 22:52:00 +0100 Subject: [PATCH 44/44] Replace Defender Admin with Transaction Proposals (#4804) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- README.md | 2 +- docs/modules/ROOT/pages/governance.adoc | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9ca41573f91..06f54553a7c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ :mage: **Not sure how to get started?** Check out [Contracts Wizard](https://wizard.openzeppelin.com/) — an interactive smart contract generator. -:building_construction: **Want to scale your decentralized application?** Check out [OpenZeppelin Defender](https://openzeppelin.com/defender) — a secure platform for automating and monitoring your operations. +:building_construction: **Want to scale your decentralized application?** Check out [OpenZeppelin Defender](https://openzeppelin.com/defender) — a mission-critical developer security platform to code, audit, deploy, monitor, and operate with confidence. > [!IMPORTANT] > OpenZeppelin Contracts uses semantic versioning to communicate backwards compatibility of its API and storage layout. For upgradeable contracts, the storage layout of different major versions should be assumed incompatible, for example, it is unsafe to upgrade from 4.9.3 to 5.0.0. Learn more at [Backwards Compatibility](https://docs.openzeppelin.com/contracts/backwards-compatibility). diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index fda51e6ff05..27efeaf9aaa 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -32,7 +32,7 @@ When using a timelock with your Governor contract, you can use either OpenZeppel https://www.tally.xyz[Tally] is a full-fledged application for user owned on-chain governance. It comprises a voting dashboard, proposal creation wizard, real time research and analysis, and educational content. -For all of these options, the Governor will be compatible with Tally: users will be able to create proposals, see voting periods and delays following xref:api:interfaces.adoc#IERC6372[IERC6372], visualize voting power and advocates, navigate proposals, and cast votes. For proposal creation in particular, projects can also use Defender Admin as an alternative interface. +For all of these options, the Governor will be compatible with Tally: users will be able to create proposals, see voting periods and delays following xref:api:interfaces.adoc#IERC6372[IERC6372], visualize voting power and advocates, navigate proposals, and cast votes. For proposal creation in particular, projects can also use https://docs.openzeppelin.com/defender/module/actions#transaction-proposals-reference[Defender Transaction Proposals] as an alternative interface. In the rest of this guide, we will focus on a fresh deploy of the vanilla OpenZeppelin Governor features without concern for compatibility with GovernorAlpha or Bravo. @@ -102,7 +102,7 @@ A proposal is a sequence of actions that the Governor contract will perform if i Let’s say we want to create a proposal to give a team a grant, in the form of ERC-20 tokens from the governance treasury. This proposal will consist of a single action where the target is the ERC-20 token, calldata is the encoded function call `transfer(, )`, and with 0 ETH attached. -Generally a proposal will be created with the help of an interface such as Tally or Defender. Here we will show how to create the proposal using Ethers.js. +Generally a proposal will be created with the help of an interface such as Tally or https://docs.openzeppelin.com/defender/module/actions#transaction-proposals-reference[Defender Proposals]. Here we will show how to create the proposal using Ethers.js. First we get all the parameters necessary for the proposal action.