diff --git a/.changeset/blue-horses-do.md b/.changeset/blue-horses-do.md new file mode 100644 index 00000000000..9df604fe448 --- /dev/null +++ b/.changeset/blue-horses-do.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`ERC2771Forwarder`: Added `deadline` for expiring transactions, batching, and more secure handling of `msg.value`. diff --git a/.changeset/blue-scissors-design.md b/.changeset/blue-scissors-design.md new file mode 100644 index 00000000000..c2f815aae4f --- /dev/null +++ b/.changeset/blue-scissors-design.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Math`: Make `ceilDiv` to revert on 0 division even if the numerator is 0 diff --git a/.changeset/flat-bottles-wonder.md b/.changeset/flat-bottles-wonder.md new file mode 100644 index 00000000000..099ea833983 --- /dev/null +++ b/.changeset/flat-bottles-wonder.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +Replace some uses of `abi.encodePacked` with clearer alternatives (e.g. `bytes.concat`, `string.concat`). diff --git a/.changeset/grumpy-bulldogs-call.md b/.changeset/grumpy-bulldogs-call.md new file mode 100644 index 00000000000..c034587f34a --- /dev/null +++ b/.changeset/grumpy-bulldogs-call.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`Governor`: Optimized use of storage for proposal data diff --git a/.changeset/grumpy-worms-tease.md b/.changeset/grumpy-worms-tease.md new file mode 100644 index 00000000000..910b996c604 --- /dev/null +++ b/.changeset/grumpy-worms-tease.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`ERC1967Utils`: Refactor the `ERC1967Upgrade` abstract contract as a library. diff --git a/.changeset/happy-falcons-walk.md b/.changeset/happy-falcons-walk.md new file mode 100644 index 00000000000..bba9642aabf --- /dev/null +++ b/.changeset/happy-falcons-walk.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`TransparentUpgradeableProxy`: Admin is now stored in an immutable variable (set during construction) to avoid unnecessary storage reads on every proxy call. This removed the ability to ever change the admin. Transfer of the upgrade capability is exclusively handled through the ownership of the `ProxyAdmin`. diff --git a/.changeset/heavy-drinks-fail.md b/.changeset/heavy-drinks-fail.md new file mode 100644 index 00000000000..bbe93ca90da --- /dev/null +++ b/.changeset/heavy-drinks-fail.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`ERC20`: Remove `Approval` event previously emitted in `transferFrom` to indicate that part of the allowance was consumed. With this change, allowances are no longer reconstructible from events. See the code for guidelines on how to re-enable this event if needed. diff --git a/.changeset/hip-beds-provide.md b/.changeset/hip-beds-provide.md new file mode 100644 index 00000000000..c6728381311 --- /dev/null +++ b/.changeset/hip-beds-provide.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Move the logic to validate ERC-1822 during an upgrade from `ERC1967Utils` to `UUPSUpgradeable`. diff --git a/.changeset/hot-coins-judge.md b/.changeset/hot-coins-judge.md new file mode 100644 index 00000000000..e544af46723 --- /dev/null +++ b/.changeset/hot-coins-judge.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Arrays`: Add `unsafeMemoryAccess` helpers to read from a memory array without checking the length. diff --git a/.changeset/hot-plums-approve.md b/.changeset/hot-plums-approve.md new file mode 100644 index 00000000000..131559027fa --- /dev/null +++ b/.changeset/hot-plums-approve.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`GovernorTimelockControl`: Clean up timelock id on execution for gas refund. diff --git a/.changeset/little-falcons-build.md b/.changeset/little-falcons-build.md new file mode 100644 index 00000000000..b310a8ae697 --- /dev/null +++ b/.changeset/little-falcons-build.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`EIP712`: Add internal getters for the name and version strings diff --git a/.changeset/lovely-geckos-hide.md b/.changeset/lovely-geckos-hide.md new file mode 100644 index 00000000000..1fbcb207755 --- /dev/null +++ b/.changeset/lovely-geckos-hide.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Replace revert strings and require statements with custom errors. diff --git a/.changeset/mighty-donuts-smile.md b/.changeset/mighty-donuts-smile.md new file mode 100644 index 00000000000..5885a73705d --- /dev/null +++ b/.changeset/mighty-donuts-smile.md @@ -0,0 +1,6 @@ +--- +'openzeppelin-solidity': patch +--- + +`Governor`: Add validation in ERC1155 and ERC721 receiver hooks to ensure Governor is the executor. + diff --git a/.changeset/tasty-tomatoes-turn.md b/.changeset/tasty-tomatoes-turn.md new file mode 100644 index 00000000000..3fe46a9b13c --- /dev/null +++ b/.changeset/tasty-tomatoes-turn.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`Strings`: Rename `toString(int256)` to `toStringSigned(int256)`. diff --git a/.changeset/thin-camels-matter.md b/.changeset/thin-camels-matter.md new file mode 100644 index 00000000000..c832b116371 --- /dev/null +++ b/.changeset/thin-camels-matter.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC1155`: Bubble errors triggered in the `onERC1155Received` and `onERC1155BatchReceived` hooks. diff --git a/.changeset/tough-drinks-hammer.md b/.changeset/tough-drinks-hammer.md new file mode 100644 index 00000000000..51b3836e456 --- /dev/null +++ b/.changeset/tough-drinks-hammer.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ERC1155`: Optimize array accesses by skipping bounds checking when unnecessary. diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 122d3956413..15cc88e9600 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -100,6 +100,7 @@ jobs: - uses: crytic/slither-action@v0.3.0 with: node-version: 18.15 + slither-version: 0.9.3 codespell: runs-on: ubuntu-latest diff --git a/.github/workflows/release-cycle.yml b/.github/workflows/release-cycle.yml index 2fd66458d3b..9d35022dc2e 100644 --- a/.github/workflows/release-cycle.yml +++ b/.github/workflows/release-cycle.yml @@ -192,6 +192,8 @@ jobs: pull-requests: write if: needs.state.outputs.merge == 'true' runs-on: ubuntu-latest + env: + MERGE_BRANCH: merge/${{ github.ref_name }} steps: - uses: actions/checkout@v3 with: @@ -200,7 +202,9 @@ jobs: uses: ./.github/actions/setup - run: bash scripts/git-user-config.sh - name: Create branch to merge - run: bash scripts/release/workflow/prepare-release-merge.sh + run: | + git checkout -B "$MERGE_BRANCH" "$GITHUB_REF_NAME" + git push -f origin "$MERGE_BRANCH" - name: Create PR back to master uses: actions/github-script@v6 with: @@ -208,7 +212,7 @@ jobs: await github.rest.pulls.create({ owner: context.repo.owner, repo: context.repo.repo, - head: 'merge/${{ github.ref_name }}', + head: process.env.MERGE_BRANCH, base: 'master', title: '${{ format('Merge {0} branch', github.ref_name) }}' }); diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d4e13046f5..2936877e186 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +> **Warning** Version 5.0 is under active development and should not be used. Install the releases from npm or use the version tags in the repository. + ### Removals The following contracts, libraries and functions were removed: @@ -59,6 +63,14 @@ function supportsInterface(bytes4 interfaceId) public view virtual override retu } ``` +## 4.9.2 (2023-06-16) + +- `MerkleProof`: Fix a bug in `processMultiProof` and `processMultiProofCalldata` that allows proving arbitrary leaves if the tree contains a node with value 0 at depth 1. + +## 4.9.1 (2023-06-07) + +- `Governor`: Add a mechanism to restrict the address of the proposer using a suffix in the description. + ## 4.9.0 (2023-05-23) - `ReentrancyGuard`: Add a `_reentrancyGuardEntered` function to expose the guard status. ([#3714](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3714)) diff --git a/GUIDELINES.md b/GUIDELINES.md index 0f4c9829e63..71f16640524 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -114,4 +114,25 @@ In addition to the official Solidity Style Guide we have a number of other conve interface IERC777 { ``` +* Contracts not intended to be used standalone should be marked abstract + so they are required to be inherited to other contracts. + + ```solidity + abstract contract AccessControl is ..., { + ``` + * Unchecked arithmetic blocks should contain comments explaining why overflow is guaranteed not to happen. If the reason is immediately apparent from the line above the unchecked block, the comment may be omitted. + +* Custom errors should be declared following the [EIP-6093](https://eips.ethereum.org/EIPS/eip-6093) rationale whenever reasonable. Also, consider the following: + + * The domain prefix should be picked in the following order: + 1. Use `ERC` if the error is a violation of an ERC specification. + 2. Use the name of the underlying component where it belongs (eg. `Governor`, `ECDSA`, or `Timelock`). + + * The location of custom errors should be decided in the following order: + 1. Take the errors from their underlying ERCs if they're already defined. + 2. Declare the errors in the underlying interface/library if the error makes sense in its context. + 3. Declare the error in the implementation if the underlying interface/library is not suitable to do so (eg. interface/library already specified in an ERC). + 4. Declare the error in an extension if the error only happens in such extension or child contracts. + + * Custom error names should not be declared twice along the library to avoid duplicated identifier declarations when inheriting from multiple contracts. diff --git a/README.md b/README.md index aba99171062..9d1c405b60e 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,23 @@ ### Installation +#### Hardhat, Truffle (npm) + ``` $ npm install @openzeppelin/contracts ``` OpenZeppelin Contracts features a [stable API](https://docs.openzeppelin.com/contracts/releases-stability#api-stability), which means that your contracts won't break unexpectedly when upgrading to a newer minor version. -An alternative to npm is to use the GitHub repository (`openzeppelin/openzeppelin-contracts`) to retrieve the contracts. When doing this, make sure to specify the tag for a release such as `v4.5.0`, instead of using the `master` branch. +#### Foundry (git) + +> **Warning** When installing via git, it is a common error to use the `master` branch. This is a development branch that should be avoided in favor of tagged releases. The release process involves security measures that the `master` branch does not guarantee. + +> **Warning** Foundry installs the latest version initially, but subsequent `forge update` commands will use the `master` branch. + +``` +$ forge install OpenZeppelin/openzeppelin-contracts +``` ### Usage @@ -70,7 +80,9 @@ Finally, you may want to take a look at the [guides on our blog](https://blog.op This project is maintained by [OpenZeppelin](https://openzeppelin.com) with the goal of providing a secure and reliable library of smart contract components for the ecosystem. We address security through risk management in various areas such as engineering and open source best practices, scoping and API design, multi-layered review processes, and incident response preparedness. -The security policy is detailed in [`SECURITY.md`](./SECURITY.md), and specifies how you can report security vulnerabilities, which versions will receive security patches, and how to stay informed about them. We run a [bug bounty program on Immunefi](https://immunefi.com/bounty/openzeppelin) to reward the responsible disclosure of vulnerabilities. +The [OpenZeppelin Contracts Security Center](https://contracts.openzeppelin.com/security) contains more details about the secure development process. + +The security policy is detailed in [`SECURITY.md`](./SECURITY.md) as well, and specifies how you can report security vulnerabilities, which versions will receive security patches, and how to stay informed about them. We run a [bug bounty program on Immunefi](https://immunefi.com/bounty/openzeppelin) to reward the responsible disclosure of vulnerabilities. The engineering guidelines we follow to promote project quality can be found in [`GUIDELINES.md`](./GUIDELINES.md). diff --git a/contracts/access/AccessControl.sol b/contracts/access/AccessControl.sol index df16dbdab6f..12dc770b308 100644 --- a/contracts/access/AccessControl.sol +++ b/contracts/access/AccessControl.sol @@ -107,16 +107,7 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { */ function _checkRole(bytes32 role, address account) internal view virtual { if (!hasRole(role, account)) { - revert( - string( - abi.encodePacked( - "AccessControl: account ", - Strings.toHexString(account), - " is missing role ", - Strings.toHexString(uint256(role), 32) - ) - ) - ); + revert AccessControlUnauthorizedAccount(account, role); } } @@ -173,14 +164,16 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { * * Requirements: * - * - the caller must be `account`. + * - the caller must be `callerConfirmation`. * * May emit a {RoleRevoked} event. */ - function renounceRole(bytes32 role, address account) public virtual { - require(account == _msgSender(), "AccessControl: can only renounce roles for self"); + function renounceRole(bytes32 role, address callerConfirmation) public virtual { + if (callerConfirmation != _msgSender()) { + revert AccessControlBadConfirmation(); + } - _revokeRole(role, account); + _revokeRole(role, callerConfirmation); } /** diff --git a/contracts/access/AccessControlDefaultAdminRules.sol b/contracts/access/AccessControlDefaultAdminRules.sol index 47df078c19f..e27eaf3db2a 100644 --- a/contracts/access/AccessControlDefaultAdminRules.sol +++ b/contracts/access/AccessControlDefaultAdminRules.sol @@ -53,7 +53,9 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu * @dev Sets the initial values for {defaultAdminDelay} and {defaultAdmin} address. */ constructor(uint48 initialDelay, address initialDefaultAdmin) { - require(initialDefaultAdmin != address(0), "AccessControl: 0 default admin"); + if (initialDefaultAdmin == address(0)) { + revert AccessControlInvalidDefaultAdmin(address(0)); + } _currentDelay = initialDelay; _grantRole(DEFAULT_ADMIN_ROLE, initialDefaultAdmin); } @@ -80,7 +82,9 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu * @dev See {AccessControl-grantRole}. Reverts for `DEFAULT_ADMIN_ROLE`. */ function grantRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { - require(role != DEFAULT_ADMIN_ROLE, "AccessControl: can't directly grant default admin role"); + if (role == DEFAULT_ADMIN_ROLE) { + revert AccessControlEnforcedDefaultAdminRules(); + } super.grantRole(role, account); } @@ -88,7 +92,9 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu * @dev See {AccessControl-revokeRole}. Reverts for `DEFAULT_ADMIN_ROLE`. */ function revokeRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { - require(role != DEFAULT_ADMIN_ROLE, "AccessControl: can't directly revoke default admin role"); + if (role == DEFAULT_ADMIN_ROLE) { + revert AccessControlEnforcedDefaultAdminRules(); + } super.revokeRole(role, account); } @@ -108,10 +114,9 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu function renounceRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { if (role == DEFAULT_ADMIN_ROLE && account == defaultAdmin()) { (address newDefaultAdmin, uint48 schedule) = pendingDefaultAdmin(); - require( - newDefaultAdmin == address(0) && _isScheduleSet(schedule) && _hasSchedulePassed(schedule), - "AccessControl: only can renounce in two delayed steps" - ); + if (newDefaultAdmin != address(0) || !_isScheduleSet(schedule) || !_hasSchedulePassed(schedule)) { + revert AccessControlEnforcedDefaultAdminDelay(schedule); + } delete _pendingDefaultAdminSchedule; } super.renounceRole(role, account); @@ -128,7 +133,9 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu */ function _grantRole(bytes32 role, address account) internal virtual override { if (role == DEFAULT_ADMIN_ROLE) { - require(defaultAdmin() == address(0), "AccessControl: default admin already granted"); + if (defaultAdmin() != address(0)) { + revert AccessControlEnforcedDefaultAdminRules(); + } _currentDefaultAdmin = account; } super._grantRole(role, account); @@ -148,7 +155,9 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu * @dev See {AccessControl-_setRoleAdmin}. Reverts for `DEFAULT_ADMIN_ROLE`. */ function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual override { - require(role != DEFAULT_ADMIN_ROLE, "AccessControl: can't violate default admin rules"); + if (role == DEFAULT_ADMIN_ROLE) { + revert AccessControlEnforcedDefaultAdminRules(); + } super._setRoleAdmin(role, adminRole); } @@ -236,7 +245,10 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu */ function acceptDefaultAdminTransfer() public virtual { (address newDefaultAdmin, ) = pendingDefaultAdmin(); - require(_msgSender() == newDefaultAdmin, "AccessControl: pending admin must accept"); + if (_msgSender() != newDefaultAdmin) { + // Enforce newDefaultAdmin explicit acceptance. + revert AccessControlInvalidDefaultAdmin(_msgSender()); + } _acceptDefaultAdminTransfer(); } @@ -247,7 +259,9 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu */ function _acceptDefaultAdminTransfer() internal virtual { (address newAdmin, uint48 schedule) = pendingDefaultAdmin(); - require(_isScheduleSet(schedule) && _hasSchedulePassed(schedule), "AccessControl: transfer delay not passed"); + if (!_isScheduleSet(schedule) || !_hasSchedulePassed(schedule)) { + revert AccessControlEnforcedDefaultAdminDelay(schedule); + } _revokeRole(DEFAULT_ADMIN_ROLE, defaultAdmin()); _grantRole(DEFAULT_ADMIN_ROLE, newAdmin); delete _pendingDefaultAdmin; diff --git a/contracts/access/IAccessControl.sol b/contracts/access/IAccessControl.sol index 34708b78d9a..9abc2b73555 100644 --- a/contracts/access/IAccessControl.sol +++ b/contracts/access/IAccessControl.sol @@ -7,6 +7,18 @@ pragma solidity ^0.8.19; * @dev External interface of AccessControl declared to support ERC165 detection. */ interface IAccessControl { + /** + * @dev The `account` is missing a role. + */ + error AccessControlUnauthorizedAccount(address account, bytes32 neededRole); + + /** + * @dev The caller of a function is not the expected one. + * + * NOTE: Don't confuse with {AccessControlUnauthorizedAccount}. + */ + error AccessControlBadConfirmation(); + /** * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` * @@ -82,7 +94,7 @@ interface IAccessControl { * * Requirements: * - * - the caller must be `account`. + * - the caller must be `callerConfirmation`. */ - function renounceRole(bytes32 role, address account) external; + function renounceRole(bytes32 role, address callerConfirmation) external; } diff --git a/contracts/access/IAccessControlDefaultAdminRules.sol b/contracts/access/IAccessControlDefaultAdminRules.sol index 94cbe871dee..fbecfe1207b 100644 --- a/contracts/access/IAccessControlDefaultAdminRules.sol +++ b/contracts/access/IAccessControlDefaultAdminRules.sol @@ -11,6 +11,28 @@ import "./IAccessControl.sol"; * _Available since v4.9._ */ interface IAccessControlDefaultAdminRules is IAccessControl { + /** + * @dev The new default admin is not a valid default admin. + */ + error AccessControlInvalidDefaultAdmin(address defaultAdmin); + + /** + * @dev At least one of the following rules was violated: + * + * - The `DEFAULT_ADMIN_ROLE` must only be managed by itself. + * - The `DEFAULT_ADMIN_ROLE` must only be held by one account at the time. + * - Any `DEFAULT_ADMIN_ROLE` transfer must be in two delayed steps. + */ + error AccessControlEnforcedDefaultAdminRules(); + + /** + * @dev The delay for transferring the default admin delay is enforced and + * the operation must wait until `schedule`. + * + * NOTE: `schedule` can be 0 indicating there's no transfer scheduled. + */ + error AccessControlEnforcedDefaultAdminDelay(uint48 schedule); + /** * @dev Emitted when a {defaultAdmin} transfer is started, setting `newAdmin` as the next * address to become the {defaultAdmin} by calling {acceptDefaultAdminTransfer} only after `acceptSchedule` diff --git a/contracts/access/Ownable.sol b/contracts/access/Ownable.sol index 6c901b7a195..627226cf715 100644 --- a/contracts/access/Ownable.sol +++ b/contracts/access/Ownable.sol @@ -10,8 +10,8 @@ import "../utils/Context.sol"; * there is an account (an owner) that can be granted exclusive access to * specific functions. * - * By default, the owner account will be the one that deploys the contract. This - * can later be changed with {transferOwnership}. + * The initial owner is set to the address provided by the deployer. This can + * later be changed with {transferOwnership}. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be applied to your functions to restrict their use to @@ -20,10 +20,20 @@ import "../utils/Context.sol"; abstract contract Ownable is Context { address private _owner; + /** + * @dev The caller account is not authorized to perform an operation. + */ + error OwnableUnauthorizedAccount(address account); + + /** + * @dev The owner is not a valid owner account. (eg. `address(0)`) + */ + error OwnableInvalidOwner(address owner); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** - * @dev Initializes the contract setting the deployer as the initial owner. + * @dev Initializes the contract setting the address provided by the deployer as the initial owner. */ constructor(address initialOwner) { _transferOwnership(initialOwner); @@ -48,7 +58,9 @@ abstract contract Ownable is Context { * @dev Throws if the sender is not the owner. */ function _checkOwner() internal view virtual { - require(owner() == _msgSender(), "Ownable: caller is not the owner"); + if (owner() != _msgSender()) { + revert OwnableUnauthorizedAccount(_msgSender()); + } } /** @@ -67,7 +79,9 @@ abstract contract Ownable is Context { * Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual onlyOwner { - require(newOwner != address(0), "Ownable: new owner is the zero address"); + if (newOwner == address(0)) { + revert OwnableInvalidOwner(address(0)); + } _transferOwnership(newOwner); } diff --git a/contracts/access/Ownable2Step.sol b/contracts/access/Ownable2Step.sol index 59ffa3e0e96..f76eb2bba60 100644 --- a/contracts/access/Ownable2Step.sol +++ b/contracts/access/Ownable2Step.sol @@ -10,7 +10,7 @@ import "./Ownable.sol"; * there is an account (an owner) that can be granted exclusive access to * specific functions. * - * By default, the owner account will be the one that deploys the contract. This + * The initial owner is specified at deployment time in the constructor for `Ownable`. This * can later be changed with {transferOwnership} and {acceptOwnership}. * * This module is used through inheritance. It will make available all functions @@ -51,7 +51,9 @@ abstract contract Ownable2Step is Ownable { */ function acceptOwnership() public virtual { address sender = _msgSender(); - require(pendingOwner() == sender, "Ownable2Step: caller is not the new owner"); + if (pendingOwner() != sender) { + revert OwnableUnauthorizedAccount(sender); + } _transferOwnership(sender); } } diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol index 5b7e1b150db..ebdf0a330aa 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -23,6 +23,11 @@ contract VestingWallet is Context { event EtherReleased(uint256 amount); event ERC20Released(address indexed token, uint256 amount); + /** + * @dev The `beneficiary` is not a valid account. + */ + error VestingWalletInvalidBeneficiary(address beneficiary); + uint256 private _released; mapping(address => uint256) private _erc20Released; address private immutable _beneficiary; @@ -33,7 +38,9 @@ contract VestingWallet is Context { * @dev Set the beneficiary, start timestamp and vesting duration of the vesting wallet. */ constructor(address beneficiaryAddress, uint64 startTimestamp, uint64 durationSeconds) payable { - require(beneficiaryAddress != address(0), "VestingWallet: beneficiary is zero address"); + if (beneficiaryAddress == address(0)) { + revert VestingWalletInvalidBeneficiary(address(0)); + } _beneficiary = beneficiaryAddress; _start = startTimestamp; _duration = durationSeconds; diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index 2e2289c6a8e..bae5d62e642 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.9.0) (governance/Governor.sol) +// OpenZeppelin Contracts (last updated v4.9.1) (governance/Governor.sol) pragma solidity ^0.8.19; @@ -34,19 +34,15 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive // solhint-disable var-name-mixedcase struct ProposalCore { - // --- start retyped from Timers.BlockNumber at offset 0x00 --- - uint64 voteStart; address proposer; - bytes4 __gap_unused0; - // --- start retyped from Timers.BlockNumber at offset 0x20 --- - uint64 voteEnd; - bytes24 __gap_unused1; - // --- Remaining fields starting at offset 0x40 --------------- + uint48 voteStart; + uint32 voteDuration; bool executed; bool canceled; } // solhint-enable var-name-mixedcase + bytes32 private constant _ALL_PROPOSAL_STATES_BITMAP = bytes32((2 ** (uint8(type(ProposalState).max) + 1)) - 1); string private _name; /// @custom:oz-retyped-from mapping(uint256 => Governor.ProposalCore) @@ -69,7 +65,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive * governance protocol (since v4.6). */ modifier onlyGovernance() { - require(_msgSender() == _executor(), "Governor: onlyGovernance"); + if (_executor() != _msgSender()) { + revert GovernorOnlyExecutor(_msgSender()); + } if (_executor() != address(this)) { bytes32 msgDataHash = keccak256(_msgData()); // loop until popping the expected operation - throw if deque is empty (operation not authorized) @@ -89,7 +87,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive * @dev Function to receive ETH that will be handled by the governor (disabled if executor is a third party contract) */ receive() external payable virtual { - require(_executor() == address(this), "Governor: must send to executor"); + if (_executor() != address(this)) { + revert GovernorDisabledDeposit(); + } } /** @@ -161,7 +161,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive * @dev See {IGovernor-state}. */ function state(uint256 proposalId) public view virtual override returns (ProposalState) { - ProposalCore storage proposal = _proposals[proposalId]; + // ProposalCore is just one slot. We can load it from storage to memory with a single sload and use memory + // object as a cache. This avoid duplicating expensive sloads. + ProposalCore memory proposal = _proposals[proposalId]; if (proposal.executed) { return ProposalState.Executed; @@ -174,7 +176,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive uint256 snapshot = proposalSnapshot(proposalId); if (snapshot == 0) { - revert("Governor: unknown proposal id"); + revert GovernorNonexistentProposal(proposalId); } uint256 currentTimepoint = clock(); @@ -214,7 +216,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive * @dev See {IGovernor-proposalDeadline}. */ function proposalDeadline(uint256 proposalId) public view virtual override returns (uint256) { - return _proposals[proposalId].voteEnd; + return _proposals[proposalId].voteStart + _proposals[proposalId].voteDuration; } /** @@ -275,29 +277,34 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive require(_isValidDescriptionForProposer(proposer, description), "Governor: proposer restricted"); uint256 currentTimepoint = clock(); - require( - getVotes(proposer, currentTimepoint - 1) >= proposalThreshold(), - "Governor: proposer votes below proposal threshold" - ); + + // Avoid stack too deep + { + uint256 proposerVotes = getVotes(proposer, currentTimepoint - 1); + uint256 votesThreshold = proposalThreshold(); + if (proposerVotes < votesThreshold) { + revert GovernorInsufficientProposerVotes(proposer, proposerVotes, votesThreshold); + } + } uint256 proposalId = hashProposal(targets, values, calldatas, keccak256(bytes(description))); - require(targets.length == values.length, "Governor: invalid proposal length"); - require(targets.length == calldatas.length, "Governor: invalid proposal length"); - require(targets.length > 0, "Governor: empty proposal"); - require(_proposals[proposalId].voteStart == 0, "Governor: proposal already exists"); + if (targets.length != values.length || targets.length != calldatas.length || targets.length == 0) { + revert GovernorInvalidProposalLength(targets.length, calldatas.length, values.length); + } + if (_proposals[proposalId].voteStart != 0) { + revert GovernorUnexpectedProposalState(proposalId, state(proposalId), bytes32(0)); + } uint256 snapshot = currentTimepoint + votingDelay(); - uint256 deadline = snapshot + votingPeriod(); + uint256 duration = votingPeriod(); _proposals[proposalId] = ProposalCore({ proposer: proposer, - voteStart: SafeCast.toUint64(snapshot), - voteEnd: SafeCast.toUint64(deadline), + voteStart: SafeCast.toUint48(snapshot), + voteDuration: SafeCast.toUint32(duration), executed: false, - canceled: false, - __gap_unused0: 0, - __gap_unused1: 0 + canceled: false }); emit ProposalCreated( @@ -308,7 +315,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive new string[](targets.length), calldatas, snapshot, - deadline, + snapshot + duration, description ); @@ -327,10 +334,13 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); ProposalState currentState = state(proposalId); - require( - currentState == ProposalState.Succeeded || currentState == ProposalState.Queued, - "Governor: proposal not successful" - ); + if (currentState != ProposalState.Succeeded && currentState != ProposalState.Queued) { + revert GovernorUnexpectedProposalState( + proposalId, + currentState, + _encodeStateBitmap(ProposalState.Succeeded) | _encodeStateBitmap(ProposalState.Queued) + ); + } _proposals[proposalId].executed = true; emit ProposalExecuted(proposalId); @@ -352,8 +362,13 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive bytes32 descriptionHash ) public virtual override returns (uint256) { uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); - require(state(proposalId) == ProposalState.Pending, "Governor: too late to cancel"); - require(_msgSender() == _proposals[proposalId].proposer, "Governor: only proposer can cancel"); + ProposalState currentState = state(proposalId); + if (currentState != ProposalState.Pending) { + revert GovernorUnexpectedProposalState(proposalId, currentState, _encodeStateBitmap(ProposalState.Pending)); + } + if (_msgSender() != proposalProposer(proposalId)) { + revert GovernorOnlyProposer(_msgSender()); + } return _cancel(targets, values, calldatas, descriptionHash); } @@ -367,10 +382,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive bytes[] memory calldatas, bytes32 /*descriptionHash*/ ) internal virtual { - string memory errorMessage = "Governor: call reverted without message"; for (uint256 i = 0; i < targets.length; ++i) { (bool success, bytes memory returndata) = targets[i].call{value: values[i]}(calldatas[i]); - Address.verifyCallResult(success, returndata, errorMessage); + Address.verifyCallResult(success, returndata); } } @@ -426,12 +440,16 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive ProposalState currentState = state(proposalId); - require( - currentState != ProposalState.Canceled && - currentState != ProposalState.Expired && - currentState != ProposalState.Executed, - "Governor: proposal not active" - ); + bytes32 forbiddenStates = _encodeStateBitmap(ProposalState.Canceled) | + _encodeStateBitmap(ProposalState.Expired) | + _encodeStateBitmap(ProposalState.Executed); + if (forbiddenStates & _encodeStateBitmap(currentState) != 0) { + revert GovernorUnexpectedProposalState( + proposalId, + currentState, + _ALL_PROPOSAL_STATES_BITMAP ^ forbiddenStates + ); + } _proposals[proposalId].canceled = true; emit ProposalCanceled(proposalId); @@ -569,10 +587,12 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive string memory reason, bytes memory params ) internal virtual returns (uint256) { - ProposalCore storage proposal = _proposals[proposalId]; - require(state(proposalId) == ProposalState.Active, "Governor: vote not currently active"); + ProposalState currentState = state(proposalId); + if (currentState != ProposalState.Active) { + revert GovernorUnexpectedProposalState(proposalId, currentState, _encodeStateBitmap(ProposalState.Active)); + } - uint256 weight = _getVotes(account, proposal.voteStart, params); + uint256 weight = _getVotes(account, proposalSnapshot(proposalId), params); _countVote(proposalId, account, support, weight, params); if (params.length == 0) { @@ -592,7 +612,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive */ function relay(address target, uint256 value, bytes calldata data) external payable virtual onlyGovernance { (bool success, bytes memory returndata) = target.call{value: value}(data); - Address.verifyCallResult(success, returndata, "Governor: relay reverted without message"); + Address.verifyCallResult(success, returndata); } /** @@ -605,20 +625,29 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive /** * @dev See {IERC721Receiver-onERC721Received}. + * Receiving tokens is disabled if the governance executor is other than the governor itself (eg. when using with a timelock). */ function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) { + if (_executor() != address(this)) { + revert GovernorDisabledDeposit(); + } return this.onERC721Received.selector; } /** * @dev See {IERC1155Receiver-onERC1155Received}. + * Receiving tokens is disabled if the governance executor is other than the governor itself (eg. when using with a timelock). */ function onERC1155Received(address, address, uint256, uint256, bytes memory) public virtual returns (bytes4) { + if (_executor() != address(this)) { + revert GovernorDisabledDeposit(); + } return this.onERC1155Received.selector; } /** * @dev See {IERC1155Receiver-onERC1155BatchReceived}. + * Receiving tokens is disabled if the governance executor is other than the governor itself (eg. when using with a timelock). */ function onERC1155BatchReceived( address, @@ -627,10 +656,29 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive uint256[] memory, bytes memory ) public virtual returns (bytes4) { + if (_executor() != address(this)) { + revert GovernorDisabledDeposit(); + } return this.onERC1155BatchReceived.selector; } /** + * @dev Encodes a `ProposalState` 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 _encodeStateBitmap(ProposalState proposalState) internal pure returns (bytes32) { + return bytes32(1 << uint8(proposalState)); + } + + /* * @dev Check if the proposer is authorized to submit a proposal with the given description. * * If the proposal description ends with `#proposer=0x???`, where `0x???` is an address written as a hex string diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 992b5ca10b1..0adde679558 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -23,6 +23,63 @@ abstract contract IGovernor is IERC165, IERC6372 { Executed } + /** + * @dev Empty proposal or a mismatch between the parameters length for a proposal call. + */ + error GovernorInvalidProposalLength(uint256 targets, uint256 calldatas, uint256 values); + + /** + * @dev The vote was already cast. + */ + error GovernorAlreadyCastVote(address voter); + + /** + * @dev Token deposits are disabled in this contract. + */ + error GovernorDisabledDeposit(); + + /** + * @dev The `account` is not a proposer. + */ + error GovernorOnlyProposer(address account); + + /** + * @dev The `account` is not the governance executor. + */ + error GovernorOnlyExecutor(address account); + + /** + * @dev The `proposalId` doesn't exist. + */ + error GovernorNonexistentProposal(uint256 proposalId); + + /** + * @dev The current state of a proposal is not the required for performing an operation. + * The `expectedStates` is a bitmap with the bits enabled for each ProposalState enum position + * counting from right to left. + * + * NOTE: If `expectedState` is `bytes32(0)`, the proposal is expected to not be in any state (i.e. not exist). + * This is the case when a proposal that is expected to be unset is already initiated (the proposal is duplicated). + * + * See {Governor-_encodeStateBitmap}. + */ + error GovernorUnexpectedProposalState(uint256 proposalId, ProposalState current, bytes32 expectedStates); + + /** + * @dev The voting period set is not a valid period. + */ + error GovernorInvalidVotingPeriod(uint256 votingPeriod); + + /** + * @dev The `proposer` does not have the required votes to operate on a proposal. + */ + error GovernorInsufficientProposerVotes(address proposer, uint256 votes, uint256 threshold); + + /** + * @dev The vote type used is not valid for the corresponding counting module. + */ + error GovernorInvalidVoteType(); + /** * @dev Emitted when a proposal is created. */ @@ -165,6 +222,9 @@ abstract contract IGovernor is IERC165, IERC6372 { * * This can be increased to leave time for users to buy voting power, or delegate it, before the voting of a * proposal starts. + * + * NOTE: While this interface returns a uint256, timepoints are stored as uint48 following the ERC-6372 clock type. + * Consequently this value must fit in a uint48 (when added to the current clock). See {IERC6372-clock}. */ function votingDelay() public view virtual returns (uint256); @@ -175,6 +235,10 @@ abstract contract IGovernor is IERC165, IERC6372 { * * NOTE: The {votingDelay} can delay the start of the vote. This must be considered when setting the voting * duration compared to the voting delay. + * + * NOTE: This value is stored when the proposal is submitted so that possible changes to the value do not affect + * proposals that have already been submitted. The type used to save it is a uint32. Consequently, while this + * interface returns a uint256, the value it returns should fit in a uint32. */ function votingPeriod() public view virtual returns (uint256); diff --git a/contracts/governance/TimelockController.sol b/contracts/governance/TimelockController.sol index 9930d6a4941..a25cd7b4a77 100644 --- a/contracts/governance/TimelockController.sol +++ b/contracts/governance/TimelockController.sol @@ -4,8 +4,9 @@ pragma solidity ^0.8.19; import "../access/AccessControl.sol"; -import "../token/ERC721/IERC721Receiver.sol"; -import "../token/ERC1155/IERC1155Receiver.sol"; +import "../token/ERC721/utils/ERC721Holder.sol"; +import "../token/ERC1155/utils/ERC1155Holder.sol"; +import "../utils/Address.sol"; /** * @dev Contract module which acts as a timelocked controller. When set as the @@ -22,7 +23,7 @@ import "../token/ERC1155/IERC1155Receiver.sol"; * * _Available since v3.3._ */ -contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver { +contract TimelockController is AccessControl, ERC721Holder, ERC1155Holder { bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE"); bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE"); bytes32 public constant CANCELLER_ROLE = keccak256("CANCELLER_ROLE"); @@ -31,6 +32,38 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver mapping(bytes32 => uint256) private _timestamps; uint256 private _minDelay; + enum OperationState { + Unset, + Pending, + Ready, + Done + } + + /** + * @dev Mismatch between the parameters length for an operation call. + */ + error TimelockInvalidOperationLength(uint256 targets, uint256 payloads, uint256 values); + + /** + * @dev The schedule operation doesn't meet the minimum delay. + */ + error TimelockInsufficientDelay(uint256 delay, uint256 minDelay); + + /** + * @dev The current state of an operation is not as required. + */ + error TimelockUnexpectedOperationState(bytes32 operationId, OperationState expected); + + /** + * @dev The predecessor to an operation not yet done. + */ + error TimelockUnexecutedPredecessor(bytes32 predecessorId); + + /** + * @dev The caller account is not authorized. + */ + error TimelockUnauthorizedCaller(address caller); + /** * @dev Emitted when a call is scheduled as part of operation `id`. */ @@ -122,8 +155,10 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver /** * @dev See {IERC165-supportsInterface}. */ - function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, AccessControl) returns (bool) { - return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(AccessControl, ERC1155Receiver) returns (bool) { + return super.supportsInterface(interfaceId); } /** @@ -243,8 +278,9 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver bytes32 salt, uint256 delay ) public virtual onlyRole(PROPOSER_ROLE) { - require(targets.length == values.length, "TimelockController: length mismatch"); - require(targets.length == payloads.length, "TimelockController: length mismatch"); + if (targets.length != values.length || targets.length != payloads.length) { + revert TimelockInvalidOperationLength(targets.length, payloads.length, values.length); + } bytes32 id = hashOperationBatch(targets, values, payloads, predecessor, salt); _schedule(id, delay); @@ -260,8 +296,13 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver * @dev Schedule an operation that is to become valid after a given delay. */ function _schedule(bytes32 id, uint256 delay) private { - require(!isOperation(id), "TimelockController: operation already scheduled"); - require(delay >= getMinDelay(), "TimelockController: insufficient delay"); + if (isOperation(id)) { + revert TimelockUnexpectedOperationState(id, OperationState.Unset); + } + uint256 minDelay = getMinDelay(); + if (delay < minDelay) { + revert TimelockInsufficientDelay(delay, minDelay); + } _timestamps[id] = block.timestamp + delay; } @@ -273,7 +314,9 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver * - the caller must have the 'canceller' role. */ function cancel(bytes32 id) public virtual onlyRole(CANCELLER_ROLE) { - require(isOperationPending(id), "TimelockController: operation cannot be cancelled"); + if (!isOperationPending(id)) { + revert TimelockUnexpectedOperationState(id, OperationState.Pending); + } delete _timestamps[id]; emit Cancelled(id); @@ -325,8 +368,9 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver bytes32 predecessor, bytes32 salt ) public payable virtual onlyRoleOrOpenRole(EXECUTOR_ROLE) { - require(targets.length == values.length, "TimelockController: length mismatch"); - require(targets.length == payloads.length, "TimelockController: length mismatch"); + if (targets.length != values.length || targets.length != payloads.length) { + revert TimelockInvalidOperationLength(targets.length, payloads.length, values.length); + } bytes32 id = hashOperationBatch(targets, values, payloads, predecessor, salt); @@ -345,23 +389,29 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver * @dev Execute an operation's call. */ function _execute(address target, uint256 value, bytes calldata data) internal virtual { - (bool success, ) = target.call{value: value}(data); - require(success, "TimelockController: underlying transaction reverted"); + (bool success, bytes memory returndata) = target.call{value: value}(data); + Address.verifyCallResult(success, returndata); } /** * @dev Checks before execution of an operation's calls. */ function _beforeCall(bytes32 id, bytes32 predecessor) private view { - require(isOperationReady(id), "TimelockController: operation is not ready"); - require(predecessor == bytes32(0) || isOperationDone(predecessor), "TimelockController: missing dependency"); + if (!isOperationReady(id)) { + revert TimelockUnexpectedOperationState(id, OperationState.Ready); + } + if (predecessor != bytes32(0) && !isOperationDone(predecessor)) { + revert TimelockUnexecutedPredecessor(predecessor); + } } /** * @dev Checks after execution of an operation's calls. */ function _afterCall(bytes32 id) private { - require(isOperationReady(id), "TimelockController: operation is not ready"); + if (!isOperationReady(id)) { + revert TimelockUnexpectedOperationState(id, OperationState.Ready); + } _timestamps[id] = _DONE_TIMESTAMP; } @@ -376,35 +426,10 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver * an operation where the timelock is the target and the data is the ABI-encoded call to this function. */ function updateDelay(uint256 newDelay) external virtual { - require(msg.sender == address(this), "TimelockController: caller must be timelock"); + if (msg.sender != address(this)) { + revert TimelockUnauthorizedCaller(msg.sender); + } emit MinDelayChange(_minDelay, newDelay); _minDelay = newDelay; } - - /** - * @dev See {IERC721Receiver-onERC721Received}. - */ - function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) { - return this.onERC721Received.selector; - } - - /** - * @dev See {IERC1155Receiver-onERC1155Received}. - */ - function onERC1155Received(address, address, uint256, uint256, bytes memory) public virtual returns (bytes4) { - return this.onERC1155Received.selector; - } - - /** - * @dev See {IERC1155Receiver-onERC1155BatchReceived}. - */ - function onERC1155BatchReceived( - address, - address, - uint256[] memory, - uint256[] memory, - bytes memory - ) public virtual returns (bytes4) { - return this.onERC1155BatchReceived.selector; - } } diff --git a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol index 425ecad0963..8d6d9ccb43a 100644 --- a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol @@ -69,7 +69,9 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp bytes[] memory calldatas, string memory description ) public virtual override returns (uint256) { - require(signatures.length == calldatas.length, "GovernorBravo: invalid signatures length"); + if (signatures.length != calldatas.length) { + revert GovernorInvalidSignaturesLength(signatures.length, calldatas.length); + } // Stores the full proposal and fallback to the public (possibly overridden) propose. The fallback is done // after the full proposal is stored, so the store operation included in the fallback will be skipped. Here we // call `propose` and not `super.propose` to make sure if a child contract override `propose`, whatever code @@ -133,10 +135,11 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); address proposer = proposalProposer(proposalId); - require( - _msgSender() == proposer || getVotes(proposer, clock() - 1) < proposalThreshold(), - "GovernorBravo: proposer above threshold" - ); + uint256 proposerVotes = getVotes(proposer, clock() - 1); + uint256 votesThreshold = proposalThreshold(); + if (_msgSender() != proposer && proposerVotes >= votesThreshold) { + revert GovernorInsufficientProposerVotes(proposer, proposerVotes, votesThreshold); + } return _cancel(targets, values, calldatas, descriptionHash); } @@ -152,7 +155,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp for (uint256 i = 0; i < fullcalldatas.length; ++i) { fullcalldatas[i] = bytes(signatures[i]).length == 0 ? calldatas[i] - : abi.encodePacked(bytes4(keccak256(bytes(signatures[i]))), calldatas[i]); + : bytes.concat(abi.encodeWithSignature(signatures[i]), calldatas[i]); } return fullcalldatas; @@ -312,7 +315,9 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp ProposalDetails storage details = _proposalDetails[proposalId]; Receipt storage receipt = details.receipts[account]; - require(!receipt.hasVoted, "GovernorCompatibilityBravo: vote already cast"); + if (receipt.hasVoted) { + revert GovernorAlreadyCastVote(account); + } receipt.hasVoted = true; receipt.support = support; receipt.votes = SafeCast.toUint96(weight); @@ -324,7 +329,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp } else if (support == uint8(VoteType.Abstain)) { details.abstainVotes += weight; } else { - revert("GovernorCompatibilityBravo: invalid vote type"); + revert GovernorInvalidVoteType(); } } } diff --git a/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol b/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol index d69bbf6193b..197936ab930 100644 --- a/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol @@ -11,6 +11,11 @@ import "../IGovernor.sol"; * _Available since v4.3._ */ abstract contract IGovernorCompatibilityBravo is IGovernor { + /** + * @dev Mismatch between the parameters length for a proposal call. + */ + error GovernorInvalidSignaturesLength(uint256 signatures, uint256 calldatas); + /** * @dev Proposal structure from Compound Governor Bravo. Not actually used by the compatibility layer, as * {{proposal}} returns a very different structure. diff --git a/contracts/governance/extensions/GovernorCountingSimple.sol b/contracts/governance/extensions/GovernorCountingSimple.sol index d5c99e593e7..315f4ad45d3 100644 --- a/contracts/governance/extensions/GovernorCountingSimple.sol +++ b/contracts/governance/extensions/GovernorCountingSimple.sol @@ -84,7 +84,9 @@ abstract contract GovernorCountingSimple is Governor { ) internal virtual override { ProposalVote storage proposalVote = _proposalVotes[proposalId]; - require(!proposalVote.hasVoted[account], "GovernorVotingSimple: vote already cast"); + if (proposalVote.hasVoted[account]) { + revert GovernorAlreadyCastVote(account); + } proposalVote.hasVoted[account] = true; if (support == uint8(VoteType.Against)) { @@ -94,7 +96,7 @@ abstract contract GovernorCountingSimple is Governor { } else if (support == uint8(VoteType.Abstain)) { proposalVote.abstainVotes += weight; } else { - revert("GovernorVotingSimple: invalid value for enum VoteType"); + revert GovernorInvalidVoteType(); } } } diff --git a/contracts/governance/extensions/GovernorPreventLateQuorum.sol b/contracts/governance/extensions/GovernorPreventLateQuorum.sol index abb81128eb1..3e730174e5b 100644 --- a/contracts/governance/extensions/GovernorPreventLateQuorum.sol +++ b/contracts/governance/extensions/GovernorPreventLateQuorum.sol @@ -18,10 +18,10 @@ import "../../utils/math/Math.sol"; * _Available since v4.5._ */ abstract contract GovernorPreventLateQuorum is Governor { - uint64 private _voteExtension; + uint48 private _voteExtension; /// @custom:oz-retyped-from mapping(uint256 => Timers.BlockNumber) - mapping(uint256 => uint64) private _extendedDeadlines; + mapping(uint256 => uint48) private _extendedDeadlines; /// @dev Emitted when a proposal deadline is pushed back due to reaching quorum late in its voting period. event ProposalExtended(uint256 indexed proposalId, uint64 extendedDeadline); @@ -34,7 +34,7 @@ abstract contract GovernorPreventLateQuorum is Governor { * clock mode) that is required to pass since the moment a proposal reaches quorum until its voting period ends. If * necessary the voting period will be extended beyond the one set during proposal creation. */ - constructor(uint64 initialVoteExtension) { + constructor(uint48 initialVoteExtension) { _setLateQuorumVoteExtension(initialVoteExtension); } @@ -62,7 +62,7 @@ abstract contract GovernorPreventLateQuorum is Governor { uint256 result = super._castVote(proposalId, account, support, reason, params); if (_extendedDeadlines[proposalId] == 0 && _quorumReached(proposalId)) { - uint64 extendedDeadline = clock() + lateQuorumVoteExtension(); + uint48 extendedDeadline = clock() + lateQuorumVoteExtension(); if (extendedDeadline > proposalDeadline(proposalId)) { emit ProposalExtended(proposalId, extendedDeadline); @@ -78,7 +78,7 @@ abstract contract GovernorPreventLateQuorum is Governor { * @dev Returns the current value of the vote extension parameter: the number of blocks that are required to pass * from the time a proposal reaches quorum until its voting period ends. */ - function lateQuorumVoteExtension() public view virtual returns (uint64) { + function lateQuorumVoteExtension() public view virtual returns (uint48) { return _voteExtension; } @@ -88,7 +88,7 @@ abstract contract GovernorPreventLateQuorum is Governor { * * Emits a {LateQuorumVoteExtensionSet} event. */ - function setLateQuorumVoteExtension(uint64 newVoteExtension) public virtual onlyGovernance { + function setLateQuorumVoteExtension(uint48 newVoteExtension) public virtual onlyGovernance { _setLateQuorumVoteExtension(newVoteExtension); } @@ -98,7 +98,7 @@ abstract contract GovernorPreventLateQuorum is Governor { * * Emits a {LateQuorumVoteExtensionSet} event. */ - function _setLateQuorumVoteExtension(uint64 newVoteExtension) internal virtual { + function _setLateQuorumVoteExtension(uint48 newVoteExtension) internal virtual { emit LateQuorumVoteExtensionSet(_voteExtension, newVoteExtension); _voteExtension = newVoteExtension; } diff --git a/contracts/governance/extensions/GovernorSettings.sol b/contracts/governance/extensions/GovernorSettings.sol index 570c88c5484..6168689add9 100644 --- a/contracts/governance/extensions/GovernorSettings.sol +++ b/contracts/governance/extensions/GovernorSettings.sol @@ -11,9 +11,12 @@ import "../Governor.sol"; * _Available since v4.4._ */ abstract contract GovernorSettings is Governor { - uint256 private _votingDelay; - uint256 private _votingPeriod; + // amount of token uint256 private _proposalThreshold; + // timepoint: limited to uint48 in core (same as clock() type) + uint48 private _votingDelay; + // duration: limited to uint32 in core + uint32 private _votingPeriod; event VotingDelaySet(uint256 oldVotingDelay, uint256 newVotingDelay); event VotingPeriodSet(uint256 oldVotingPeriod, uint256 newVotingPeriod); @@ -22,7 +25,7 @@ abstract contract GovernorSettings is Governor { /** * @dev Initialize the governance parameters. */ - constructor(uint256 initialVotingDelay, uint256 initialVotingPeriod, uint256 initialProposalThreshold) { + constructor(uint48 initialVotingDelay, uint32 initialVotingPeriod, uint256 initialProposalThreshold) { _setVotingDelay(initialVotingDelay); _setVotingPeriod(initialVotingPeriod); _setProposalThreshold(initialProposalThreshold); @@ -54,7 +57,7 @@ abstract contract GovernorSettings is Governor { * * Emits a {VotingDelaySet} event. */ - function setVotingDelay(uint256 newVotingDelay) public virtual onlyGovernance { + function setVotingDelay(uint48 newVotingDelay) public virtual onlyGovernance { _setVotingDelay(newVotingDelay); } @@ -63,7 +66,7 @@ abstract contract GovernorSettings is Governor { * * Emits a {VotingPeriodSet} event. */ - function setVotingPeriod(uint256 newVotingPeriod) public virtual onlyGovernance { + function setVotingPeriod(uint32 newVotingPeriod) public virtual onlyGovernance { _setVotingPeriod(newVotingPeriod); } @@ -81,7 +84,7 @@ abstract contract GovernorSettings is Governor { * * Emits a {VotingDelaySet} event. */ - function _setVotingDelay(uint256 newVotingDelay) internal virtual { + function _setVotingDelay(uint48 newVotingDelay) internal virtual { emit VotingDelaySet(_votingDelay, newVotingDelay); _votingDelay = newVotingDelay; } @@ -91,9 +94,11 @@ abstract contract GovernorSettings is Governor { * * Emits a {VotingPeriodSet} event. */ - function _setVotingPeriod(uint256 newVotingPeriod) internal virtual { + function _setVotingPeriod(uint32 newVotingPeriod) internal virtual { // voting period must be at least one block long - require(newVotingPeriod > 0, "GovernorSettings: voting period too low"); + if (newVotingPeriod == 0) { + revert GovernorInvalidVotingPeriod(0); + } emit VotingPeriodSet(_votingPeriod, newVotingPeriod); _votingPeriod = newVotingPeriod; } diff --git a/contracts/governance/extensions/GovernorTimelockCompound.sol b/contracts/governance/extensions/GovernorTimelockCompound.sol index 1efd3efff85..ed4d916a661 100644 --- a/contracts/governance/extensions/GovernorTimelockCompound.sol +++ b/contracts/governance/extensions/GovernorTimelockCompound.sol @@ -24,7 +24,7 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { ICompoundTimelock private _timelock; /// @custom:oz-retyped-from mapping(uint256 => GovernorTimelockCompound.ProposalTimelock) - mapping(uint256 => uint64) private _proposalTimelocks; + mapping(uint256 => uint256) private _proposalTimelocks; /** * @dev Emitted when the timelock controller used for proposal execution is modified. @@ -90,16 +90,22 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { ) public virtual override returns (uint256) { uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); - require(state(proposalId) == ProposalState.Succeeded, "Governor: proposal not successful"); + ProposalState currentState = state(proposalId); + if (currentState != ProposalState.Succeeded) { + revert GovernorUnexpectedProposalState( + proposalId, + currentState, + _encodeStateBitmap(ProposalState.Succeeded) + ); + } uint256 eta = block.timestamp + _timelock.delay(); - _proposalTimelocks[proposalId] = SafeCast.toUint64(eta); + _proposalTimelocks[proposalId] = eta; for (uint256 i = 0; i < targets.length; ++i) { - require( - !_timelock.queuedTransactions(keccak256(abi.encode(targets[i], values[i], "", calldatas[i], eta))), - "GovernorTimelockCompound: identical proposal action already queued" - ); + if (_timelock.queuedTransactions(keccak256(abi.encode(targets[i], values[i], "", calldatas[i], eta)))) { + revert GovernorAlreadyQueuedProposal(proposalId); + } _timelock.queueTransaction(targets[i], values[i], "", calldatas[i], eta); } @@ -119,7 +125,9 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { bytes32 /*descriptionHash*/ ) internal virtual override { uint256 eta = proposalEta(proposalId); - require(eta > 0, "GovernorTimelockCompound: proposal not yet queued"); + if (eta == 0) { + revert GovernorNotQueuedProposal(proposalId); + } Address.sendValue(payable(_timelock), msg.value); for (uint256 i = 0; i < targets.length; ++i) { _timelock.executeTransaction(targets[i], values[i], "", calldatas[i], eta); diff --git a/contracts/governance/extensions/GovernorTimelockControl.sol b/contracts/governance/extensions/GovernorTimelockControl.sol index 3fbce763a46..fadbcc70152 100644 --- a/contracts/governance/extensions/GovernorTimelockControl.sol +++ b/contracts/governance/extensions/GovernorTimelockControl.sol @@ -60,11 +60,13 @@ abstract contract GovernorTimelockControl is IGovernorTimelock, Governor { bytes32 queueid = _timelockIds[proposalId]; if (queueid == bytes32(0)) { return currentState; - } else if (_timelock.isOperationDone(queueid)) { - return ProposalState.Executed; } else if (_timelock.isOperationPending(queueid)) { return ProposalState.Queued; + } else if (_timelock.isOperationDone(queueid)) { + // This can happen if the proposal is executed directly on the timelock. + return ProposalState.Executed; } else { + // This can happen if the proposal is canceled directly on the timelock. return ProposalState.Canceled; } } @@ -95,7 +97,14 @@ abstract contract GovernorTimelockControl is IGovernorTimelock, Governor { ) public virtual override returns (uint256) { uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); - require(state(proposalId) == ProposalState.Succeeded, "Governor: proposal not successful"); + ProposalState currentState = state(proposalId); + if (currentState != ProposalState.Succeeded) { + revert GovernorUnexpectedProposalState( + proposalId, + currentState, + _encodeStateBitmap(ProposalState.Succeeded) + ); + } uint256 delay = _timelock.getMinDelay(); _timelockIds[proposalId] = _timelock.hashOperationBatch(targets, values, calldatas, 0, descriptionHash); @@ -110,13 +119,16 @@ abstract contract GovernorTimelockControl is IGovernorTimelock, Governor { * @dev Overridden execute function that run the already queued proposal through the timelock. */ function _execute( - uint256 /* proposalId */, + uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash ) internal virtual override { + // execute _timelock.executeBatch{value: msg.value}(targets, values, calldatas, 0, descriptionHash); + // cleanup for refund + delete _timelockIds[proposalId]; } /** @@ -133,9 +145,12 @@ abstract contract GovernorTimelockControl is IGovernorTimelock, Governor { bytes32 descriptionHash ) internal virtual override returns (uint256) { uint256 proposalId = super._cancel(targets, values, calldatas, descriptionHash); + bytes32 timelockId = _timelockIds[proposalId]; - if (_timelockIds[proposalId] != 0) { - _timelock.cancel(_timelockIds[proposalId]); + if (timelockId != 0) { + // cancel + _timelock.cancel(timelockId); + // cleanup delete _timelockIds[proposalId]; } diff --git a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol index 6c10240cee5..0094fecd67b 100644 --- a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol +++ b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol @@ -21,6 +21,11 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes { event QuorumNumeratorUpdated(uint256 oldQuorumNumerator, uint256 newQuorumNumerator); + /** + * @dev The quorum set is not a valid fraction. + */ + error GovernorInvalidQuorumFraction(uint256 quorumNumerator, uint256 quorumDenominator); + /** * @dev Initialize quorum as a fraction of the token's total supply. * @@ -94,10 +99,10 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes { * - New numerator must be smaller or equal to the denominator. */ function _updateQuorumNumerator(uint256 newQuorumNumerator) internal virtual { - require( - newQuorumNumerator <= quorumDenominator(), - "GovernorVotesQuorumFraction: quorumNumerator over quorumDenominator" - ); + uint256 denominator = quorumDenominator(); + if (newQuorumNumerator > denominator) { + revert GovernorInvalidQuorumFraction(newQuorumNumerator, denominator); + } uint256 oldQuorumNumerator = quorumNumerator(); _quorumNumeratorHistory.push(SafeCast.toUint32(clock()), SafeCast.toUint224(newQuorumNumerator)); diff --git a/contracts/governance/extensions/IGovernorTimelock.sol b/contracts/governance/extensions/IGovernorTimelock.sol index 570092bc53d..c5142948132 100644 --- a/contracts/governance/extensions/IGovernorTimelock.sol +++ b/contracts/governance/extensions/IGovernorTimelock.sol @@ -11,6 +11,16 @@ import "../IGovernor.sol"; * _Available since v4.3._ */ abstract contract IGovernorTimelock is IGovernor { + /** + * @dev The proposal hasn't been queued yet. + */ + error GovernorNotQueuedProposal(uint256 proposalId); + + /** + * @dev The proposal has already been queued. + */ + error GovernorAlreadyQueuedProposal(uint256 proposalId); + event ProposalQueued(uint256 proposalId, uint256 eta); function timelock() public view virtual returns (address); diff --git a/contracts/governance/utils/IVotes.sol b/contracts/governance/utils/IVotes.sol index a1e4fe63afd..a8a20856f16 100644 --- a/contracts/governance/utils/IVotes.sol +++ b/contracts/governance/utils/IVotes.sol @@ -8,6 +8,11 @@ pragma solidity ^0.8.19; * _Available since v4.5._ */ interface IVotes { + /** + * @dev The signature used has expired. + */ + error VotesExpiredSignature(uint256 expiry); + /** * @dev Emitted when an account changes their delegate. */ diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index 5fc15da292b..09eb4e22c63 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -42,6 +42,16 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { /// @custom:oz-retyped-from Checkpoints.History Checkpoints.Trace224 private _totalCheckpoints; + /** + * @dev The clock was incorrectly modified. + */ + error ERC6372InconsistentClock(); + + /** + * @dev Lookup to future votes is not available. + */ + error ERC5805FutureLookup(uint256 timepoint, uint48 clock); + /** * @dev Clock used for flagging checkpoints. Can be overridden to implement timestamp based * checkpoints (and voting), in which case {CLOCK_MODE} should be overridden as well to match. @@ -56,7 +66,9 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { // solhint-disable-next-line func-name-mixedcase function CLOCK_MODE() public view virtual returns (string memory) { // Check that the clock was not modified - require(clock() == block.number, "Votes: broken clock mode"); + if (clock() != block.number) { + revert ERC6372InconsistentClock(); + } return "mode=blocknumber&from=default"; } @@ -76,7 +88,10 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. */ function getPastVotes(address account, uint256 timepoint) public view virtual returns (uint256) { - require(timepoint < clock(), "Votes: future lookup"); + uint48 currentTimepoint = clock(); + if (timepoint >= currentTimepoint) { + revert ERC5805FutureLookup(timepoint, currentTimepoint); + } return _delegateCheckpoints[account].upperLookupRecent(SafeCast.toUint32(timepoint)); } @@ -93,7 +108,10 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. */ function getPastTotalSupply(uint256 timepoint) public view virtual returns (uint256) { - require(timepoint < clock(), "Votes: future lookup"); + uint48 currentTimepoint = clock(); + if (timepoint >= currentTimepoint) { + revert ERC5805FutureLookup(timepoint, currentTimepoint); + } return _totalCheckpoints.upperLookupRecent(SafeCast.toUint32(timepoint)); } @@ -130,14 +148,16 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { bytes32 r, bytes32 s ) public virtual { - require(block.timestamp <= expiry, "Votes: signature expired"); + if (block.timestamp > expiry) { + revert VotesExpiredSignature(expiry); + } address signer = ECDSA.recover( _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), v, r, s ); - require(nonce == _useNonce(signer), "Votes: invalid nonce"); + _useCheckedNonce(signer, nonce); _delegate(signer, delegatee); } diff --git a/contracts/interfaces/draft-IERC6093.sol b/contracts/interfaces/draft-IERC6093.sol new file mode 100644 index 00000000000..fbe31051a49 --- /dev/null +++ b/contracts/interfaces/draft-IERC6093.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/** + * @dev Standard ERC20 Errors + * Interface of the ERC6093 custom errors for ERC20 tokens + * as defined in https://eips.ethereum.org/EIPS/eip-6093 + */ +interface IERC20Errors { + /** + * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param balance Current balance for the interacting account. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC20InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC20InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers. + * @param spender Address that may be allowed to operate on tokens without being their owner. + * @param allowance Amount of tokens a `spender` is allowed to operate with. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC20InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `spender` to be approved. Used in approvals. + * @param spender Address that may be allowed to operate on tokens without being their owner. + */ + error ERC20InvalidSpender(address spender); +} + +/** + * @dev Standard ERC721 Errors + * Interface of the ERC6093 custom errors for ERC721 tokens + * as defined in https://eips.ethereum.org/EIPS/eip-6093 + */ +interface IERC721Errors { + /** + * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in EIP-20. + * Used in balance queries. + * @param owner Address of the current owner of a token. + */ + error ERC721InvalidOwner(address owner); + + /** + * @dev Indicates a `tokenId` whose `owner` is the zero address. + * @param tokenId Identifier number of a token. + */ + error ERC721NonexistentToken(uint256 tokenId); + + /** + * @dev Indicates an error related to the ownership over a particular token. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param tokenId Identifier number of a token. + * @param owner Address of the current owner of a token. + */ + error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC721InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC721InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `operator`’s approval. Used in transfers. + * @param operator Address that may be allowed to operate on tokens without being their owner. + * @param tokenId Identifier number of a token. + */ + error ERC721InsufficientApproval(address operator, uint256 tokenId); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC721InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `operator` to be approved. Used in approvals. + * @param operator Address that may be allowed to operate on tokens without being their owner. + */ + error ERC721InvalidOperator(address operator); +} + +/** + * @dev Standard ERC1155 Errors + * Interface of the ERC6093 custom errors for ERC1155 tokens + * as defined in https://eips.ethereum.org/EIPS/eip-6093 + */ +interface IERC1155Errors { + /** + * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param balance Current balance for the interacting account. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC1155InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC1155InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `operator`’s approval. Used in transfers. + * @param operator Address that may be allowed to operate on tokens without being their owner. + * @param owner Address of the current owner of a token. + */ + error ERC1155MissingApprovalForAll(address operator, address owner); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC1155InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `operator` to be approved. Used in approvals. + * @param operator Address that may be allowed to operate on tokens without being their owner. + */ + error ERC1155InvalidOperator(address operator); + + /** + * @dev Indicates an array length mismatch between ids and values in a safeBatchTransferFrom operation. + * Used in batch transfers. + * @param idsLength Length of the array of token identifiers + * @param valuesLength Length of the array of token amounts + */ + error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength); +} diff --git a/contracts/metatx/ERC2771Forwarder.sol b/contracts/metatx/ERC2771Forwarder.sol new file mode 100644 index 00000000000..651fdce0b62 --- /dev/null +++ b/contracts/metatx/ERC2771Forwarder.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (metatx/ERC2771Forwarder.sol) + +pragma solidity ^0.8.19; + +import "../utils/cryptography/ECDSA.sol"; +import "../utils/cryptography/EIP712.sol"; +import "../utils/Nonces.sol"; +import "../utils/Address.sol"; + +/** + * @dev A forwarder compatible with ERC2771 contracts. See {ERC2771Context}. + * + * This forwarder operates on forward requests that include: + * + * * `from`: An address to operate on behalf of. It is required to be equal to the request signer. + * * `to`: The address that should be called. + * * `value`: The amount of native token to attach with the requested call. + * * `gas`: The amount of gas limit that will be forwarded with the requested call. + * * `nonce`: A unique transaction ordering identifier to avoid replayability and request invalidation. + * * `deadline`: A timestamp after which the request is not executable anymore. + * * `data`: Encoded `msg.data` to send with the requested call. + */ +contract ERC2771Forwarder is EIP712, Nonces { + using ECDSA for bytes32; + + struct ForwardRequestData { + address from; + address to; + uint256 value; + uint256 gas; + uint48 deadline; + bytes data; + bytes signature; + } + + bytes32 private constant _FORWARD_REQUEST_TYPEHASH = + keccak256( + "ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,uint48 deadline,bytes data)" + ); + + /** + * @dev Emitted when a `ForwardRequest` is executed. + * + * NOTE: An unsuccessful forward request could be due to an invalid signature, an expired deadline, + * or simply a revert in the requested call. The contract guarantees that the relayer is not able to force + * the requested call to run out of gas. + */ + event ExecutedForwardRequest(address indexed signer, uint256 nonce, bool success); + + /** + * @dev The request `from` doesn't match with the recovered `signer`. + */ + error ERC2771ForwarderInvalidSigner(address signer, address from); + + /** + * @dev The `requestedValue` doesn't match with the available `msgValue`. + */ + error ERC2771ForwarderMismatchedValue(uint256 requestedValue, uint256 msgValue); + + /** + * @dev The request `deadline` has expired. + */ + error ERC2771ForwarderExpiredRequest(uint48 deadline); + + /** + * @dev See {EIP712-constructor}. + */ + constructor(string memory name) EIP712(name, "1") {} + + /** + * @dev Returns `true` if a request is valid for a provided `signature` at the current block timestamp. + * + * A transaction is considered valid when it hasn't expired (deadline is not met), and the signer + * matches the `from` parameter of the signed request. + * + * NOTE: A request may return false here but it won't cause {executeBatch} to revert if a refund + * receiver is provided. + */ + function verify(ForwardRequestData calldata request) public view virtual returns (bool) { + (bool alive, bool signerMatch, ) = _validate(request); + return alive && signerMatch; + } + + /** + * @dev Executes a `request` on behalf of `signature`'s signer using the ERC-2771 protocol. The gas + * provided to the requested call may not be exactly the amount requested, but the call will not run + * out of gas. Will revert if the request is invalid or the call reverts, in this case the nonce is not consumed. + * + * Requirements: + * + * - The request value should be equal to the provided `msg.value`. + * - The request should be valid according to {verify}. + */ + function execute(ForwardRequestData calldata request) public payable virtual { + // We make sure that msg.value and request.value match exactly. + // If the request is invalid or the call reverts, this whole function + // will revert, ensuring value isn't stuck. + if (msg.value != request.value) { + revert ERC2771ForwarderMismatchedValue(request.value, msg.value); + } + + if (!_execute(request, true)) { + revert Address.FailedInnerCall(); + } + } + + /** + * @dev Batch version of {execute} with optional refunding and atomic execution. + * + * In case a batch contains at least one invalid request (see {verify}), the + * request will be skipped and the `refundReceiver` parameter will receive back the + * unused requested value at the end of the execution. This is done to prevent reverting + * the entire batch when a request is invalid or has already been submitted. + * + * If the `refundReceiver` is the `address(0)`, this function will revert when at least + * one of the requests was not valid instead of skipping it. This could be useful if + * a batch is required to get executed atomically (at least at the top-level). For example, + * refunding (and thus atomicity) can be opt-out if the relayer is using a service that avoids + * including reverted transactions. + * + * Requirements: + * + * - The sum of the requests' values should be equal to the provided `msg.value`. + * - All of the requests should be valid (see {verify}) when `refundReceiver` is the zero address. + * + * NOTE: Setting a zero `refundReceiver` guarantees an all-or-nothing requests execution only for + * the first-level forwarded calls. In case a forwarded request calls to a contract with another + * subcall, the second-level call may revert without the top-level call reverting. + */ + function executeBatch( + ForwardRequestData[] calldata requests, + address payable refundReceiver + ) public payable virtual { + bool atomic = refundReceiver == address(0); + + uint256 requestsValue; + uint256 refundValue; + + for (uint256 i; i < requests.length; ++i) { + requestsValue += requests[i].value; + bool success = _execute(requests[i], atomic); + if (!success) { + refundValue += requests[i].value; + } + } + + // The batch should revert if there's a mismatched msg.value provided + // to avoid request value tampering + if (requestsValue != msg.value) { + revert ERC2771ForwarderMismatchedValue(requestsValue, msg.value); + } + + // Some requests with value were invalid (possibly due to frontrunning). + // To avoid leaving ETH in the contract this value is refunded. + if (refundValue != 0) { + // We know refundReceiver != address(0) && requestsValue == msg.value + // meaning we can ensure refundValue is not taken from the original contract's balance + // and refundReceiver is a known account. + Address.sendValue(refundReceiver, refundValue); + } + } + + /** + * @dev Validates if the provided request can be executed at current block timestamp with + * the given `request.signature` on behalf of `request.signer`. + */ + function _validate( + ForwardRequestData calldata request + ) internal view virtual returns (bool alive, bool signerMatch, address signer) { + signer = _recoverForwardRequestSigner(request); + return (request.deadline >= block.timestamp, signer == request.from, signer); + } + + /** + * @dev Recovers the signer of an EIP712 message hash for a forward `request` and its corresponding `signature`. + * See {ECDSA-recover}. + */ + function _recoverForwardRequestSigner(ForwardRequestData calldata request) internal view virtual returns (address) { + return + _hashTypedDataV4( + keccak256( + abi.encode( + _FORWARD_REQUEST_TYPEHASH, + request.from, + request.to, + request.value, + request.gas, + nonces(request.from), + request.deadline, + keccak256(request.data) + ) + ) + ).recover(request.signature); + } + + /** + * @dev Validates and executes a signed request returning the request call `success` value. + * + * Internal function without msg.value validation. + * + * Requirements: + * + * - The caller must have provided enough gas to forward with the call. + * - The request must be valid (see {verify}) if the `requireValidRequest` is true. + * + * Emits an {ExecutedForwardRequest} event. + * + * IMPORTANT: Using this function doesn't check that all the `msg.value` was sent, potentially + * leaving value stuck in the contract. + */ + function _execute( + ForwardRequestData calldata request, + bool requireValidRequest + ) internal virtual returns (bool success) { + (bool alive, bool signerMatch, address signer) = _validate(request); + + // Need to explicitly specify if a revert is required since non-reverting is default for + // batches and reversion is opt-in since it could be useful in some scenarios + if (requireValidRequest) { + if (!alive) { + revert ERC2771ForwarderExpiredRequest(request.deadline); + } + + if (!signerMatch) { + revert ERC2771ForwarderInvalidSigner(signer, request.from); + } + } + + // Ignore an invalid request because requireValidRequest = false + if (signerMatch && alive) { + // Nonce should be used before the call to prevent reusing by reentrancy + uint256 currentNonce = _useNonce(signer); + + (success, ) = request.to.call{gas: request.gas, value: request.value}( + abi.encodePacked(request.data, request.from) + ); + + _checkForwardedGas(request); + + emit ExecutedForwardRequest(signer, currentNonce, success); + } + } + + /** + * @dev Checks if the requested gas was correctly forwarded to the callee. + * + * As a consequence of https://eips.ethereum.org/EIPS/eip-150[EIP-150]: + * - At most `gasleft() - floor(gasleft() / 64)` is forwarded to the callee. + * - At least `floor(gasleft() / 64)` is kept in the caller. + * + * It reverts consuming all the available gas if the forwarded gas is not the requested gas. + * + * IMPORTANT: This function should be called exactly the end of the forwarded call. Any gas consumed + * in between will make room for bypassing this check. + */ + function _checkForwardedGas(ForwardRequestData calldata request) private view { + // To avoid insufficient gas griefing attacks, as referenced in https://ronan.eth.limo/blog/ethereum-gas-dangers/ + // + // A malicious relayer can attempt to shrink the gas forwarded so that the underlying call reverts out-of-gas + // but the forwarding itself still succeeds. In order to make sure that the subcall received sufficient gas, + // we will inspect gasleft() after the forwarding. + // + // Let X be the gas available before the subcall, such that the subcall gets at most X * 63 / 64. + // We can't know X after CALL dynamic costs, but we want it to be such that X * 63 / 64 >= req.gas. + // Let Y be the gas used in the subcall. gasleft() measured immediately after the subcall will be gasleft() = X - Y. + // If the subcall ran out of gas, then Y = X * 63 / 64 and gasleft() = X - Y = X / 64. + // Under this assumption req.gas / 63 > gasleft() is true is true if and only if + // req.gas / 63 > X / 64, or equivalently req.gas > X * 63 / 64. + // This means that if the subcall runs out of gas we are able to detect that insufficient gas was passed. + // + // We will now also see that req.gas / 63 > gasleft() implies that req.gas >= X * 63 / 64. + // The contract guarantees Y <= req.gas, thus gasleft() = X - Y >= X - req.gas. + // - req.gas / 63 > gasleft() + // - req.gas / 63 >= X - req.gas + // - req.gas >= X * 63 / 64 + // In other words if req.gas < X * 63 / 64 then req.gas / 63 <= gasleft(), thus if the relayer behaves honestly + // the forwarding does not revert. + if (gasleft() < request.gas / 63) { + // We explicitly trigger invalid opcode to consume all gas and bubble-up the effects, since + // neither revert or assert consume all gas since Solidity 0.8.0 + // https://docs.soliditylang.org/en/v0.8.0/control-structures.html#panic-via-assert-and-error-via-require + /// @solidity memory-safe-assembly + assembly { + invalid() + } + } + } +} diff --git a/contracts/metatx/MinimalForwarder.sol b/contracts/metatx/MinimalForwarder.sol deleted file mode 100644 index 8ea7a76e816..00000000000 --- a/contracts/metatx/MinimalForwarder.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.9.0) (metatx/MinimalForwarder.sol) - -pragma solidity ^0.8.19; - -import "../utils/cryptography/ECDSA.sol"; -import "../utils/cryptography/EIP712.sol"; - -/** - * @dev Simple minimal forwarder to be used together with an ERC2771 compatible contract. See {ERC2771Context}. - * - * MinimalForwarder is mainly meant for testing, as it is missing features to be a good production-ready forwarder. This - * contract does not intend to have all the properties that are needed for a sound forwarding system. A fully - * functioning forwarding system with good properties requires more complexity. We suggest you look at other projects - * such as the GSN which do have the goal of building a system like that. - */ -contract MinimalForwarder is EIP712 { - using ECDSA for bytes32; - - struct ForwardRequest { - address from; - address to; - uint256 value; - uint256 gas; - uint256 nonce; - bytes data; - } - - bytes32 private constant _TYPEHASH = - keccak256("ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data)"); - - mapping(address => uint256) private _nonces; - - constructor() EIP712("MinimalForwarder", "0.0.1") {} - - function getNonce(address from) public view returns (uint256) { - return _nonces[from]; - } - - function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) { - address signer = _hashTypedDataV4( - keccak256(abi.encode(_TYPEHASH, req.from, req.to, req.value, req.gas, req.nonce, keccak256(req.data))) - ).recover(signature); - return _nonces[req.from] == req.nonce && signer == req.from; - } - - function execute( - ForwardRequest calldata req, - bytes calldata signature - ) public payable returns (bool, bytes memory) { - require(verify(req, signature), "MinimalForwarder: signature does not match request"); - _nonces[req.from] = req.nonce + 1; - - (bool success, bytes memory returndata) = req.to.call{gas: req.gas, value: req.value}( - abi.encodePacked(req.data, req.from) - ); - - // Validate that the relayer has sent enough gas for the call. - // See https://ronan.eth.limo/blog/ethereum-gas-dangers/ - if (gasleft() <= req.gas / 63) { - // We explicitly trigger invalid opcode to consume all gas and bubble-up the effects, since - // neither revert or assert consume all gas since Solidity 0.8.0 - // https://docs.soliditylang.org/en/v0.8.0/control-structures.html#panic-via-assert-and-error-via-require - /// @solidity memory-safe-assembly - assembly { - invalid() - } - } - - return (success, returndata); - } -} diff --git a/contracts/metatx/README.adoc b/contracts/metatx/README.adoc index eccdeaf9740..9f25802e4d8 100644 --- a/contracts/metatx/README.adoc +++ b/contracts/metatx/README.adoc @@ -9,4 +9,4 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/ == Utils -{{MinimalForwarder}} +{{ERC2771Forwarder}} diff --git a/contracts/mocks/AddressFnPointersMock.sol b/contracts/mocks/AddressFnPointersMock.sol new file mode 100644 index 00000000000..c696b3ec1d7 --- /dev/null +++ b/contracts/mocks/AddressFnPointersMock.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/Address.sol"; + +/** + * @dev A mock to expose `Address`'s functions with function pointers. + */ +contract AddressFnPointerMock { + error CustomRevert(); + + function functionCall(address target, bytes memory data) external returns (bytes memory) { + return Address.functionCall(target, data, _customRevert); + } + + function functionCallWithValue(address target, bytes memory data, uint256 value) external returns (bytes memory) { + return Address.functionCallWithValue(target, data, value, _customRevert); + } + + function functionStaticCall(address target, bytes memory data) external view returns (bytes memory) { + return Address.functionStaticCall(target, data, _customRevert); + } + + function functionDelegateCall(address target, bytes memory data) external returns (bytes memory) { + return Address.functionDelegateCall(target, data, _customRevert); + } + + function verifyCallResultFromTarget( + address target, + bool success, + bytes memory returndata + ) external view returns (bytes memory) { + return Address.verifyCallResultFromTarget(target, success, returndata, _customRevert); + } + + function verifyCallResult(bool success, bytes memory returndata) external view returns (bytes memory) { + return Address.verifyCallResult(success, returndata, _customRevert); + } + + function verifyCallResultVoid(bool success, bytes memory returndata) external view returns (bytes memory) { + return Address.verifyCallResult(success, returndata, _customRevertVoid); + } + + function _customRevert() internal pure { + revert CustomRevert(); + } + + function _customRevertVoid() internal pure {} +} diff --git a/contracts/mocks/DummyImplementation.sol b/contracts/mocks/DummyImplementation.sol index 85503c36e74..71761a75561 100644 --- a/contracts/mocks/DummyImplementation.sol +++ b/contracts/mocks/DummyImplementation.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.19; +import "../proxy/ERC1967/ERC1967Utils.sol"; + abstract contract Impl { function version() public pure virtual returns (string memory); } @@ -44,6 +46,11 @@ contract DummyImplementation { function reverts() public pure { require(false, "DummyImplementation reverted"); } + + // Use for forcing an unsafe TransparentUpgradeableProxy admin override + function unsafeOverrideAdmin(address newAdmin) public { + StorageSlot.getAddressSlot(ERC1967Utils.ADMIN_SLOT).value = newAdmin; + } } contract DummyImplementationV2 is DummyImplementation { diff --git a/contracts/mocks/ReentrancyAttack.sol b/contracts/mocks/ReentrancyAttack.sol index 2da8b1f1ae8..df292430133 100644 --- a/contracts/mocks/ReentrancyAttack.sol +++ b/contracts/mocks/ReentrancyAttack.sol @@ -5,8 +5,8 @@ pragma solidity ^0.8.19; import "../utils/Context.sol"; contract ReentrancyAttack is Context { - function callSender(bytes4 data) public { - (bool success, ) = _msgSender().call(abi.encodeWithSelector(data)); + function callSender(bytes calldata data) public { + (bool success, ) = _msgSender().call(data); require(success, "ReentrancyAttack: failed call"); } } diff --git a/contracts/mocks/ReentrancyMock.sol b/contracts/mocks/ReentrancyMock.sol index b4819dd5989..053e53d779e 100644 --- a/contracts/mocks/ReentrancyMock.sol +++ b/contracts/mocks/ReentrancyMock.sol @@ -33,8 +33,7 @@ contract ReentrancyMock is ReentrancyGuard { function countAndCall(ReentrancyAttack attacker) public nonReentrant { _count(); - bytes4 func = bytes4(keccak256("callback()")); - attacker.callSender(func); + attacker.callSender(abi.encodeCall(this.callback, ())); } function _count() private { diff --git a/contracts/mocks/proxy/ClashingImplementation.sol b/contracts/mocks/proxy/ClashingImplementation.sol index 957bc34be69..89904b91f16 100644 --- a/contracts/mocks/proxy/ClashingImplementation.sol +++ b/contracts/mocks/proxy/ClashingImplementation.sol @@ -9,7 +9,7 @@ pragma solidity ^0.8.19; contract ClashingImplementation { event ClashingImplementationCall(); - function changeAdmin(address) external payable { + function upgradeTo(address) external payable { emit ClashingImplementationCall(); } diff --git a/contracts/mocks/proxy/UUPSLegacy.sol b/contracts/mocks/proxy/UUPSLegacy.sol deleted file mode 100644 index f8ea7214ba8..00000000000 --- a/contracts/mocks/proxy/UUPSLegacy.sol +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.19; - -import "./UUPSUpgradeableMock.sol"; - -// This contract implements the pre-4.5 UUPS upgrade function with a rollback test. -// It's used to test that newer UUPS contracts are considered valid upgrades by older UUPS contracts. -contract UUPSUpgradeableLegacyMock is UUPSUpgradeableMock { - // Inlined from ERC1967Upgrade - bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; - - // ERC1967Upgrade._setImplementation is private so we reproduce it here. - // An extra underscore prevents a name clash error. - function __setImplementation(address newImplementation) private { - require(newImplementation.code.length > 0, "ERC1967: new implementation is not a contract"); - StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; - } - - function _upgradeToAndCallSecureLegacyV1(address newImplementation, bytes memory data, bool forceCall) internal { - address oldImplementation = _getImplementation(); - - // Initial upgrade and setup call - __setImplementation(newImplementation); - if (data.length > 0 || forceCall) { - Address.functionDelegateCall(newImplementation, data); - } - - // Perform rollback test if not already in progress - StorageSlot.BooleanSlot storage rollbackTesting = StorageSlot.getBooleanSlot(_ROLLBACK_SLOT); - if (!rollbackTesting.value) { - // Trigger rollback using upgradeTo from the new implementation - rollbackTesting.value = true; - Address.functionDelegateCall(newImplementation, abi.encodeCall(this.upgradeTo, (oldImplementation))); - rollbackTesting.value = false; - // Check rollback was effective - require(oldImplementation == _getImplementation(), "ERC1967Upgrade: upgrade breaks further upgrades"); - // Finally reset to the new implementation and log the upgrade - _upgradeTo(newImplementation); - } - } - - // hooking into the old mechanism - function upgradeTo(address newImplementation) public override { - _upgradeToAndCallSecureLegacyV1(newImplementation, bytes(""), false); - } - - function upgradeToAndCall(address newImplementation, bytes memory data) public payable override { - _upgradeToAndCallSecureLegacyV1(newImplementation, data, false); - } -} diff --git a/contracts/mocks/proxy/UUPSUpgradeableMock.sol b/contracts/mocks/proxy/UUPSUpgradeableMock.sol index 60eed4c9397..77744998b61 100644 --- a/contracts/mocks/proxy/UUPSUpgradeableMock.sol +++ b/contracts/mocks/proxy/UUPSUpgradeableMock.sol @@ -23,10 +23,16 @@ contract UUPSUpgradeableMock is NonUpgradeableMock, UUPSUpgradeable { contract UUPSUpgradeableUnsafeMock is UUPSUpgradeableMock { function upgradeTo(address newImplementation) public override { - _upgradeToAndCall(newImplementation, bytes(""), false); + ERC1967Utils.upgradeToAndCall(newImplementation, bytes(""), false); } function upgradeToAndCall(address newImplementation, bytes memory data) public payable override { - _upgradeToAndCall(newImplementation, data, false); + ERC1967Utils.upgradeToAndCall(newImplementation, data, false); + } +} + +contract UUPSUnsupportedProxiableUUID is UUPSUpgradeableMock { + function proxiableUUID() external pure override returns (bytes32) { + return keccak256("invalid UUID"); } } diff --git a/contracts/mocks/token/ERC1155ReceiverMock.sol b/contracts/mocks/token/ERC1155ReceiverMock.sol index 2b591c058b9..a5d7233cfd5 100644 --- a/contracts/mocks/token/ERC1155ReceiverMock.sol +++ b/contracts/mocks/token/ERC1155ReceiverMock.sol @@ -6,19 +6,26 @@ import "../../token/ERC1155/IERC1155Receiver.sol"; import "../../utils/introspection/ERC165.sol"; contract ERC1155ReceiverMock is ERC165, IERC1155Receiver { - bytes4 private _recRetval; - bool private _recReverts; - bytes4 private _batRetval; - bool private _batReverts; + enum RevertType { + None, + RevertWithoutMessage, + RevertWithMessage, + RevertWithCustomError, + Panic + } + + bytes4 private immutable _recRetval; + bytes4 private immutable _batRetval; + RevertType private immutable _error; event Received(address operator, address from, uint256 id, uint256 value, bytes data, uint256 gas); event BatchReceived(address operator, address from, uint256[] ids, uint256[] values, bytes data, uint256 gas); + error CustomError(bytes4); - constructor(bytes4 recRetval, bool recReverts, bytes4 batRetval, bool batReverts) { + constructor(bytes4 recRetval, bytes4 batRetval, RevertType error) { _recRetval = recRetval; - _recReverts = recReverts; _batRetval = batRetval; - _batReverts = batReverts; + _error = error; } function onERC1155Received( @@ -28,7 +35,17 @@ contract ERC1155ReceiverMock is ERC165, IERC1155Receiver { uint256 value, bytes calldata data ) external returns (bytes4) { - require(!_recReverts, "ERC1155ReceiverMock: reverting on receive"); + if (_error == RevertType.RevertWithoutMessage) { + revert(); + } else if (_error == RevertType.RevertWithMessage) { + revert("ERC1155ReceiverMock: reverting on receive"); + } else if (_error == RevertType.RevertWithCustomError) { + revert CustomError(_recRetval); + } else if (_error == RevertType.Panic) { + uint256 a = uint256(0) / uint256(0); + a; + } + emit Received(operator, from, id, value, data, gasleft()); return _recRetval; } @@ -40,7 +57,17 @@ contract ERC1155ReceiverMock is ERC165, IERC1155Receiver { uint256[] calldata values, bytes calldata data ) external returns (bytes4) { - require(!_batReverts, "ERC1155ReceiverMock: reverting on batch receive"); + if (_error == RevertType.RevertWithoutMessage) { + revert(); + } else if (_error == RevertType.RevertWithMessage) { + revert("ERC1155ReceiverMock: reverting on batch receive"); + } else if (_error == RevertType.RevertWithCustomError) { + revert CustomError(_recRetval); + } else if (_error == RevertType.Panic) { + uint256 a = uint256(0) / uint256(0); + a; + } + emit BatchReceived(operator, from, ids, values, data, gasleft()); return _batRetval; } diff --git a/contracts/mocks/token/ERC20ApprovalMock.sol b/contracts/mocks/token/ERC20ApprovalMock.sol new file mode 100644 index 00000000000..da8f51e1ecc --- /dev/null +++ b/contracts/mocks/token/ERC20ApprovalMock.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../../token/ERC20/ERC20.sol"; + +abstract contract ERC20ApprovalMock is ERC20 { + function _approve(address owner, address spender, uint256 amount, bool) internal virtual override { + super._approve(owner, spender, amount, true); + } +} diff --git a/contracts/mocks/token/ERC20Reentrant.sol b/contracts/mocks/token/ERC20Reentrant.sol index ee803b9e1a5..00ba7426087 100644 --- a/contracts/mocks/token/ERC20Reentrant.sol +++ b/contracts/mocks/token/ERC20Reentrant.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import "../../token/ERC20/ERC20.sol"; -import "../../token/ERC20/extensions/ERC4626.sol"; +import "../../utils/Address.sol"; contract ERC20Reentrant is ERC20("TEST", "TST") { enum Type { diff --git a/contracts/mocks/token/ERC4626LimitsMock.sol b/contracts/mocks/token/ERC4626LimitsMock.sol new file mode 100644 index 00000000000..a47826bb864 --- /dev/null +++ b/contracts/mocks/token/ERC4626LimitsMock.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../../token/ERC20/extensions/ERC4626.sol"; + +abstract contract ERC4626LimitsMock is ERC4626 { + uint256 _maxDeposit; + uint256 _maxMint; + + constructor() { + _maxDeposit = 100 ether; + _maxMint = 100 ether; + } + + function maxDeposit(address) public view override returns (uint256) { + return _maxDeposit; + } + + function maxMint(address) public view override returns (uint256) { + return _maxMint; + } +} diff --git a/contracts/mocks/token/ERC721ConsecutiveMock.sol b/contracts/mocks/token/ERC721ConsecutiveMock.sol index 851e45ceb41..4aae388e094 100644 --- a/contracts/mocks/token/ERC721ConsecutiveMock.sol +++ b/contracts/mocks/token/ERC721ConsecutiveMock.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.19; import "../../token/ERC721/extensions/ERC721Consecutive.sol"; -import "../../token/ERC721/extensions/ERC721Enumerable.sol"; import "../../token/ERC721/extensions/ERC721Pausable.sol"; import "../../token/ERC721/extensions/ERC721Votes.sol"; diff --git a/contracts/mocks/token/ERC721ReceiverMock.sol b/contracts/mocks/token/ERC721ReceiverMock.sol index e97ceacf323..41661317464 100644 --- a/contracts/mocks/token/ERC721ReceiverMock.sol +++ b/contracts/mocks/token/ERC721ReceiverMock.sol @@ -5,19 +5,21 @@ pragma solidity ^0.8.19; import "../../token/ERC721/IERC721Receiver.sol"; contract ERC721ReceiverMock is IERC721Receiver { - enum Error { + enum RevertType { None, - RevertWithMessage, RevertWithoutMessage, + RevertWithMessage, + RevertWithCustomError, Panic } bytes4 private immutable _retval; - Error private immutable _error; + RevertType private immutable _error; event Received(address operator, address from, uint256 tokenId, bytes data, uint256 gas); + error CustomError(bytes4); - constructor(bytes4 retval, Error error) { + constructor(bytes4 retval, RevertType error) { _retval = retval; _error = error; } @@ -28,14 +30,17 @@ contract ERC721ReceiverMock is IERC721Receiver { uint256 tokenId, bytes memory data ) public returns (bytes4) { - if (_error == Error.RevertWithMessage) { - revert("ERC721ReceiverMock: reverting"); - } else if (_error == Error.RevertWithoutMessage) { + if (_error == RevertType.RevertWithoutMessage) { revert(); - } else if (_error == Error.Panic) { + } else if (_error == RevertType.RevertWithMessage) { + revert("ERC721ReceiverMock: reverting"); + } else if (_error == RevertType.RevertWithCustomError) { + revert CustomError(_retval); + } else if (_error == RevertType.Panic) { uint256 a = uint256(0) / uint256(0); a; } + emit Received(operator, from, tokenId, data, gasleft()); return _retval; } diff --git a/contracts/package.json b/contracts/package.json index 4d0f576bbd4..df141192d3d 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,7 +1,7 @@ { "name": "@openzeppelin/contracts", "description": "Secure Smart Contract library for Solidity", - "version": "4.9.0", + "version": "4.9.2", "files": [ "**/*.sol", "/build/contracts/*.json", diff --git a/contracts/proxy/Clones.sol b/contracts/proxy/Clones.sol index 7cdab55f6a4..d859d56452b 100644 --- a/contracts/proxy/Clones.sol +++ b/contracts/proxy/Clones.sol @@ -17,6 +17,11 @@ pragma solidity ^0.8.19; * _Available since v3.4._ */ library Clones { + /** + * @dev A clone instance deployment failed. + */ + error ERC1167FailedCreateClone(); + /** * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`. * @@ -32,7 +37,9 @@ library Clones { mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3)) instance := create(0, 0x09, 0x37) } - require(instance != address(0), "ERC1167: create failed"); + if (instance == address(0)) { + revert ERC1167FailedCreateClone(); + } } /** @@ -52,7 +59,9 @@ library Clones { mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3)) instance := create2(0, 0x09, 0x37, salt) } - require(instance != address(0), "ERC1167: create2 failed"); + if (instance == address(0)) { + revert ERC1167FailedCreateClone(); + } } /** diff --git a/contracts/proxy/ERC1967/ERC1967Proxy.sol b/contracts/proxy/ERC1967/ERC1967Proxy.sol index ea5c204f867..5a752f13d8b 100644 --- a/contracts/proxy/ERC1967/ERC1967Proxy.sol +++ b/contracts/proxy/ERC1967/ERC1967Proxy.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.19; import "../Proxy.sol"; -import "./ERC1967Upgrade.sol"; +import "./ERC1967Utils.sol"; /** * @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an @@ -12,7 +12,7 @@ import "./ERC1967Upgrade.sol"; * https://eips.ethereum.org/EIPS/eip-1967[EIP1967], so that it doesn't conflict with the storage layout of the * implementation behind the proxy. */ -contract ERC1967Proxy is Proxy, ERC1967Upgrade { +contract ERC1967Proxy is Proxy { /** * @dev Initializes the upgradeable proxy with an initial implementation specified by `_logic`. * @@ -20,7 +20,7 @@ contract ERC1967Proxy is Proxy, ERC1967Upgrade { * function call, and allows initializing the storage of the proxy like a Solidity constructor. */ constructor(address _logic, bytes memory _data) payable { - _upgradeToAndCall(_logic, _data, false); + ERC1967Utils.upgradeToAndCall(_logic, _data, false); } /** @@ -31,6 +31,6 @@ contract ERC1967Proxy is Proxy, ERC1967Upgrade { * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` */ function _implementation() internal view virtual override returns (address impl) { - return _getImplementation(); + return ERC1967Utils.getImplementation(); } } diff --git a/contracts/proxy/ERC1967/ERC1967Upgrade.sol b/contracts/proxy/ERC1967/ERC1967Upgrade.sol deleted file mode 100644 index e42a06eb1f3..00000000000 --- a/contracts/proxy/ERC1967/ERC1967Upgrade.sol +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.9.0) (proxy/ERC1967/ERC1967Upgrade.sol) - -pragma solidity ^0.8.19; - -import "../beacon/IBeacon.sol"; -import "../../interfaces/IERC1967.sol"; -import "../../interfaces/draft-IERC1822.sol"; -import "../../utils/Address.sol"; -import "../../utils/StorageSlot.sol"; - -/** - * @dev This abstract contract provides getters and event emitting update functions for - * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots. - * - * _Available since v4.1._ - */ -abstract contract ERC1967Upgrade is IERC1967 { - // This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1 - bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; - - /** - * @dev Storage slot with the address of the current implementation. - * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is - * validated in the constructor. - */ - bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - - /** - * @dev Returns the current implementation address. - */ - function _getImplementation() internal view returns (address) { - return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; - } - - /** - * @dev Stores a new address in the EIP1967 implementation slot. - */ - function _setImplementation(address newImplementation) private { - require(newImplementation.code.length > 0, "ERC1967: new implementation is not a contract"); - StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; - } - - /** - * @dev Perform implementation upgrade - * - * Emits an {Upgraded} event. - */ - function _upgradeTo(address newImplementation) internal { - _setImplementation(newImplementation); - emit Upgraded(newImplementation); - } - - /** - * @dev Perform implementation upgrade with additional setup call. - * - * Emits an {Upgraded} event. - */ - function _upgradeToAndCall(address newImplementation, bytes memory data, bool forceCall) internal { - _upgradeTo(newImplementation); - if (data.length > 0 || forceCall) { - Address.functionDelegateCall(newImplementation, data); - } - } - - /** - * @dev Perform implementation upgrade with security checks for UUPS proxies, and additional setup call. - * - * Emits an {Upgraded} event. - */ - function _upgradeToAndCallUUPS(address newImplementation, bytes memory data, bool forceCall) internal { - // Upgrades from old implementations will perform a rollback test. This test requires the new - // implementation to upgrade back to the old, non-ERC1822 compliant, implementation. Removing - // this special case will break upgrade paths from old UUPS implementation to new ones. - if (StorageSlot.getBooleanSlot(_ROLLBACK_SLOT).value) { - _setImplementation(newImplementation); - } else { - try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) { - require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID"); - } catch { - revert("ERC1967Upgrade: new implementation is not UUPS"); - } - _upgradeToAndCall(newImplementation, data, forceCall); - } - } - - /** - * @dev Storage slot with the admin of the contract. - * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is - * validated in the constructor. - */ - bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; - - /** - * @dev Returns the current admin. - * - * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the - * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. - * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` - */ - function _getAdmin() internal view returns (address) { - return StorageSlot.getAddressSlot(_ADMIN_SLOT).value; - } - - /** - * @dev Stores a new address in the EIP1967 admin slot. - */ - function _setAdmin(address newAdmin) private { - require(newAdmin != address(0), "ERC1967: new admin is the zero address"); - StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin; - } - - /** - * @dev Changes the admin of the proxy. - * - * Emits an {AdminChanged} event. - */ - function _changeAdmin(address newAdmin) internal { - emit AdminChanged(_getAdmin(), newAdmin); - _setAdmin(newAdmin); - } - - /** - * @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy. - * This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)) and is validated in the constructor. - */ - bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; - - /** - * @dev Returns the current beacon. - */ - function _getBeacon() internal view returns (address) { - return StorageSlot.getAddressSlot(_BEACON_SLOT).value; - } - - /** - * @dev Stores a new beacon in the EIP1967 beacon slot. - */ - function _setBeacon(address newBeacon) private { - require(newBeacon.code.length > 0, "ERC1967: new beacon is not a contract"); - require( - IBeacon(newBeacon).implementation().code.length > 0, - "ERC1967: beacon implementation is not a contract" - ); - StorageSlot.getAddressSlot(_BEACON_SLOT).value = newBeacon; - } - - /** - * @dev Perform beacon upgrade with additional setup call. Note: This upgrades the address of the beacon, it does - * not upgrade the implementation contained in the beacon (see {UpgradeableBeacon-_setImplementation} for that). - * - * Emits a {BeaconUpgraded} event. - */ - function _upgradeBeaconToAndCall(address newBeacon, bytes memory data, bool forceCall) internal { - _setBeacon(newBeacon); - emit BeaconUpgraded(newBeacon); - if (data.length > 0 || forceCall) { - Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data); - } - } -} diff --git a/contracts/proxy/ERC1967/ERC1967Utils.sol b/contracts/proxy/ERC1967/ERC1967Utils.sol new file mode 100644 index 00000000000..c244982bf31 --- /dev/null +++ b/contracts/proxy/ERC1967/ERC1967Utils.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/ERC1967/ERC1967Utils.sol) + +pragma solidity ^0.8.20; + +import "../beacon/IBeacon.sol"; +import "../../utils/Address.sol"; +import "../../utils/StorageSlot.sol"; + +/** + * @dev This abstract contract provides getters and event emitting update functions for + * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots. + * + * _Available since v4.1._ + */ +library ERC1967Utils { + // We re-declare ERC-1967 events here because they can't be used directly from IERC1967. + // This will be fixed in Solidity 0.8.21. At that point we should remove these events. + /** + * @dev Emitted when the implementation is upgraded. + */ + event Upgraded(address indexed implementation); + + /** + * @dev Emitted when the admin account has changed. + */ + event AdminChanged(address previousAdmin, address newAdmin); + + /** + * @dev Emitted when the beacon is changed. + */ + event BeaconUpgraded(address indexed beacon); + + /** + * @dev Storage slot with the address of the current implementation. + * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is + * validated in the constructor. + */ + // solhint-disable-next-line private-vars-leading-underscore + bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /** + * @dev The `implementation` of the proxy is invalid. + */ + error ERC1967InvalidImplementation(address implementation); + + /** + * @dev The `admin` of the proxy is invalid. + */ + error ERC1967InvalidAdmin(address admin); + + /** + * @dev The `beacon` of the proxy is invalid. + */ + error ERC1967InvalidBeacon(address beacon); + + /** + * @dev Returns the current implementation address. + */ + function getImplementation() internal view returns (address) { + return StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value; + } + + /** + * @dev Stores a new address in the EIP1967 implementation slot. + */ + function _setImplementation(address newImplementation) private { + if (newImplementation.code.length == 0) { + revert ERC1967InvalidImplementation(newImplementation); + } + StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = newImplementation; + } + + /** + * @dev Perform implementation upgrade + * + * Emits an {IERC1967-Upgraded} event. + */ + function upgradeTo(address newImplementation) internal { + _setImplementation(newImplementation); + emit Upgraded(newImplementation); + } + + /** + * @dev Perform implementation upgrade with additional setup call. + * + * Emits an {IERC1967-Upgraded} event. + */ + function upgradeToAndCall(address newImplementation, bytes memory data, bool forceCall) internal { + upgradeTo(newImplementation); + if (data.length > 0 || forceCall) { + Address.functionDelegateCall(newImplementation, data); + } + } + + /** + * @dev Storage slot with the admin of the contract. + * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is + * validated in the constructor. + */ + // solhint-disable-next-line private-vars-leading-underscore + bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /** + * @dev Returns the current admin. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` + */ + function getAdmin() internal view returns (address) { + return StorageSlot.getAddressSlot(ADMIN_SLOT).value; + } + + /** + * @dev Stores a new address in the EIP1967 admin slot. + */ + function _setAdmin(address newAdmin) private { + if (newAdmin == address(0)) { + revert ERC1967InvalidAdmin(address(0)); + } + StorageSlot.getAddressSlot(ADMIN_SLOT).value = newAdmin; + } + + /** + * @dev Changes the admin of the proxy. + * + * Emits an {IERC1967-AdminChanged} event. + */ + function changeAdmin(address newAdmin) internal { + emit AdminChanged(getAdmin(), newAdmin); + _setAdmin(newAdmin); + } + + /** + * @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy. + * This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1) and is validated in the constructor. + */ + // solhint-disable-next-line private-vars-leading-underscore + bytes32 internal constant BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; + + /** + * @dev Returns the current beacon. + */ + function getBeacon() internal view returns (address) { + return StorageSlot.getAddressSlot(BEACON_SLOT).value; + } + + /** + * @dev Stores a new beacon in the EIP1967 beacon slot. + */ + function _setBeacon(address newBeacon) private { + if (newBeacon.code.length == 0) { + revert ERC1967InvalidBeacon(newBeacon); + } + + address beaconImplementation = IBeacon(newBeacon).implementation(); + if (beaconImplementation.code.length == 0) { + revert ERC1967InvalidImplementation(beaconImplementation); + } + + StorageSlot.getAddressSlot(BEACON_SLOT).value = newBeacon; + } + + /** + * @dev Perform beacon upgrade with additional setup call. Note: This upgrades the address of the beacon, it does + * not upgrade the implementation contained in the beacon (see {UpgradeableBeacon-_setImplementation} for that). + * + * Emits an {IERC1967-BeaconUpgraded} event. + */ + function upgradeBeaconToAndCall(address newBeacon, bytes memory data, bool forceCall) internal { + _setBeacon(newBeacon); + emit BeaconUpgraded(newBeacon); + if (data.length > 0 || forceCall) { + Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data); + } + } +} diff --git a/contracts/proxy/README.adoc b/contracts/proxy/README.adoc index 89717a7bf73..3c4a78d1905 100644 --- a/contracts/proxy/README.adoc +++ b/contracts/proxy/README.adoc @@ -11,12 +11,12 @@ Most of the proxies below are built on an abstract base contract. 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. -- {ERC1967Upgrade}: Internal functions to get and set the storage slots defined in EIP1967. +- {ERC1967Utils}: Internal functions to get and set the storage slots defined in EIP1967. - {ERC1967Proxy}: A proxy using EIP1967 storage slots. Not upgradeable by default. There are two alternative ways to add upgradeability to an ERC1967 proxy. Their differences are explained below in <>. -- {TransparentUpgradeableProxy}: A proxy with a built in admin and upgrade interface. +- {TransparentUpgradeableProxy}: A proxy with a built-in immutable admin and upgrade interface. - {UUPSUpgradeable}: An upgradeability mechanism to be included in the implementation contract. CAUTION: Using upgradeable proxies correctly and securely is a difficult task that requires deep knowledge of the proxy pattern, Solidity, and the EVM. Unless you want a lot of low level control, we recommend using the xref:upgrades-plugins::index.adoc[OpenZeppelin Upgrades Plugins] for Truffle and Hardhat. @@ -60,7 +60,7 @@ The current implementation of this security mechanism uses https://eips.ethereum {{ERC1967Proxy}} -{{ERC1967Upgrade}} +{{ERC1967Utils}} == Transparent Proxy diff --git a/contracts/proxy/beacon/BeaconProxy.sol b/contracts/proxy/beacon/BeaconProxy.sol index d603e38a7f7..dc6b5c90b67 100644 --- a/contracts/proxy/beacon/BeaconProxy.sol +++ b/contracts/proxy/beacon/BeaconProxy.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.19; import "./IBeacon.sol"; import "../Proxy.sol"; -import "../ERC1967/ERC1967Upgrade.sol"; +import "../ERC1967/ERC1967Utils.sol"; /** * @dev This contract implements a proxy that gets the implementation address for each call from an {UpgradeableBeacon}. @@ -15,7 +15,7 @@ import "../ERC1967/ERC1967Upgrade.sol"; * * _Available since v3.4._ */ -contract BeaconProxy is Proxy, ERC1967Upgrade { +contract BeaconProxy is Proxy { /** * @dev Initializes the proxy with `beacon`. * @@ -28,13 +28,13 @@ contract BeaconProxy is Proxy, ERC1967Upgrade { * - `beacon` must be a contract with the interface {IBeacon}. */ constructor(address beacon, bytes memory data) payable { - _upgradeBeaconToAndCall(beacon, data, false); + ERC1967Utils.upgradeBeaconToAndCall(beacon, data, false); } /** * @dev Returns the current implementation address of the associated beacon. */ function _implementation() internal view virtual override returns (address) { - return IBeacon(_getBeacon()).implementation(); + return IBeacon(ERC1967Utils.getBeacon()).implementation(); } } diff --git a/contracts/proxy/beacon/UpgradeableBeacon.sol b/contracts/proxy/beacon/UpgradeableBeacon.sol index 37d27f67a55..c5e64ea5958 100644 --- a/contracts/proxy/beacon/UpgradeableBeacon.sol +++ b/contracts/proxy/beacon/UpgradeableBeacon.sol @@ -15,6 +15,11 @@ import "../../access/Ownable.sol"; contract UpgradeableBeacon is IBeacon, Ownable { address private _implementation; + /** + * @dev The `implementation` of the beacon is invalid. + */ + error BeaconInvalidImplementation(address implementation); + /** * @dev Emitted when the implementation returned by the beacon is changed. */ @@ -57,7 +62,9 @@ contract UpgradeableBeacon is IBeacon, Ownable { * - `newImplementation` must be a contract. */ function _setImplementation(address newImplementation) private { - require(newImplementation.code.length > 0, "UpgradeableBeacon: implementation is not a contract"); + if (newImplementation.code.length == 0) { + revert BeaconInvalidImplementation(newImplementation); + } _implementation = newImplementation; } } diff --git a/contracts/proxy/transparent/ProxyAdmin.sol b/contracts/proxy/transparent/ProxyAdmin.sol index 490f552f852..e8578a58561 100644 --- a/contracts/proxy/transparent/ProxyAdmin.sol +++ b/contracts/proxy/transparent/ProxyAdmin.sol @@ -16,17 +16,6 @@ contract ProxyAdmin is Ownable { */ constructor(address initialOwner) Ownable(initialOwner) {} - /** - * @dev Changes the admin of `proxy` to `newAdmin`. - * - * Requirements: - * - * - This contract must be the current admin of `proxy`. - */ - function changeProxyAdmin(ITransparentUpgradeableProxy proxy, address newAdmin) public virtual onlyOwner { - proxy.changeAdmin(newAdmin); - } - /** * @dev Upgrades `proxy` to `implementation`. See {TransparentUpgradeableProxy-upgradeTo}. * diff --git a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol index 01f55e99b8d..71ce665d0c1 100644 --- a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.19; import "../ERC1967/ERC1967Proxy.sol"; +import "../../interfaces/IERC1967.sol"; /** * @dev Interface for {TransparentUpgradeableProxy}. In order to implement transparency, {TransparentUpgradeableProxy} @@ -12,15 +13,13 @@ import "../ERC1967/ERC1967Proxy.sol"; * include them in the ABI so this interface must be used to interact with it. */ interface ITransparentUpgradeableProxy is IERC1967 { - function changeAdmin(address) external; - function upgradeTo(address) external; function upgradeToAndCall(address, bytes memory) external payable; } /** - * @dev This contract implements a proxy that is upgradeable by an admin. + * @dev This contract implements a proxy that is upgradeable by an immutable admin. * * To avoid https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357[proxy selector * clashing], which can potentially be used in an attack, this contract uses the @@ -30,15 +29,16 @@ interface ITransparentUpgradeableProxy is IERC1967 { * 1. If any account other than the admin calls the proxy, the call will be forwarded to the implementation, even if * that call matches one of the admin functions exposed by the proxy itself. * 2. If the admin calls the proxy, it can access the admin functions, but its calls will never be forwarded to the - * implementation. If the admin tries to call a function on the implementation it will fail with an error that says - * "admin cannot fallback to proxy target". + * implementation. If the admin tries to call a function on the implementation it will fail with an error indicating the + * proxy admin cannot fallback to the target implementation. * - * These properties mean that the admin account can only be used for admin actions like upgrading the proxy or changing - * the admin, so it's best if it's a dedicated account that is not used for anything else. This will avoid headaches due - * to sudden errors when trying to call a function from the proxy implementation. + * These properties mean that the admin account can only be used for upgrading the proxy, so it's best if it's a dedicated + * account that is not used for anything else. This will avoid headaches due to sudden errors when trying to call a function + * from the proxy implementation. * * Our recommendation is for the dedicated account to be an instance of the {ProxyAdmin} contract. If set up this way, - * you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy. + * you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy, which extends from the + * {Ownable} contract to allow for changing the proxy's admin owner. * * NOTE: The real interface of this proxy is that defined in `ITransparentUpgradeableProxy`. This contract does not * inherit from that interface, and instead the admin functions are implicitly implemented using a custom dispatch @@ -46,35 +46,56 @@ interface ITransparentUpgradeableProxy is IERC1967 { * fully implement transparency without decoding reverts caused by selector clashes between the proxy and the * implementation. * + * IMPORTANT: This contract avoids unnecessary storage reads by setting the admin only during construction as an immutable variable, + * preventing any changes thereafter. However, the admin slot defined in ERC-1967 can still be overwritten by the implementation + * logic pointed to by this proxy. In such cases, the contract may end up in an undesirable state where the admin slot is different + * from the actual admin. + * * WARNING: It is not recommended to extend this contract to add additional external functions. If you do so, the compiler * will not check that there are no selector conflicts, due to the note above. A selector clash between any new function * and the functions declared in {ITransparentUpgradeableProxy} will be resolved in favor of the new one. This could * render the admin operations inaccessible, which could prevent upgradeability. Transparency may also be compromised. */ contract TransparentUpgradeableProxy is ERC1967Proxy { + // An immutable address for the admin avoid unnecessary SLOADs before each call + // at the expense of removing the ability to change the admin once it's set. + // This is acceptable if the admin is always a ProxyAdmin instance or similar contract + // with its own ability to transfer the permissions to another account. + address private immutable _admin; + + /** + * @dev The proxy caller is the current admin, and can't fallback to the proxy target. + */ + error ProxyDeniedAdminAccess(); + + /** + * @dev msg.value is not 0. + */ + error ProxyNonPayableFunction(); + /** * @dev Initializes an upgradeable proxy managed by `_admin`, backed by the implementation at `_logic`, and * optionally initialized with `_data` as explained in {ERC1967Proxy-constructor}. */ constructor(address _logic, address admin_, bytes memory _data) payable ERC1967Proxy(_logic, _data) { - _changeAdmin(admin_); + _admin = admin_; + // Set the storage value and emit an event for ERC-1967 compatibility + ERC1967Utils.changeAdmin(admin_); } /** * @dev If caller is the admin process the call internally, otherwise transparently fallback to the proxy behavior */ function _fallback() internal virtual override { - if (msg.sender == _getAdmin()) { + if (msg.sender == _admin) { bytes memory ret; bytes4 selector = msg.sig; if (selector == ITransparentUpgradeableProxy.upgradeTo.selector) { ret = _dispatchUpgradeTo(); } else if (selector == ITransparentUpgradeableProxy.upgradeToAndCall.selector) { ret = _dispatchUpgradeToAndCall(); - } else if (selector == ITransparentUpgradeableProxy.changeAdmin.selector) { - ret = _dispatchChangeAdmin(); } else { - revert("TransparentUpgradeableProxy: admin cannot fallback to proxy target"); + revert ProxyDeniedAdminAccess(); } assembly { return(add(ret, 0x20), mload(ret)) @@ -84,20 +105,6 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { } } - /** - * @dev Changes the admin of the proxy. - * - * Emits an {AdminChanged} event. - */ - function _dispatchChangeAdmin() private returns (bytes memory) { - _requireZeroValue(); - - address newAdmin = abi.decode(msg.data[4:], (address)); - _changeAdmin(newAdmin); - - return ""; - } - /** * @dev Upgrade the implementation of the proxy. */ @@ -105,7 +112,7 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { _requireZeroValue(); address newImplementation = abi.decode(msg.data[4:], (address)); - _upgradeToAndCall(newImplementation, bytes(""), false); + ERC1967Utils.upgradeToAndCall(newImplementation, bytes(""), false); return ""; } @@ -117,7 +124,7 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { */ function _dispatchUpgradeToAndCall() private returns (bytes memory) { (address newImplementation, bytes memory data) = abi.decode(msg.data[4:], (address, bytes)); - _upgradeToAndCall(newImplementation, data, true); + ERC1967Utils.upgradeToAndCall(newImplementation, data, true); return ""; } @@ -127,6 +134,8 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { * non-payability of function implemented through dispatchers while still allowing value to pass through. */ function _requireZeroValue() private { - require(msg.value == 0); + if (msg.value != 0) { + revert ProxyNonPayableFunction(); + } } } diff --git a/contracts/proxy/utils/Initializable.sol b/contracts/proxy/utils/Initializable.sol index a4288791839..3ae5e4a6575 100644 --- a/contracts/proxy/utils/Initializable.sol +++ b/contracts/proxy/utils/Initializable.sol @@ -67,6 +67,16 @@ abstract contract Initializable { */ bool private _initializing; + /** + * @dev The contract is already initialized. + */ + error AlreadyInitialized(); + + /** + * @dev The contract is not initializing. + */ + error NotInitializing(); + /** * @dev Triggered when the contract has been initialized or reinitialized. */ @@ -83,10 +93,9 @@ abstract contract Initializable { */ modifier initializer() { bool isTopLevelCall = !_initializing; - require( - (isTopLevelCall && _initialized < 1) || (address(this).code.length == 0 && _initialized == 1), - "Initializable: contract is already initialized" - ); + if (!(isTopLevelCall && _initialized < 1) && !(address(this).code.length == 0 && _initialized == 1)) { + revert AlreadyInitialized(); + } _initialized = 1; if (isTopLevelCall) { _initializing = true; @@ -117,7 +126,9 @@ abstract contract Initializable { * Emits an {Initialized} event. */ modifier reinitializer(uint8 version) { - require(!_initializing && _initialized < version, "Initializable: contract is already initialized"); + if (_initializing || _initialized >= version) { + revert AlreadyInitialized(); + } _initialized = version; _initializing = true; _; @@ -130,7 +141,9 @@ abstract contract Initializable { * {initializer} and {reinitializer} modifiers, directly or indirectly. */ modifier onlyInitializing() { - require(_initializing, "Initializable: contract is not initializing"); + if (!_initializing) { + revert NotInitializing(); + } _; } @@ -143,7 +156,9 @@ abstract contract Initializable { * Emits an {Initialized} event the first time it is successfully executed. */ function _disableInitializers() internal virtual { - require(!_initializing, "Initializable: contract is initializing"); + if (_initializing) { + revert AlreadyInitialized(); + } if (_initialized != type(uint8).max) { _initialized = type(uint8).max; emit Initialized(type(uint8).max); diff --git a/contracts/proxy/utils/UUPSUpgradeable.sol b/contracts/proxy/utils/UUPSUpgradeable.sol index 1fd73247c9d..07de178bc4c 100644 --- a/contracts/proxy/utils/UUPSUpgradeable.sol +++ b/contracts/proxy/utils/UUPSUpgradeable.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.19; import "../../interfaces/draft-IERC1822.sol"; -import "../ERC1967/ERC1967Upgrade.sol"; +import "../ERC1967/ERC1967Utils.sol"; /** * @dev An upgradeability mechanism designed for UUPS proxies. The functions included here can perform an upgrade of an @@ -18,10 +18,20 @@ import "../ERC1967/ERC1967Upgrade.sol"; * * _Available since v4.1._ */ -abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { +abstract contract UUPSUpgradeable is IERC1822Proxiable { /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment address private immutable __self = address(this); + /** + * @dev The call is from an unauthorized context. + */ + error UUPSUnauthorizedCallContext(); + + /** + * @dev The storage `slot` is unsupported as a UUID. + */ + error UUPSUnsupportedProxiableUUID(bytes32 slot); + /** * @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 @@ -30,8 +40,12 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * fail. */ modifier onlyProxy() { - require(address(this) != __self, "Function must be called through delegatecall"); - require(_getImplementation() == __self, "Function must be called through active proxy"); + if ( + address(this) == __self || // Must be called through delegatecall + ERC1967Utils.getImplementation() != __self // Must be called through an active proxy + ) { + revert UUPSUnauthorizedCallContext(); + } _; } @@ -40,7 +54,10 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * callable on the implementing contract but not through proxies. */ modifier notDelegated() { - require(address(this) == __self, "UUPSUpgradeable: must not be called through delegatecall"); + if (address(this) != __self) { + // Must not be called through delegatecall + revert UUPSUnauthorizedCallContext(); + } _; } @@ -53,7 +70,7 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier. */ function proxiableUUID() external view virtual notDelegated returns (bytes32) { - return _IMPLEMENTATION_SLOT; + return ERC1967Utils.IMPLEMENTATION_SLOT; } /** @@ -96,4 +113,21 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * ``` */ function _authorizeUpgrade(address newImplementation) internal virtual; + + /** + * @dev Perform implementation upgrade with security checks for UUPS proxies, and additional setup call. + * + * Emits an {IERC1967-Upgraded} event. + */ + function _upgradeToAndCallUUPS(address newImplementation, bytes memory data, bool forceCall) private { + try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) { + if (slot != ERC1967Utils.IMPLEMENTATION_SLOT) { + revert UUPSUnsupportedProxiableUUID(slot); + } + ERC1967Utils.upgradeToAndCall(newImplementation, data, forceCall); + } catch { + // The implementation is not UUPS + revert ERC1967Utils.ERC1967InvalidImplementation(newImplementation); + } + } } diff --git a/contracts/security/Pausable.sol b/contracts/security/Pausable.sol index cdf3ee2cda8..dc0afa66339 100644 --- a/contracts/security/Pausable.sol +++ b/contracts/security/Pausable.sol @@ -15,6 +15,8 @@ import "../utils/Context.sol"; * simply including this module, only once the modifiers are put in place. */ abstract contract Pausable is Context { + bool private _paused; + /** * @dev Emitted when the pause is triggered by `account`. */ @@ -25,7 +27,15 @@ abstract contract Pausable is Context { */ event Unpaused(address account); - bool private _paused; + /** + * @dev The operation failed because the contract is paused. + */ + error EnforcedPause(); + + /** + * @dev The operation failed because the contract is not paused. + */ + error ExpectedPause(); /** * @dev Initializes the contract in unpaused state. @@ -69,14 +79,18 @@ abstract contract Pausable is Context { * @dev Throws if the contract is paused. */ function _requireNotPaused() internal view virtual { - require(!paused(), "Pausable: paused"); + if (paused()) { + revert EnforcedPause(); + } } /** * @dev Throws if the contract is not paused. */ function _requirePaused() internal view virtual { - require(paused(), "Pausable: not paused"); + if (!paused()) { + revert ExpectedPause(); + } } /** diff --git a/contracts/security/ReentrancyGuard.sol b/contracts/security/ReentrancyGuard.sol index 88a86ae7eee..40ae5b05050 100644 --- a/contracts/security/ReentrancyGuard.sol +++ b/contracts/security/ReentrancyGuard.sol @@ -36,6 +36,11 @@ abstract contract ReentrancyGuard { uint256 private _status; + /** + * @dev Unauthorized reentrant call. + */ + error ReentrancyGuardReentrantCall(); + constructor() { _status = _NOT_ENTERED; } @@ -55,7 +60,9 @@ abstract contract ReentrancyGuard { function _nonReentrantBefore() private { // On the first call to nonReentrant, _status will be _NOT_ENTERED - require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); + if (_status == _ENTERED) { + revert ReentrancyGuardReentrantCall(); + } // Any calls to nonReentrant after this point will fail _status = _ENTERED; diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol index c2f217d7e3a..e4bd74826e6 100644 --- a/contracts/token/ERC1155/ERC1155.sol +++ b/contracts/token/ERC1155/ERC1155.sol @@ -8,6 +8,8 @@ import "./IERC1155Receiver.sol"; import "./extensions/IERC1155MetadataURI.sol"; import "../../utils/Context.sol"; import "../../utils/introspection/ERC165.sol"; +import "../../utils/Arrays.sol"; +import "../../interfaces/draft-IERC6093.sol"; /** * @dev Implementation of the basic standard multi-token. @@ -16,7 +18,10 @@ import "../../utils/introspection/ERC165.sol"; * * _Available since v3.1._ */ -contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { +abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IERC1155Errors { + using Arrays for uint256[]; + using Arrays for address[]; + // Mapping from token ID to account balances mapping(uint256 => mapping(address => uint256)) private _balances; @@ -79,12 +84,14 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { address[] memory accounts, uint256[] memory ids ) public view virtual returns (uint256[] memory) { - require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch"); + if (accounts.length != ids.length) { + revert ERC1155InvalidArrayLength(ids.length, accounts.length); + } uint256[] memory batchBalances = new uint256[](accounts.length); for (uint256 i = 0; i < accounts.length; ++i) { - batchBalances[i] = balanceOf(accounts[i], ids[i]); + batchBalances[i] = balanceOf(accounts.unsafeMemoryAccess(i), ids.unsafeMemoryAccess(i)); } return batchBalances; @@ -108,10 +115,9 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * @dev See {IERC1155-safeTransferFrom}. */ function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes memory data) public virtual { - require( - from == _msgSender() || isApprovedForAll(from, _msgSender()), - "ERC1155: caller is not token owner or approved" - ); + if (from != _msgSender() && !isApprovedForAll(from, _msgSender())) { + revert ERC1155MissingApprovalForAll(_msgSender(), from); + } _safeTransferFrom(from, to, id, amount, data); } @@ -125,10 +131,9 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { uint256[] memory amounts, bytes memory data ) public virtual { - require( - from == _msgSender() || isApprovedForAll(from, _msgSender()), - "ERC1155: caller is not token owner or approved" - ); + if (from != _msgSender() && !isApprovedForAll(from, _msgSender())) { + revert ERC1155MissingApprovalForAll(_msgSender(), from); + } _safeBatchTransferFrom(from, to, ids, amounts, data); } @@ -149,17 +154,21 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { uint256[] memory amounts, bytes memory data ) internal virtual { - require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch"); + if (ids.length != amounts.length) { + revert ERC1155InvalidArrayLength(ids.length, amounts.length); + } address operator = _msgSender(); for (uint256 i = 0; i < ids.length; ++i) { - uint256 id = ids[i]; - uint256 amount = amounts[i]; + uint256 id = ids.unsafeMemoryAccess(i); + uint256 amount = amounts.unsafeMemoryAccess(i); if (from != address(0)) { uint256 fromBalance = _balances[id][from]; - require(fromBalance >= amount, "ERC1155: insufficient balance for transfer"); + if (fromBalance < amount) { + revert ERC1155InsufficientBalance(from, fromBalance, amount, id); + } unchecked { _balances[id][from] = fromBalance - amount; } @@ -171,8 +180,8 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { } if (ids.length == 1) { - uint256 id = ids[0]; - uint256 amount = amounts[0]; + uint256 id = ids.unsafeMemoryAccess(0); + uint256 amount = amounts.unsafeMemoryAccess(0); emit TransferSingle(operator, from, to, id, amount); if (to != address(0)) { _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data); @@ -198,8 +207,12 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * acceptance magic value. */ function _safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes memory data) internal { - require(to != address(0), "ERC1155: transfer to the zero address"); - require(from != address(0), "ERC1155: transfer from the zero address"); + if (to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + if (from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } (uint256[] memory ids, uint256[] memory amounts) = _asSingletonArrays(id, amount); _update(from, to, ids, amounts, data); } @@ -221,8 +234,12 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { uint256[] memory amounts, bytes memory data ) internal { - require(to != address(0), "ERC1155: transfer to the zero address"); - require(from != address(0), "ERC1155: transfer from the zero address"); + if (to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + if (from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } _update(from, to, ids, amounts, data); } @@ -261,7 +278,9 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * acceptance magic value. */ function _mint(address to, uint256 id, uint256 amount, bytes memory data) internal { - require(to != address(0), "ERC1155: mint to the zero address"); + if (to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } (uint256[] memory ids, uint256[] memory amounts) = _asSingletonArrays(id, amount); _update(address(0), to, ids, amounts, data); } @@ -278,7 +297,9 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * acceptance magic value. */ function _mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) internal { - require(to != address(0), "ERC1155: mint to the zero address"); + if (to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } _update(address(0), to, ids, amounts, data); } @@ -293,7 +314,9 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * - `from` must have at least `amount` tokens of token type `id`. */ function _burn(address from, uint256 id, uint256 amount) internal { - require(from != address(0), "ERC1155: burn from the zero address"); + if (from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } (uint256[] memory ids, uint256[] memory amounts) = _asSingletonArrays(id, amount); _update(from, address(0), ids, amounts, ""); } @@ -308,7 +331,9 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * - `ids` and `amounts` must have the same length. */ function _burnBatch(address from, uint256[] memory ids, uint256[] memory amounts) internal { - require(from != address(0), "ERC1155: burn from the zero address"); + if (from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } _update(from, address(0), ids, amounts, ""); } @@ -318,7 +343,9 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * Emits an {ApprovalForAll} event. */ function _setApprovalForAll(address owner, address operator, bool approved) internal virtual { - require(owner != operator, "ERC1155: setting approval status for self"); + if (owner == operator) { + revert ERC1155InvalidOperator(operator); + } _operatorApprovals[owner][operator] = approved; emit ApprovalForAll(owner, operator, approved); } @@ -334,12 +361,19 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { if (to.code.length > 0) { try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) { if (response != IERC1155Receiver.onERC1155Received.selector) { - revert("ERC1155: ERC1155Receiver rejected tokens"); + // Tokens rejected + revert ERC1155InvalidReceiver(to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + // non-ERC1155Receiver implementer + revert ERC1155InvalidReceiver(to); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } } - } catch Error(string memory reason) { - revert(reason); - } catch { - revert("ERC1155: transfer to non-ERC1155Receiver implementer"); } } } @@ -357,12 +391,19 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { bytes4 response ) { if (response != IERC1155Receiver.onERC1155BatchReceived.selector) { - revert("ERC1155: ERC1155Receiver rejected tokens"); + // Tokens rejected + revert ERC1155InvalidReceiver(to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + // non-ERC1155Receiver implementer + revert ERC1155InvalidReceiver(to); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } } - } catch Error(string memory reason) { - revert(reason); - } catch { - revert("ERC1155: transfer to non-ERC1155Receiver implementer"); } } } diff --git a/contracts/token/ERC1155/extensions/ERC1155Burnable.sol b/contracts/token/ERC1155/extensions/ERC1155Burnable.sol index c079f07e1e0..d7bee05d924 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Burnable.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Burnable.sol @@ -13,19 +13,17 @@ import "../ERC1155.sol"; */ abstract contract ERC1155Burnable is ERC1155 { function burn(address account, uint256 id, uint256 value) public virtual { - require( - account == _msgSender() || isApprovedForAll(account, _msgSender()), - "ERC1155: caller is not token owner or approved" - ); + if (account != _msgSender() && !isApprovedForAll(account, _msgSender())) { + revert ERC1155MissingApprovalForAll(_msgSender(), account); + } _burn(account, id, value); } function burnBatch(address account, uint256[] memory ids, uint256[] memory values) public virtual { - require( - account == _msgSender() || isApprovedForAll(account, _msgSender()), - "ERC1155: caller is not token owner or approved" - ); + if (account != _msgSender() && !isApprovedForAll(account, _msgSender())) { + revert ERC1155MissingApprovalForAll(_msgSender(), account); + } _burnBatch(account, ids, values); } diff --git a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol index 95f006e6f55..f8357062c28 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol @@ -35,8 +35,7 @@ abstract contract ERC1155Pausable is ERC1155, Pausable { uint256[] memory ids, uint256[] memory amounts, bytes memory data - ) internal virtual override { - require(!paused(), "ERC1155Pausable: token transfer while paused"); + ) internal virtual override whenNotPaused { super._update(from, to, ids, amounts, data); } } diff --git a/contracts/token/ERC1155/extensions/ERC1155Supply.sol b/contracts/token/ERC1155/extensions/ERC1155Supply.sol index 4ad83ea0289..f32fbb74bb7 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Supply.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Supply.sol @@ -66,11 +66,8 @@ abstract contract ERC1155Supply is ERC1155 { for (uint256 i = 0; i < ids.length; ++i) { uint256 id = ids[i]; uint256 amount = amounts[i]; - uint256 supply = _totalSupply[id]; - require(supply >= amount, "ERC1155: burn amount exceeds totalSupply"); + _totalSupply[id] -= amount; unchecked { - // Overflow not possible: amounts[i] <= totalSupply(i) - _totalSupply[id] = supply - amount; // Overflow not possible: sum(amounts[i]) <= sum(totalSupply(i)) <= totalSupplyAll totalBurnAmount += amount; } diff --git a/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol b/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol index 5b0da2b33a2..79782a42b4e 100644 --- a/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol +++ b/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol @@ -42,8 +42,8 @@ abstract contract ERC1155URIStorage is ERC1155 { function uri(uint256 tokenId) public view virtual override returns (string memory) { string memory tokenURI = _tokenURIs[tokenId]; - // If token URI is set, concatenate base URI and tokenURI (via abi.encodePacked). - return bytes(tokenURI).length > 0 ? string(abi.encodePacked(_baseURI, tokenURI)) : super.uri(tokenId); + // If token URI is set, concatenate base URI and tokenURI (via string.concat). + return bytes(tokenURI).length > 0 ? string.concat(_baseURI, tokenURI) : super.uri(tokenId); } /** diff --git a/contracts/token/ERC1155/utils/ERC1155Holder.sol b/contracts/token/ERC1155/utils/ERC1155Holder.sol index b06a37f1821..d24a1a53a03 100644 --- a/contracts/token/ERC1155/utils/ERC1155Holder.sol +++ b/contracts/token/ERC1155/utils/ERC1155Holder.sol @@ -13,7 +13,7 @@ import "./ERC1155Receiver.sol"; * * @dev _Available since v3.1._ */ -contract ERC1155Holder is ERC1155Receiver { +abstract contract ERC1155Holder is ERC1155Receiver { function onERC1155Received( address, address, diff --git a/contracts/token/ERC20/ERC20.sol b/contracts/token/ERC20/ERC20.sol index 4db525a7ad7..5ddebf22b25 100644 --- a/contracts/token/ERC20/ERC20.sol +++ b/contracts/token/ERC20/ERC20.sol @@ -6,6 +6,7 @@ pragma solidity ^0.8.19; import "./IERC20.sol"; import "./extensions/IERC20Metadata.sol"; import "../../utils/Context.sol"; +import "../../interfaces/draft-IERC6093.sol"; /** * @dev Implementation of the {IERC20} interface. @@ -34,7 +35,7 @@ import "../../utils/Context.sol"; * functions have been added to mitigate the well-known issues around setting * allowances. See {IERC20-approve}. */ -contract ERC20 is Context, IERC20, IERC20Metadata { +abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { mapping(address => uint256) private _balances; mapping(address => mapping(address => uint256)) private _allowances; @@ -44,6 +45,11 @@ contract ERC20 is Context, IERC20, IERC20Metadata { string private _name; string private _symbol; + /** + * @dev Indicates a failed `decreaseAllowance` request. + */ + error ERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease); + /** * @dev Sets the values for {name} and {symbol}. * @@ -191,14 +197,16 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * * - `spender` cannot be the zero address. * - `spender` must have allowance for the caller of at least - * `subtractedValue`. + * `requestedDecrease`. */ - function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + function decreaseAllowance(address spender, uint256 requestedDecrease) public virtual returns (bool) { address owner = _msgSender(); uint256 currentAllowance = allowance(owner, spender); - require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); + if (currentAllowance < requestedDecrease) { + revert ERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease); + } unchecked { - _approve(owner, spender, currentAllowance - subtractedValue); + _approve(owner, spender, currentAllowance - requestedDecrease); } return true; @@ -215,8 +223,12 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * NOTE: This function is not virtual, {_update} should be overridden instead. */ function _transfer(address from, address to, uint256 amount) internal { - require(from != address(0), "ERC20: transfer from the zero address"); - require(to != address(0), "ERC20: transfer to the zero address"); + if (from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } _update(from, to, amount); } @@ -231,7 +243,9 @@ contract ERC20 is Context, IERC20, IERC20Metadata { _totalSupply += amount; } else { uint256 fromBalance = _balances[from]; - require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); + if (fromBalance < amount) { + revert ERC20InsufficientBalance(from, fromBalance, amount); + } unchecked { // Overflow not possible: amount <= fromBalance <= totalSupply. _balances[from] = fromBalance - amount; @@ -262,7 +276,9 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * NOTE: This function is not virtual, {_update} should be overridden instead. */ function _mint(address account, uint256 amount) internal { - require(account != address(0), "ERC20: mint to the zero address"); + if (account == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } _update(address(0), account, amount); } @@ -275,7 +291,9 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * NOTE: This function is not virtual, {_update} should be overridden instead */ function _burn(address account, uint256 amount) internal { - require(account != address(0), "ERC20: burn from the zero address"); + if (account == address(0)) { + revert ERC20InvalidSender(address(0)); + } _update(account, address(0), amount); } @@ -293,11 +311,37 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * - `spender` cannot be the zero address. */ function _approve(address owner, address spender, uint256 amount) internal virtual { - require(owner != address(0), "ERC20: approve from the zero address"); - require(spender != address(0), "ERC20: approve to the zero address"); + _approve(owner, spender, amount, true); + } + /** + * @dev Alternative version of {_approve} with an optional flag that can enable or disable the Approval event. + * + * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by + * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any + * `Approval` event during `transferFrom` operations. + * + * Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to true + * using the following override: + * ``` + * function _approve(address owner, address spender, uint256 amount, bool) internal virtual override { + * super._approve(owner, spender, amount, true); + * } + * ``` + * + * Requirements are the same as {_approve}. + */ + function _approve(address owner, address spender, uint256 amount, bool emitEvent) internal virtual { + if (owner == address(0)) { + revert ERC20InvalidApprover(address(0)); + } + if (spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } _allowances[owner][spender] = amount; - emit Approval(owner, spender, amount); + if (emitEvent) { + emit Approval(owner, spender, amount); + } } /** @@ -311,9 +355,11 @@ contract ERC20 is Context, IERC20, IERC20Metadata { function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { uint256 currentAllowance = allowance(owner, spender); if (currentAllowance != type(uint256).max) { - require(currentAllowance >= amount, "ERC20: insufficient allowance"); + if (currentAllowance < amount) { + revert ERC20InsufficientAllowance(spender, currentAllowance, amount); + } unchecked { - _approve(owner, spender, currentAllowance - amount); + _approve(owner, spender, currentAllowance - amount, false); } } } diff --git a/contracts/token/ERC20/extensions/ERC20Capped.sol b/contracts/token/ERC20/extensions/ERC20Capped.sol index cda072651b9..41e9ce5cf90 100644 --- a/contracts/token/ERC20/extensions/ERC20Capped.sol +++ b/contracts/token/ERC20/extensions/ERC20Capped.sol @@ -11,12 +11,24 @@ import "../ERC20.sol"; abstract contract ERC20Capped is ERC20 { uint256 private immutable _cap; + /** + * @dev Total supply cap has been exceeded. + */ + error ERC20ExceededCap(uint256 increasedSupply, uint256 cap); + + /** + * @dev The supplied cap is not a valid cap. + */ + error ERC20InvalidCap(uint256 cap); + /** * @dev Sets the value of the `cap`. This value is immutable, it can only be * set once during construction. */ constructor(uint256 cap_) { - require(cap_ > 0, "ERC20Capped: cap is 0"); + if (cap_ == 0) { + revert ERC20InvalidCap(0); + } _cap = cap_; } @@ -31,10 +43,14 @@ abstract contract ERC20Capped is ERC20 { * @dev See {ERC20-_update}. */ function _update(address from, address to, uint256 amount) internal virtual override { + super._update(from, to, amount); + if (from == address(0)) { - require(totalSupply() + amount <= cap(), "ERC20Capped: cap exceeded"); + uint256 maxSupply = cap(); + uint256 supply = totalSupply(); + if (supply > maxSupply) { + revert ERC20ExceededCap(supply, maxSupply); + } } - - super._update(from, to, amount); } } diff --git a/contracts/token/ERC20/extensions/ERC20FlashMint.sol b/contracts/token/ERC20/extensions/ERC20FlashMint.sol index 7a4076678b3..09c20baccb2 100644 --- a/contracts/token/ERC20/extensions/ERC20FlashMint.sol +++ b/contracts/token/ERC20/extensions/ERC20FlashMint.sol @@ -19,6 +19,21 @@ import "../ERC20.sol"; abstract contract ERC20FlashMint is ERC20, IERC3156FlashLender { bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan"); + /** + * @dev The loan token is not valid. + */ + error ERC3156UnsupportedToken(address token); + + /** + * @dev The requested loan exceeds the max loan amount for `token`. + */ + error ERC3156ExceededMaxLoan(uint256 maxLoan); + + /** + * @dev The receiver of a flashloan is not a valid {onFlashLoan} implementer. + */ + error ERC3156InvalidReceiver(address receiver); + /** * @dev Returns the maximum amount of tokens available for loan. * @param token The address of the token that is requested. @@ -37,7 +52,9 @@ abstract contract ERC20FlashMint is ERC20, IERC3156FlashLender { * @return The fees applied to the corresponding flash loan. */ function flashFee(address token, uint256 amount) public view virtual returns (uint256) { - require(token == address(this), "ERC20FlashMint: wrong token"); + if (token != address(this)) { + revert ERC3156UnsupportedToken(token); + } return _flashFee(token, amount); } @@ -89,13 +106,15 @@ abstract contract ERC20FlashMint is ERC20, IERC3156FlashLender { uint256 amount, bytes calldata data ) public virtual returns (bool) { - require(amount <= maxFlashLoan(token), "ERC20FlashMint: amount exceeds maxFlashLoan"); + uint256 maxLoan = maxFlashLoan(token); + if (amount > maxLoan) { + revert ERC3156ExceededMaxLoan(maxLoan); + } uint256 fee = flashFee(token, amount); _mint(address(receiver), amount); - require( - receiver.onFlashLoan(msg.sender, token, amount, fee, data) == _RETURN_VALUE, - "ERC20FlashMint: invalid return value" - ); + if (receiver.onFlashLoan(msg.sender, token, amount, fee, data) != _RETURN_VALUE) { + revert ERC3156InvalidReceiver(address(receiver)); + } address flashFeeReceiver = _flashFeeReceiver(); _spendAllowance(address(receiver), address(this), amount + fee); if (fee == 0 || flashFeeReceiver == address(0)) { diff --git a/contracts/token/ERC20/extensions/ERC20Pausable.sol b/contracts/token/ERC20/extensions/ERC20Pausable.sol index b31cac7de31..5ef50f9c6b0 100644 --- a/contracts/token/ERC20/extensions/ERC20Pausable.sol +++ b/contracts/token/ERC20/extensions/ERC20Pausable.sol @@ -27,8 +27,7 @@ abstract contract ERC20Pausable is ERC20, Pausable { * * - the contract must not be paused. */ - function _update(address from, address to, uint256 amount) internal virtual override { - require(!paused(), "ERC20Pausable: token transfer while paused"); + function _update(address from, address to, uint256 amount) internal virtual override whenNotPaused { super._update(from, to, amount); } } diff --git a/contracts/token/ERC20/extensions/ERC20Permit.sol b/contracts/token/ERC20/extensions/ERC20Permit.sol index 9379e44518d..4378eb7c132 100644 --- a/contracts/token/ERC20/extensions/ERC20Permit.sol +++ b/contracts/token/ERC20/extensions/ERC20Permit.sol @@ -24,6 +24,16 @@ abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { bytes32 private constant _PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + /** + * @dev Permit deadline has expired. + */ + error ERC2612ExpiredSignature(uint256 deadline); + + /** + * @dev Mismatched signature. + */ + error ERC2612InvalidSigner(address signer, address owner); + /** * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * @@ -43,14 +53,18 @@ abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { bytes32 r, bytes32 s ) public virtual { - require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); + if (block.timestamp > deadline) { + revert ERC2612ExpiredSignature(deadline); + } bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); bytes32 hash = _hashTypedDataV4(structHash); address signer = ECDSA.recover(hash, v, r, s); - require(signer == owner, "ERC20Permit: invalid signature"); + if (signer != owner) { + revert ERC2612InvalidSigner(signer, owner); + } _approve(owner, spender, value); } diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index c078878ece0..98f798efee1 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -23,6 +23,11 @@ import "../../../utils/math/SafeCast.sol"; * _Available since v4.2._ */ abstract contract ERC20Votes is ERC20, Votes { + /** + * @dev Total supply cap has been exceeded, introducing a risk of votes overflowing. + */ + error ERC20ExceededSafeSupply(uint256 increasedSupply, uint256 cap); + /** * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). */ @@ -38,7 +43,11 @@ abstract contract ERC20Votes is ERC20, Votes { function _update(address from, address to, uint256 amount) internal virtual override { super._update(from, to, amount); if (from == address(0)) { - require(totalSupply() <= _maxSupply(), "ERC20Votes: total supply risks overflowing votes"); + uint256 supply = totalSupply(); + uint256 cap = _maxSupply(); + if (supply > cap) { + revert ERC20ExceededSafeSupply(supply, cap); + } } _transferVotingUnits(from, to, amount); } diff --git a/contracts/token/ERC20/extensions/ERC20Wrapper.sol b/contracts/token/ERC20/extensions/ERC20Wrapper.sol index bf2b225cfa5..389965e9c10 100644 --- a/contracts/token/ERC20/extensions/ERC20Wrapper.sol +++ b/contracts/token/ERC20/extensions/ERC20Wrapper.sol @@ -18,8 +18,15 @@ import "../utils/SafeERC20.sol"; abstract contract ERC20Wrapper is ERC20 { IERC20 private immutable _underlying; + /** + * @dev The underlying token couldn't be wrapped. + */ + error ERC20InvalidUnderlying(address token); + constructor(IERC20 underlyingToken) { - require(underlyingToken != this, "ERC20Wrapper: cannot self wrap"); + if (underlyingToken == this) { + revert ERC20InvalidUnderlying(address(this)); + } _underlying = underlyingToken; } @@ -46,7 +53,9 @@ abstract contract ERC20Wrapper is ERC20 { */ function depositFor(address account, uint256 amount) public virtual returns (bool) { address sender = _msgSender(); - require(sender != address(this), "ERC20Wrapper: wrapper can't deposit"); + if (sender == address(this)) { + revert ERC20InvalidSender(address(this)); + } SafeERC20.safeTransferFrom(_underlying, sender, address(this), amount); _mint(account, amount); return true; diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index 665c95a023d..9ea6789f774 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -53,6 +53,26 @@ abstract contract ERC4626 is ERC20, IERC4626 { IERC20 private immutable _asset; uint8 private immutable _underlyingDecimals; + /** + * @dev Attempted to deposit more assets than the max amount for `receiver`. + */ + error ERC4626ExceededMaxDeposit(address receiver, uint256 assets, uint256 max); + + /** + * @dev Attempted to mint more shares than the max amount for `receiver`. + */ + error ERC4626ExceededMaxMint(address receiver, uint256 shares, uint256 max); + + /** + * @dev Attempted to withdraw more assets than the max amount for `receiver`. + */ + error ERC4626ExceededMaxWithdraw(address owner, uint256 assets, uint256 max); + + /** + * @dev Attempted to redeem more shares than the max amount for `receiver`. + */ + error ERC4626ExceededMaxRedeem(address owner, uint256 shares, uint256 max); + /** * @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC20 or ERC777). */ @@ -151,7 +171,10 @@ abstract contract ERC4626 is ERC20, IERC4626 { /** @dev See {IERC4626-deposit}. */ function deposit(uint256 assets, address receiver) public virtual returns (uint256) { - require(assets <= maxDeposit(receiver), "ERC4626: deposit more than max"); + uint256 maxAssets = maxDeposit(receiver); + if (assets > maxAssets) { + revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets); + } uint256 shares = previewDeposit(assets); _deposit(_msgSender(), receiver, assets, shares); @@ -165,7 +188,10 @@ abstract contract ERC4626 is ERC20, IERC4626 { * In this case, the shares will be minted without requiring any assets to be deposited. */ function mint(uint256 shares, address receiver) public virtual returns (uint256) { - require(shares <= maxMint(receiver), "ERC4626: mint more than max"); + uint256 maxShares = maxMint(receiver); + if (shares > maxShares) { + revert ERC4626ExceededMaxMint(receiver, shares, maxShares); + } uint256 assets = previewMint(shares); _deposit(_msgSender(), receiver, assets, shares); @@ -175,7 +201,10 @@ abstract contract ERC4626 is ERC20, IERC4626 { /** @dev See {IERC4626-withdraw}. */ function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256) { - require(assets <= maxWithdraw(owner), "ERC4626: withdraw more than max"); + uint256 maxAssets = maxWithdraw(owner); + if (assets > maxAssets) { + revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets); + } uint256 shares = previewWithdraw(assets); _withdraw(_msgSender(), receiver, owner, assets, shares); @@ -185,7 +214,10 @@ abstract contract ERC4626 is ERC20, IERC4626 { /** @dev See {IERC4626-redeem}. */ function redeem(uint256 shares, address receiver, address owner) public virtual returns (uint256) { - require(shares <= maxRedeem(owner), "ERC4626: redeem more than max"); + uint256 maxShares = maxRedeem(owner); + if (shares > maxShares) { + revert ERC4626ExceededMaxRedeem(owner, shares, maxShares); + } uint256 assets = previewRedeem(shares); _withdraw(_msgSender(), receiver, owner, assets, shares); diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index b1532d1cbbc..599307e7f73 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -19,6 +19,16 @@ import "../../../utils/Address.sol"; library SafeERC20 { using Address for address; + /** + * @dev An operation with an ERC20 token failed. + */ + error SafeERC20FailedOperation(address token); + + /** + * @dev Indicates a failed `decreaseAllowance` request. + */ + error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease); + /** * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value, * non-reverting calls are assumed to be successful. @@ -45,14 +55,16 @@ library SafeERC20 { } /** - * @dev Decrease the calling contract's allowance toward `spender` by `value`. If `token` returns no value, + * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no value, * non-reverting calls are assumed to be successful. */ - function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal { + function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal { unchecked { - uint256 oldAllowance = token.allowance(address(this), spender); - require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); - forceApprove(token, spender, oldAllowance - value); + uint256 currentAllowance = token.allowance(address(this), spender); + if (currentAllowance < requestedDecrease) { + revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease); + } + forceApprove(token, spender, currentAllowance - requestedDecrease); } } @@ -87,7 +99,9 @@ library SafeERC20 { uint256 nonceBefore = token.nonces(owner); token.permit(owner, spender, value, deadline, v, r, s); uint256 nonceAfter = token.nonces(owner); - require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed"); + if (nonceAfter != nonceBefore + 1) { + revert SafeERC20FailedOperation(address(token)); + } } /** @@ -101,8 +115,10 @@ library SafeERC20 { // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that // the target address contains contract code and also asserts for success in the low-level call. - bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed"); - require(returndata.length == 0 || abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); + bytes memory returndata = address(token).functionCall(data); + if (returndata.length != 0 && !abi.decode(returndata, (bool))) { + revert SafeERC20FailedOperation(address(token)); + } } /** diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 3eb3f74cf35..21ed95813a6 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -9,13 +9,14 @@ import "./extensions/IERC721Metadata.sol"; import "../../utils/Context.sol"; import "../../utils/Strings.sol"; import "../../utils/introspection/ERC165.sol"; +import "../../interfaces/draft-IERC6093.sol"; /** * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including * the Metadata extension, but not including the Enumerable extension, which is available separately as * {ERC721Enumerable}. */ -contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { +abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Errors { using Strings for uint256; // Token name @@ -58,7 +59,9 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * @dev See {IERC721-balanceOf}. */ function balanceOf(address owner) public view virtual returns (uint256) { - require(owner != address(0), "ERC721: address zero is not a valid owner"); + if (owner == address(0)) { + revert ERC721InvalidOwner(address(0)); + } return _balances[owner]; } @@ -67,7 +70,9 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { */ function ownerOf(uint256 tokenId) public view virtual returns (address) { address owner = _ownerOf(tokenId); - require(owner != address(0), "ERC721: invalid token ID"); + if (owner == address(0)) { + revert ERC721NonexistentToken(tokenId); + } return owner; } @@ -92,7 +97,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { _requireMinted(tokenId); string memory baseURI = _baseURI(); - return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ""; + return bytes(baseURI).length > 0 ? string.concat(baseURI, tokenId.toString()) : ""; } /** @@ -109,12 +114,13 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { */ function approve(address to, uint256 tokenId) public virtual { address owner = ownerOf(tokenId); - require(to != owner, "ERC721: approval to current owner"); + if (to == owner) { + revert ERC721InvalidOperator(owner); + } - require( - _msgSender() == owner || isApprovedForAll(owner, _msgSender()), - "ERC721: approve caller is not token owner or approved for all" - ); + if (_msgSender() != owner && !isApprovedForAll(owner, _msgSender())) { + revert ERC721InvalidApprover(_msgSender()); + } _approve(to, tokenId); } @@ -146,8 +152,9 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * @dev See {IERC721-transferFrom}. */ function transferFrom(address from, address to, uint256 tokenId) public virtual { - //solhint-disable-next-line max-line-length - require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved"); + if (!_isApprovedOrOwner(_msgSender(), tokenId)) { + revert ERC721InsufficientApproval(_msgSender(), tokenId); + } _transfer(from, to, tokenId); } @@ -163,7 +170,9 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * @dev See {IERC721-safeTransferFrom}. */ function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual { - require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved"); + if (!_isApprovedOrOwner(_msgSender(), tokenId)) { + revert ERC721InsufficientApproval(_msgSender(), tokenId); + } _safeTransfer(from, to, tokenId, data); } @@ -187,7 +196,9 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { */ function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual { _transfer(from, to, tokenId); - require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer"); + if (!_checkOnERC721Received(from, to, tokenId, data)) { + revert ERC721InvalidReceiver(to); + } } /** @@ -241,10 +252,9 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { */ function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual { _mint(to, tokenId); - require( - _checkOnERC721Received(address(0), to, tokenId, data), - "ERC721: transfer to non ERC721Receiver implementer" - ); + if (!_checkOnERC721Received(address(0), to, tokenId, data)) { + revert ERC721InvalidReceiver(to); + } } /** @@ -260,13 +270,19 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * Emits a {Transfer} event. */ function _mint(address to, uint256 tokenId) internal virtual { - require(to != address(0), "ERC721: mint to the zero address"); - require(!_exists(tokenId), "ERC721: token already minted"); + if (to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + if (_exists(tokenId)) { + revert ERC721InvalidSender(address(0)); + } _beforeTokenTransfer(address(0), to, tokenId, 1); // Check that tokenId was not minted by `_beforeTokenTransfer` hook - require(!_exists(tokenId), "ERC721: token already minted"); + if (_exists(tokenId)) { + revert ERC721InvalidSender(address(0)); + } unchecked { // Will not overflow unless all 2**256 token ids are minted to the same owner. @@ -328,13 +344,21 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * Emits a {Transfer} event. */ function _transfer(address from, address to, uint256 tokenId) internal virtual { - require(ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); - require(to != address(0), "ERC721: transfer to the zero address"); + address owner = ownerOf(tokenId); + if (owner != from) { + revert ERC721IncorrectOwner(from, tokenId, owner); + } + if (to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } _beforeTokenTransfer(from, to, tokenId, 1); // Check that tokenId was not transferred by `_beforeTokenTransfer` hook - require(ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); + owner = ownerOf(tokenId); + if (owner != from) { + revert ERC721IncorrectOwner(from, tokenId, owner); + } // Clear approvals from the previous owner delete _tokenApprovals[tokenId]; @@ -372,7 +396,9 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * Emits an {ApprovalForAll} event. */ function _setApprovalForAll(address owner, address operator, bool approved) internal virtual { - require(owner != operator, "ERC721: approve to caller"); + if (owner == operator) { + revert ERC721InvalidOperator(owner); + } _operatorApprovals[owner][operator] = approved; emit ApprovalForAll(owner, operator, approved); } @@ -381,7 +407,9 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * @dev Reverts if the `tokenId` has not been minted yet. */ function _requireMinted(uint256 tokenId) internal view virtual { - require(_exists(tokenId), "ERC721: invalid token ID"); + if (!_exists(tokenId)) { + revert ERC721NonexistentToken(tokenId); + } } /** @@ -405,7 +433,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { return retval == IERC721Receiver.onERC721Received.selector; } catch (bytes memory reason) { if (reason.length == 0) { - revert("ERC721: transfer to non ERC721Receiver implementer"); + revert ERC721InvalidReceiver(to); } else { /// @solidity memory-safe-assembly assembly { diff --git a/contracts/token/ERC721/extensions/ERC721Burnable.sol b/contracts/token/ERC721/extensions/ERC721Burnable.sol index 5489169e892..217f039cad6 100644 --- a/contracts/token/ERC721/extensions/ERC721Burnable.sol +++ b/contracts/token/ERC721/extensions/ERC721Burnable.sol @@ -19,8 +19,9 @@ abstract contract ERC721Burnable is Context, ERC721 { * - The caller must own `tokenId` or be an approved operator. */ function burn(uint256 tokenId) public virtual { - //solhint-disable-next-line max-line-length - require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved"); + if (!_isApprovedOrOwner(_msgSender(), tokenId)) { + revert ERC721InsufficientApproval(_msgSender(), tokenId); + } _burn(tokenId); } } diff --git a/contracts/token/ERC721/extensions/ERC721Consecutive.sol b/contracts/token/ERC721/extensions/ERC721Consecutive.sol index b7295e47621..f1308cdab1a 100644 --- a/contracts/token/ERC721/extensions/ERC721Consecutive.sol +++ b/contracts/token/ERC721/extensions/ERC721Consecutive.sol @@ -36,6 +36,28 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { Checkpoints.Trace160 private _sequentialOwnership; BitMaps.BitMap private _sequentialBurn; + /** + * @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. + */ + error ERC721ForbiddenBatchMint(); + + /** + * @dev Exceeds the max amount of mints per batch. + */ + error ERC721ExceededMaxBatchMint(uint256 batchSize, uint256 maxBatch); + + /** + * @dev Individual minting is not allowed. + */ + error ERC721ForbiddenMint(); + + /** + * @dev Batch burn is not supported. + */ + error ERC721ForbiddenBatchBurn(); + /** * @dev Maximum size of a batch of consecutive tokens. This is designed to limit stress on off-chain indexing * services that have to record one entry per token, and have protections against "unreasonably large" batches of @@ -86,9 +108,17 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { // minting a batch of size 0 is a no-op if (batchSize > 0) { - require(address(this).code.length == 0, "ERC721Consecutive: batch minting restricted to constructor"); - require(to != address(0), "ERC721Consecutive: mint to the zero address"); - require(batchSize <= _maxBatchSize(), "ERC721Consecutive: batch too large"); + if (address(this).code.length > 0) { + revert ERC721ForbiddenBatchMint(); + } + if (to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + + uint256 maxBatchSize = _maxBatchSize(); + if (batchSize > maxBatchSize) { + revert ERC721ExceededMaxBatchMint(batchSize, maxBatchSize); + } // hook before _beforeTokenTransfer(address(0), to, next, batchSize); @@ -117,7 +147,9 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { * After construction, {_mintConsecutive} is no longer available and {_mint} becomes available. */ function _mint(address to, uint256 tokenId) internal virtual override { - require(address(this).code.length > 0, "ERC721Consecutive: can't mint during construction"); + if (address(this).code.length == 0) { + revert ERC721ForbiddenMint(); + } super._mint(to, tokenId); } @@ -137,7 +169,9 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { !_sequentialBurn.get(firstTokenId) ) // and the token was never marked as burnt { - require(batchSize == 1, "ERC721Consecutive: batch burn not supported"); + if (batchSize != 1) { + revert ERC721ForbiddenBatchBurn(); + } _sequentialBurn.set(firstTokenId); } super._afterTokenTransfer(from, to, firstTokenId, batchSize); diff --git a/contracts/token/ERC721/extensions/ERC721Enumerable.sol b/contracts/token/ERC721/extensions/ERC721Enumerable.sol index 8cea9e19ae2..18e2ba5d626 100644 --- a/contracts/token/ERC721/extensions/ERC721Enumerable.sol +++ b/contracts/token/ERC721/extensions/ERC721Enumerable.sol @@ -26,6 +26,18 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { // Mapping from token id to position in the allTokens array mapping(uint256 => uint256) private _allTokensIndex; + /** + * @dev An `owner`'s token query was out of bounds for `index`. + * + * NOTE: The owner being `address(0)` indicates a global out of bounds index. + */ + error ERC721OutOfBoundsIndex(address owner, uint256 index); + + /** + * @dev Batch mint is not allowed. + */ + error ERC721EnumerableForbiddenBatchMint(); + /** * @dev See {IERC165-supportsInterface}. */ @@ -37,7 +49,9 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { * @dev See {IERC721Enumerable-tokenOfOwnerByIndex}. */ function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual returns (uint256) { - require(index < balanceOf(owner), "ERC721Enumerable: owner index out of bounds"); + if (index >= balanceOf(owner)) { + revert ERC721OutOfBoundsIndex(owner, index); + } return _ownedTokens[owner][index]; } @@ -52,7 +66,9 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { * @dev See {IERC721Enumerable-tokenByIndex}. */ function tokenByIndex(uint256 index) public view virtual returns (uint256) { - require(index < totalSupply(), "ERC721Enumerable: global index out of bounds"); + if (index >= totalSupply()) { + revert ERC721OutOfBoundsIndex(address(0), index); + } return _allTokens[index]; } @@ -69,7 +85,7 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { if (batchSize > 1) { // Will only trigger during construction. Batch transferring (minting) is not available afterwards. - revert("ERC721Enumerable: consecutive transfers not supported"); + revert ERC721EnumerableForbiddenBatchMint(); } uint256 tokenId = firstTokenId; diff --git a/contracts/token/ERC721/extensions/ERC721Pausable.sol b/contracts/token/ERC721/extensions/ERC721Pausable.sol index 0cadaa7c77e..a9472c5dc84 100644 --- a/contracts/token/ERC721/extensions/ERC721Pausable.sol +++ b/contracts/token/ERC721/extensions/ERC721Pausable.sol @@ -35,6 +35,6 @@ abstract contract ERC721Pausable is ERC721, Pausable { ) internal virtual override { super._beforeTokenTransfer(from, to, firstTokenId, batchSize); - require(!paused(), "ERC721Pausable: token transfer while paused"); + _requireNotPaused(); } } diff --git a/contracts/token/ERC721/extensions/ERC721URIStorage.sol b/contracts/token/ERC721/extensions/ERC721URIStorage.sol index 6350a0952ad..ae625fc2890 100644 --- a/contracts/token/ERC721/extensions/ERC721URIStorage.sol +++ b/contracts/token/ERC721/extensions/ERC721URIStorage.sol @@ -35,9 +35,9 @@ abstract contract ERC721URIStorage is IERC4906, ERC721 { if (bytes(base).length == 0) { return _tokenURI; } - // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked). + // If both are set, concatenate the baseURI and tokenURI (via string.concat). if (bytes(_tokenURI).length > 0) { - return string(abi.encodePacked(base, _tokenURI)); + return string.concat(base, _tokenURI); } return super.tokenURI(tokenId); @@ -53,7 +53,9 @@ abstract contract ERC721URIStorage is IERC4906, ERC721 { * - `tokenId` must exist. */ function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual { - require(_exists(tokenId), "ERC721URIStorage: URI set of nonexistent token"); + if (!_exists(tokenId)) { + revert ERC721NonexistentToken(tokenId); + } _tokenURIs[tokenId] = _tokenURI; emit MetadataUpdate(tokenId); diff --git a/contracts/token/ERC721/extensions/ERC721Wrapper.sol b/contracts/token/ERC721/extensions/ERC721Wrapper.sol index 9226349f4c0..47a42c1f004 100644 --- a/contracts/token/ERC721/extensions/ERC721Wrapper.sol +++ b/contracts/token/ERC721/extensions/ERC721Wrapper.sol @@ -17,6 +17,11 @@ import "../ERC721.sol"; abstract contract ERC721Wrapper is ERC721, IERC721Receiver { IERC721 private immutable _underlying; + /** + * @dev The received ERC721 token couldn't be wrapped. + */ + error ERC721UnsupportedToken(address token); + constructor(IERC721 underlyingToken) { _underlying = underlyingToken; } @@ -46,7 +51,9 @@ abstract contract ERC721Wrapper is ERC721, IERC721Receiver { uint256 length = tokenIds.length; for (uint256 i = 0; i < length; ++i) { uint256 tokenId = tokenIds[i]; - require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721Wrapper: caller is not token owner or approved"); + if (!_isApprovedOrOwner(_msgSender(), tokenId)) { + revert ERC721InsufficientApproval(_msgSender(), tokenId); + } _burn(tokenId); // Checks were already performed at this point, and there's no way to retake ownership or approval from // the wrapped tokenId after this point, so it's safe to remove the reentrancy check for the next line. @@ -68,7 +75,9 @@ abstract contract ERC721Wrapper is ERC721, IERC721Receiver { * for recovering in that scenario. */ function onERC721Received(address, address from, uint256 tokenId, bytes memory) public virtual returns (bytes4) { - require(address(underlying()) == _msgSender(), "ERC721Wrapper: caller is not underlying"); + if (address(underlying()) != _msgSender()) { + revert ERC721UnsupportedToken(_msgSender()); + } _safeMint(from, tokenId); return IERC721Receiver.onERC721Received.selector; } @@ -78,7 +87,10 @@ abstract contract ERC721Wrapper is ERC721, IERC721Receiver { * function that can be exposed with access control if desired. */ function _recover(address account, uint256 tokenId) internal virtual returns (uint256) { - require(underlying().ownerOf(tokenId) == address(this), "ERC721Wrapper: wrapper is not token owner"); + address owner = underlying().ownerOf(tokenId); + if (owner != address(this)) { + revert ERC721IncorrectOwner(address(this), tokenId, owner); + } _safeMint(account, tokenId); return tokenId; } diff --git a/contracts/token/ERC721/utils/ERC721Holder.sol b/contracts/token/ERC721/utils/ERC721Holder.sol index 9f5b2e9f9df..a3ee8b5f0ca 100644 --- a/contracts/token/ERC721/utils/ERC721Holder.sol +++ b/contracts/token/ERC721/utils/ERC721Holder.sol @@ -11,7 +11,7 @@ import "../IERC721Receiver.sol"; * Accepts all token transfers. * Make sure the contract is able to use its token with {IERC721-safeTransferFrom}, {IERC721-approve} or {IERC721-setApprovalForAll}. */ -contract ERC721Holder is IERC721Receiver { +abstract contract ERC721Holder is IERC721Receiver { /** * @dev See {IERC721Receiver-onERC721Received}. * diff --git a/contracts/token/common/ERC2981.sol b/contracts/token/common/ERC2981.sol index 85e9027333e..21869ee255b 100644 --- a/contracts/token/common/ERC2981.sol +++ b/contracts/token/common/ERC2981.sol @@ -30,6 +30,26 @@ abstract contract ERC2981 is IERC2981, ERC165 { RoyaltyInfo private _defaultRoyaltyInfo; mapping(uint256 => RoyaltyInfo) private _tokenRoyaltyInfo; + /** + * @dev The default royalty set is invalid (eg. (numerator / denominator) >= 1). + */ + error ERC2981InvalidDefaultRoyalty(uint256 numerator, uint256 denominator); + + /** + * @dev The default royalty receiver is invalid. + */ + error ERC2981InvalidDefaultRoyaltyReceiver(address receiver); + + /** + * @dev The royalty set for an specific `tokenId` is invalid (eg. (numerator / denominator) >= 1). + */ + error ERC2981InvalidTokenRoyalty(uint256 tokenId, uint256 numerator, uint256 denominator); + + /** + * @dev The royalty receiver for `tokenId` is invalid. + */ + error ERC2981InvalidTokenRoyaltyReceiver(uint256 tokenId, address receiver); + /** * @dev See {IERC165-supportsInterface}. */ @@ -70,8 +90,14 @@ abstract contract ERC2981 is IERC2981, ERC165 { * - `feeNumerator` cannot be greater than the fee denominator. */ function _setDefaultRoyalty(address receiver, uint96 feeNumerator) internal virtual { - require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice"); - require(receiver != address(0), "ERC2981: invalid receiver"); + uint256 denominator = _feeDenominator(); + if (feeNumerator > denominator) { + // Royalty fee will exceed the sale price + revert ERC2981InvalidDefaultRoyalty(feeNumerator, denominator); + } + if (receiver == address(0)) { + revert ERC2981InvalidDefaultRoyaltyReceiver(address(0)); + } _defaultRoyaltyInfo = RoyaltyInfo(receiver, feeNumerator); } @@ -92,8 +118,14 @@ abstract contract ERC2981 is IERC2981, ERC165 { * - `feeNumerator` cannot be greater than the fee denominator. */ function _setTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator) internal virtual { - require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice"); - require(receiver != address(0), "ERC2981: Invalid parameters"); + uint256 denominator = _feeDenominator(); + if (feeNumerator > denominator) { + // Royalty fee will exceed the sale price + revert ERC2981InvalidTokenRoyalty(tokenId, feeNumerator, denominator); + } + if (receiver == address(0)) { + revert ERC2981InvalidTokenRoyaltyReceiver(tokenId, address(0)); + } _tokenRoyaltyInfo[tokenId] = RoyaltyInfo(receiver, feeNumerator); } diff --git a/contracts/utils/Address.sol b/contracts/utils/Address.sol index 02f475620b1..859332b3917 100644 --- a/contracts/utils/Address.sol +++ b/contracts/utils/Address.sol @@ -7,6 +7,21 @@ pragma solidity ^0.8.19; * @dev Collection of functions related to the address type */ library Address { + /** + * @dev The ETH balance of the account is not enough to perform the operation. + */ + error AddressInsufficientBalance(address account); + + /** + * @dev There's no code at `target` (it is not a contract). + */ + error AddressEmptyCode(address target); + + /** + * @dev A call to an address target failed. The target may have reverted. + */ + error FailedInnerCall(); + /** * @dev Replacement for Solidity's `transfer`: sends `amount` wei to * `recipient`, forwarding all available gas and reverting on errors. @@ -24,10 +39,14 @@ library Address { * https://solidity.readthedocs.io/en/v0.8.0/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. */ function sendValue(address payable recipient, uint256 amount) internal { - require(address(this).balance >= amount, "Address: insufficient balance"); + if (address(this).balance < amount) { + revert AddressInsufficientBalance(address(this)); + } (bool success, ) = recipient.call{value: amount}(""); - require(success, "Address: unable to send value, recipient may have reverted"); + if (!success) { + revert FailedInnerCall(); + } } /** @@ -49,21 +68,25 @@ library Address { * _Available since v3.1._ */ function functionCall(address target, bytes memory data) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, "Address: low-level call failed"); + return functionCallWithValue(target, data, 0, defaultRevert); } /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with - * `errorMessage` as a fallback revert reason when `target` reverts. + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with a + * `customRevert` function as a fallback when `target` reverts. * - * _Available since v3.1._ + * Requirements: + * + * - `customRevert` must be a reverting function. + * + * _Available since v5.0._ */ function functionCall( address target, bytes memory data, - string memory errorMessage + function() internal view customRevert ) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, errorMessage); + return functionCallWithValue(target, data, 0, customRevert); } /** @@ -78,24 +101,30 @@ library Address { * _Available since v3.1._ */ function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { - return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + return functionCallWithValue(target, data, value, defaultRevert); } /** * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but - * with `errorMessage` as a fallback revert reason when `target` reverts. + * with a `customRevert` function as a fallback revert reason when `target` reverts. * - * _Available since v3.1._ + * Requirements: + * + * - `customRevert` must be a reverting function. + * + * _Available since v5.0._ */ function functionCallWithValue( address target, bytes memory data, uint256 value, - string memory errorMessage + function() internal view customRevert ) internal returns (bytes memory) { - require(address(this).balance >= value, "Address: insufficient balance for call"); + if (address(this).balance < value) { + revert AddressInsufficientBalance(address(this)); + } (bool success, bytes memory returndata) = target.call{value: value}(data); - return verifyCallResultFromTarget(target, success, returndata, errorMessage); + return verifyCallResultFromTarget(target, success, returndata, customRevert); } /** @@ -105,7 +134,7 @@ library Address { * _Available since v3.3._ */ function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { - return functionStaticCall(target, data, "Address: low-level static call failed"); + return functionStaticCall(target, data, defaultRevert); } /** @@ -117,10 +146,10 @@ library Address { function functionStaticCall( address target, bytes memory data, - string memory errorMessage + function() internal view customRevert ) internal view returns (bytes memory) { (bool success, bytes memory returndata) = target.staticcall(data); - return verifyCallResultFromTarget(target, success, returndata, errorMessage); + return verifyCallResultFromTarget(target, success, returndata, customRevert); } /** @@ -130,7 +159,7 @@ library Address { * _Available since v3.4._ */ function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { - return functionDelegateCall(target, data, "Address: low-level delegate call failed"); + return functionDelegateCall(target, data, defaultRevert); } /** @@ -142,55 +171,78 @@ library Address { function functionDelegateCall( address target, bytes memory data, - string memory errorMessage + function() internal view customRevert ) internal returns (bytes memory) { (bool success, bytes memory returndata) = target.delegatecall(data); - return verifyCallResultFromTarget(target, success, returndata, errorMessage); + return verifyCallResultFromTarget(target, success, returndata, customRevert); } /** * @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling - * the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract. + * the revert reason or using the provided `customRevert`) in case of unsuccessful call or if target was not a contract. * - * _Available since v4.8._ + * _Available since v5.0._ */ function verifyCallResultFromTarget( address target, bool success, bytes memory returndata, - string memory errorMessage + function() internal view customRevert ) internal view returns (bytes memory) { if (success) { if (returndata.length == 0) { // only check if target is a contract if the call was successful and the return data is empty // otherwise we already know that it was a contract - require(target.code.length > 0, "Address: call to non-contract"); + if (target.code.length == 0) { + revert AddressEmptyCode(target); + } } return returndata; } else { - _revert(returndata, errorMessage); + _revert(returndata, customRevert); } } /** * @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the - * revert reason or using the provided one. + * revert reason or with a default revert error. + * + * _Available since v5.0._ + */ + function verifyCallResult(bool success, bytes memory returndata) internal view returns (bytes memory) { + return verifyCallResult(success, returndata, defaultRevert); + } + + /** + * @dev Same as {xref-Address-verifyCallResult-bool-bytes-}[`verifyCallResult`], but with a + * `customRevert` function as a fallback when `success` is `false`. * - * _Available since v4.3._ + * Requirements: + * + * - `customRevert` must be a reverting function. + * + * _Available since v5.0._ */ function verifyCallResult( bool success, bytes memory returndata, - string memory errorMessage - ) internal pure returns (bytes memory) { + function() internal view customRevert + ) internal view returns (bytes memory) { if (success) { return returndata; } else { - _revert(returndata, errorMessage); + _revert(returndata, customRevert); } } - function _revert(bytes memory returndata, string memory errorMessage) private pure { + /** + * @dev Default reverting function when no `customRevert` is provided in a function call. + */ + function defaultRevert() internal pure { + revert FailedInnerCall(); + } + + function _revert(bytes memory returndata, function() internal view customRevert) private view { // Look for revert reason and bubble it up if present if (returndata.length > 0) { // The easiest way to bubble the revert reason is using memory via assembly @@ -200,7 +252,8 @@ library Address { revert(add(32, returndata), returndata_size) } } else { - revert(errorMessage); + customRevert(); + revert FailedInnerCall(); } } } diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index 383a0820aaf..f4ef45645a8 100644 --- a/contracts/utils/Arrays.sol +++ b/contracts/utils/Arrays.sol @@ -102,4 +102,26 @@ library Arrays { } return slot.getUint256Slot(); } + + /** + * @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check. + * + * WARNING: Only use if you are certain `pos` is lower than the array length. + */ + function unsafeMemoryAccess(uint256[] memory arr, uint256 pos) internal pure returns (uint256 res) { + assembly { + res := mload(add(add(arr, 0x20), mul(pos, 0x20))) + } + } + + /** + * @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check. + * + * WARNING: Only use if you are certain `pos` is lower than the array length. + */ + function unsafeMemoryAccess(address[] memory arr, uint256 pos) internal pure returns (address res) { + assembly { + res := mload(add(add(arr, 0x20), mul(pos, 0x20))) + } + } } diff --git a/contracts/utils/Create2.sol b/contracts/utils/Create2.sol index d5776885f92..24d27ea0bd4 100644 --- a/contracts/utils/Create2.sol +++ b/contracts/utils/Create2.sol @@ -13,6 +13,21 @@ pragma solidity ^0.8.19; * information. */ library Create2 { + /** + * @dev Not enough balance for performing a CREATE2 deploy. + */ + error Create2InsufficientBalance(uint256 balance, uint256 needed); + + /** + * @dev There's no code to deploy. + */ + error Create2EmptyBytecode(); + + /** + * @dev The deployment failed. + */ + error Create2FailedDeployment(); + /** * @dev Deploys a contract using `CREATE2`. The address where the contract * will be deployed can be known in advance via {computeAddress}. @@ -28,13 +43,19 @@ library Create2 { * - if `amount` is non-zero, `bytecode` must have a `payable` constructor. */ function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) internal returns (address addr) { - require(address(this).balance >= amount, "Create2: insufficient balance"); - require(bytecode.length != 0, "Create2: bytecode length is zero"); + if (address(this).balance < amount) { + revert Create2InsufficientBalance(address(this).balance, amount); + } + if (bytecode.length == 0) { + revert Create2EmptyBytecode(); + } /// @solidity memory-safe-assembly assembly { addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt) } - require(addr != address(0), "Create2: Failed on deploy"); + if (addr == address(0)) { + revert Create2FailedDeployment(); + } } /** diff --git a/contracts/utils/Nonces.sol b/contracts/utils/Nonces.sol index 04b884797e1..f8ea1dfd382 100644 --- a/contracts/utils/Nonces.sol +++ b/contracts/utils/Nonces.sol @@ -5,6 +5,11 @@ pragma solidity ^0.8.19; * @dev Provides tracking nonces for addresses. Nonces will only increment. */ abstract contract Nonces { + /** + * @dev The nonce used for an `account` is not the expected current nonce. + */ + error InvalidAccountNonce(address account, uint256 currentNonce); + mapping(address => uint256) private _nonces; /** @@ -27,4 +32,15 @@ abstract contract Nonces { return _nonces[owner]++; } } + + /** + * @dev Same as {_useNonce} but checking that `nonce` is the next valid for `owner`. + */ + function _useCheckedNonce(address owner, uint256 nonce) internal virtual returns (uint256) { + uint256 current = _useNonce(owner); + if (nonce != current) { + revert InvalidAccountNonce(owner, current); + } + return current; + } } diff --git a/contracts/utils/StorageSlot.sol b/contracts/utils/StorageSlot.sol index f12640e7bf7..b0e9189675c 100644 --- a/contracts/utils/StorageSlot.sol +++ b/contracts/utils/StorageSlot.sol @@ -22,7 +22,7 @@ pragma solidity ^0.8.19; * } * * function _setImplementation(address newImplementation) internal { - * require(newImplementation.code.length > 0, "ERC1967: new implementation is not a contract"); + * require(newImplementation.code.length > 0); * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; * } * } diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 050f6f9ca6a..afbf463d1a4 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -13,6 +13,11 @@ library Strings { bytes16 private constant _SYMBOLS = "0123456789abcdef"; uint8 private constant _ADDRESS_LENGTH = 20; + /** + * @dev The `value` string doesn't fit in the specified `length`. + */ + error StringsInsufficientHexLength(uint256 value, uint256 length); + /** * @dev Converts a `uint256` to its ASCII `string` decimal representation. */ @@ -41,8 +46,8 @@ library Strings { /** * @dev Converts a `int256` to its ASCII `string` decimal representation. */ - function toString(int256 value) internal pure returns (string memory) { - return string(abi.encodePacked(value < 0 ? "-" : "", toString(SignedMath.abs(value)))); + function toStringSigned(int256 value) internal pure returns (string memory) { + return string.concat(value < 0 ? "-" : "", toString(SignedMath.abs(value))); } /** @@ -58,14 +63,17 @@ library Strings { * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. */ function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + uint256 localValue = value; bytes memory buffer = new bytes(2 * length + 2); buffer[0] = "0"; buffer[1] = "x"; for (uint256 i = 2 * length + 1; i > 1; --i) { - buffer[i] = _SYMBOLS[value & 0xf]; - value >>= 4; + buffer[i] = _SYMBOLS[localValue & 0xf]; + localValue >>= 4; + } + if (localValue != 0) { + revert StringsInsufficientHexLength(value, length); } - require(value == 0, "Strings: hex length insufficient"); return string(buffer); } diff --git a/contracts/utils/cryptography/ECDSA.sol b/contracts/utils/cryptography/ECDSA.sol index b907abfc091..b8f1affee7a 100644 --- a/contracts/utils/cryptography/ECDSA.sol +++ b/contracts/utils/cryptography/ECDSA.sol @@ -19,15 +19,30 @@ library ECDSA { InvalidSignatureS } - function _throwError(RecoverError error) private pure { + /** + * @dev The signature derives the `address(0)`. + */ + error ECDSAInvalidSignature(); + + /** + * @dev The signature has an invalid length. + */ + error ECDSAInvalidSignatureLength(uint256 length); + + /** + * @dev The signature has an S value that is in the upper half order. + */ + error ECDSAInvalidSignatureS(bytes32 s); + + function _throwError(RecoverError error, bytes32 errorArg) private pure { if (error == RecoverError.NoError) { return; // no error: do nothing } else if (error == RecoverError.InvalidSignature) { - revert("ECDSA: invalid signature"); + revert ECDSAInvalidSignature(); } else if (error == RecoverError.InvalidSignatureLength) { - revert("ECDSA: invalid signature length"); + revert ECDSAInvalidSignatureLength(uint256(errorArg)); } else if (error == RecoverError.InvalidSignatureS) { - revert("ECDSA: invalid signature 's' value"); + revert ECDSAInvalidSignatureS(errorArg); } } @@ -51,7 +66,7 @@ library ECDSA { * * _Available since v4.3._ */ - function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) { + function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError, bytes32) { if (signature.length == 65) { bytes32 r; bytes32 s; @@ -66,7 +81,7 @@ library ECDSA { } return tryRecover(hash, v, r, s); } else { - return (address(0), RecoverError.InvalidSignatureLength); + return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length)); } } @@ -85,8 +100,8 @@ library ECDSA { * be too long), and then calling {toEthSignedMessageHash} on it. */ function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { - (address recovered, RecoverError error) = tryRecover(hash, signature); - _throwError(error); + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature); + _throwError(error, errorArg); return recovered; } @@ -97,7 +112,7 @@ library ECDSA { * * _Available since v4.3._ */ - function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError) { + function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError, bytes32) { unchecked { bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); // We do not check for an overflow here since the shift operation results in 0 or 1. @@ -112,8 +127,8 @@ library ECDSA { * _Available since v4.2._ */ function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { - (address recovered, RecoverError error) = tryRecover(hash, r, vs); - _throwError(error); + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs); + _throwError(error, errorArg); return recovered; } @@ -123,7 +138,12 @@ library ECDSA { * * _Available since v4.3._ */ - function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address, RecoverError) { + function tryRecover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address, RecoverError, bytes32) { // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most @@ -134,16 +154,16 @@ library ECDSA { // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept // these malleable signatures as well. if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { - return (address(0), RecoverError.InvalidSignatureS); + return (address(0), RecoverError.InvalidSignatureS, s); } // If the signature is valid (and not malleable), return the signer address address signer = ecrecover(hash, v, r, s); if (signer == address(0)) { - return (address(0), RecoverError.InvalidSignature); + return (address(0), RecoverError.InvalidSignature, bytes32(0)); } - return (signer, RecoverError.NoError); + return (signer, RecoverError.NoError, bytes32(0)); } /** @@ -151,8 +171,8 @@ library ECDSA { * `r` and `s` signature fields separately. */ function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { - (address recovered, RecoverError error) = tryRecover(hash, v, r, s); - _throwError(error); + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s); + _throwError(error, errorArg); return recovered; } diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol index b9c9c9d195d..1089de0050d 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -110,7 +110,7 @@ abstract contract EIP712 is IERC5267 { } /** - * @dev See {EIP-5267}. + * @dev See {IERC-5267}. * * _Available since v4.9._ */ @@ -130,12 +130,38 @@ abstract contract EIP712 is IERC5267 { { return ( hex"0f", // 01111 - _name.toStringWithFallback(_nameFallback), - _version.toStringWithFallback(_versionFallback), + _EIP712Name(), + _EIP712Version(), block.chainid, address(this), bytes32(0), new uint256[](0) ); } + + /** + * @dev The name parameter for the EIP712 domain. + * + * NOTE: By default this function reads _name which is an immutable value. + * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). + * + * _Available since v5.0._ + */ + // solhint-disable-next-line func-name-mixedcase + function _EIP712Name() internal view returns (string memory) { + return _name.toStringWithFallback(_nameFallback); + } + + /** + * @dev The version parameter for the EIP712 domain. + * + * NOTE: By default this function reads _version which is an immutable value. + * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). + * + * _Available since v5.0._ + */ + // solhint-disable-next-line func-name-mixedcase + function _EIP712Version() internal view returns (string memory) { + return _version.toStringWithFallback(_versionFallback); + } } diff --git a/contracts/utils/cryptography/MerkleProof.sol b/contracts/utils/cryptography/MerkleProof.sol index 0bcdda2cdb4..94586ff7b3f 100644 --- a/contracts/utils/cryptography/MerkleProof.sol +++ b/contracts/utils/cryptography/MerkleProof.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/MerkleProof.sol) +// OpenZeppelin Contracts (last updated v4.9.2) (utils/cryptography/MerkleProof.sol) pragma solidity ^0.8.19; @@ -18,6 +18,11 @@ pragma solidity ^0.8.19; * against this attack out of the box. */ library MerkleProof { + /** + *@dev The multiproof provided is not valid. + */ + error MerkleProofInvalidMultiproof(); + /** * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree * defined by `root`. For this, a `proof` must be provided, containing @@ -121,10 +126,13 @@ library MerkleProof { // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of // the merkle tree. uint256 leavesLen = leaves.length; + uint256 proofLen = proof.length; uint256 totalHashes = proofFlags.length; // Check proof validity. - require(leavesLen + proof.length - 1 == totalHashes, "MerkleProof: invalid multiproof"); + if (leavesLen + proofLen - 1 != totalHashes) { + revert MerkleProofInvalidMultiproof(); + } // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop". @@ -146,6 +154,9 @@ library MerkleProof { } if (totalHashes > 0) { + if (proofPos != proofLen) { + revert MerkleProofInvalidMultiproof(); + } unchecked { return hashes[totalHashes - 1]; } @@ -173,10 +184,13 @@ library MerkleProof { // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of // the merkle tree. uint256 leavesLen = leaves.length; + uint256 proofLen = proof.length; uint256 totalHashes = proofFlags.length; // Check proof validity. - require(leavesLen + proof.length - 1 == totalHashes, "MerkleProof: invalid multiproof"); + if (leavesLen + proofLen - 1 != totalHashes) { + revert MerkleProofInvalidMultiproof(); + } // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop". @@ -198,6 +212,9 @@ library MerkleProof { } if (totalHashes > 0) { + if (proofPos != proofLen) { + revert MerkleProofInvalidMultiproof(); + } unchecked { return hashes[totalHashes - 1]; } diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index 941f7538fac..25fdee5b31f 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -22,7 +22,7 @@ library SignatureChecker { * change through time. It could return true at block N and false at block N+1 (or the opposite). */ function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { - (address recovered, ECDSA.RecoverError error) = ECDSA.tryRecover(hash, signature); + (address recovered, ECDSA.RecoverError error, ) = ECDSA.tryRecover(hash, signature); return (error == ECDSA.RecoverError.NoError && recovered == signer) || isValidERC1271SignatureNow(signer, hash, signature); diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index ff3dc8d9f46..d372295d7a8 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -7,6 +7,11 @@ pragma solidity ^0.8.19; * @dev Standard math utilities missing in the Solidity language. */ library Math { + /** + * @dev Muldiv operation overflow. + */ + error MathOverflowedMulDiv(); + enum Rounding { Down, // Toward negative infinity Up, // Toward infinity @@ -109,6 +114,11 @@ library Math { * of rounding down. */ function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + if (b == 0) { + // Guarantee the same behavior as in a regular Solidity division. + return a / b; + } + // (a + b - 1) / b can overflow on addition, so we distribute. return a == 0 ? 0 : (a - 1) / b + 1; } @@ -140,7 +150,9 @@ library Math { } // Make sure the result is less than 2^256. Also prevents denominator == 0. - require(denominator > prod1, "Math: mulDiv overflow"); + if (denominator <= prod1) { + revert MathOverflowedMulDiv(); + } /////////////////////////////////////////////// // 512 by 256 division. diff --git a/contracts/utils/math/SafeCast.sol b/contracts/utils/math/SafeCast.sol index d9e21bb172d..d3b86b0884d 100644 --- a/contracts/utils/math/SafeCast.sol +++ b/contracts/utils/math/SafeCast.sol @@ -17,6 +17,26 @@ pragma solidity ^0.8.19; * class of bugs, so it's recommended to use it always. */ library SafeCast { + /** + * @dev Value doesn't fit in an uint of `bits` size. + */ + error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value); + + /** + * @dev An int value doesn't fit in an uint of `bits` size. + */ + error SafeCastOverflowedIntToUint(int256 value); + + /** + * @dev Value doesn't fit in an int of `bits` size. + */ + error SafeCastOverflowedIntDowncast(uint8 bits, int256 value); + + /** + * @dev An uint value doesn't fit in an int of `bits` size. + */ + error SafeCastOverflowedUintToInt(uint256 value); + /** * @dev Returns the downcasted uint248 from uint256, reverting on * overflow (when the input is greater than largest uint248). @@ -30,7 +50,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint248(uint256 value) internal pure returns (uint248) { - require(value <= type(uint248).max, "SafeCast: value doesn't fit in 248 bits"); + if (value > type(uint248).max) { + revert SafeCastOverflowedUintDowncast(248, value); + } return uint248(value); } @@ -47,7 +69,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint240(uint256 value) internal pure returns (uint240) { - require(value <= type(uint240).max, "SafeCast: value doesn't fit in 240 bits"); + if (value > type(uint240).max) { + revert SafeCastOverflowedUintDowncast(240, value); + } return uint240(value); } @@ -64,7 +88,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint232(uint256 value) internal pure returns (uint232) { - require(value <= type(uint232).max, "SafeCast: value doesn't fit in 232 bits"); + if (value > type(uint232).max) { + revert SafeCastOverflowedUintDowncast(232, value); + } return uint232(value); } @@ -81,7 +107,9 @@ library SafeCast { * _Available since v4.2._ */ function toUint224(uint256 value) internal pure returns (uint224) { - require(value <= type(uint224).max, "SafeCast: value doesn't fit in 224 bits"); + if (value > type(uint224).max) { + revert SafeCastOverflowedUintDowncast(224, value); + } return uint224(value); } @@ -98,7 +126,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint216(uint256 value) internal pure returns (uint216) { - require(value <= type(uint216).max, "SafeCast: value doesn't fit in 216 bits"); + if (value > type(uint216).max) { + revert SafeCastOverflowedUintDowncast(216, value); + } return uint216(value); } @@ -115,7 +145,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint208(uint256 value) internal pure returns (uint208) { - require(value <= type(uint208).max, "SafeCast: value doesn't fit in 208 bits"); + if (value > type(uint208).max) { + revert SafeCastOverflowedUintDowncast(208, value); + } return uint208(value); } @@ -132,7 +164,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint200(uint256 value) internal pure returns (uint200) { - require(value <= type(uint200).max, "SafeCast: value doesn't fit in 200 bits"); + if (value > type(uint200).max) { + revert SafeCastOverflowedUintDowncast(200, value); + } return uint200(value); } @@ -149,7 +183,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint192(uint256 value) internal pure returns (uint192) { - require(value <= type(uint192).max, "SafeCast: value doesn't fit in 192 bits"); + if (value > type(uint192).max) { + revert SafeCastOverflowedUintDowncast(192, value); + } return uint192(value); } @@ -166,7 +202,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint184(uint256 value) internal pure returns (uint184) { - require(value <= type(uint184).max, "SafeCast: value doesn't fit in 184 bits"); + if (value > type(uint184).max) { + revert SafeCastOverflowedUintDowncast(184, value); + } return uint184(value); } @@ -183,7 +221,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint176(uint256 value) internal pure returns (uint176) { - require(value <= type(uint176).max, "SafeCast: value doesn't fit in 176 bits"); + if (value > type(uint176).max) { + revert SafeCastOverflowedUintDowncast(176, value); + } return uint176(value); } @@ -200,7 +240,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint168(uint256 value) internal pure returns (uint168) { - require(value <= type(uint168).max, "SafeCast: value doesn't fit in 168 bits"); + if (value > type(uint168).max) { + revert SafeCastOverflowedUintDowncast(168, value); + } return uint168(value); } @@ -217,7 +259,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint160(uint256 value) internal pure returns (uint160) { - require(value <= type(uint160).max, "SafeCast: value doesn't fit in 160 bits"); + if (value > type(uint160).max) { + revert SafeCastOverflowedUintDowncast(160, value); + } return uint160(value); } @@ -234,7 +278,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint152(uint256 value) internal pure returns (uint152) { - require(value <= type(uint152).max, "SafeCast: value doesn't fit in 152 bits"); + if (value > type(uint152).max) { + revert SafeCastOverflowedUintDowncast(152, value); + } return uint152(value); } @@ -251,7 +297,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint144(uint256 value) internal pure returns (uint144) { - require(value <= type(uint144).max, "SafeCast: value doesn't fit in 144 bits"); + if (value > type(uint144).max) { + revert SafeCastOverflowedUintDowncast(144, value); + } return uint144(value); } @@ -268,7 +316,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint136(uint256 value) internal pure returns (uint136) { - require(value <= type(uint136).max, "SafeCast: value doesn't fit in 136 bits"); + if (value > type(uint136).max) { + revert SafeCastOverflowedUintDowncast(136, value); + } return uint136(value); } @@ -285,7 +335,9 @@ library SafeCast { * _Available since v2.5._ */ function toUint128(uint256 value) internal pure returns (uint128) { - require(value <= type(uint128).max, "SafeCast: value doesn't fit in 128 bits"); + if (value > type(uint128).max) { + revert SafeCastOverflowedUintDowncast(128, value); + } return uint128(value); } @@ -302,7 +354,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint120(uint256 value) internal pure returns (uint120) { - require(value <= type(uint120).max, "SafeCast: value doesn't fit in 120 bits"); + if (value > type(uint120).max) { + revert SafeCastOverflowedUintDowncast(120, value); + } return uint120(value); } @@ -319,7 +373,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint112(uint256 value) internal pure returns (uint112) { - require(value <= type(uint112).max, "SafeCast: value doesn't fit in 112 bits"); + if (value > type(uint112).max) { + revert SafeCastOverflowedUintDowncast(112, value); + } return uint112(value); } @@ -336,7 +392,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint104(uint256 value) internal pure returns (uint104) { - require(value <= type(uint104).max, "SafeCast: value doesn't fit in 104 bits"); + if (value > type(uint104).max) { + revert SafeCastOverflowedUintDowncast(104, value); + } return uint104(value); } @@ -353,7 +411,9 @@ library SafeCast { * _Available since v4.2._ */ function toUint96(uint256 value) internal pure returns (uint96) { - require(value <= type(uint96).max, "SafeCast: value doesn't fit in 96 bits"); + if (value > type(uint96).max) { + revert SafeCastOverflowedUintDowncast(96, value); + } return uint96(value); } @@ -370,7 +430,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint88(uint256 value) internal pure returns (uint88) { - require(value <= type(uint88).max, "SafeCast: value doesn't fit in 88 bits"); + if (value > type(uint88).max) { + revert SafeCastOverflowedUintDowncast(88, value); + } return uint88(value); } @@ -387,7 +449,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint80(uint256 value) internal pure returns (uint80) { - require(value <= type(uint80).max, "SafeCast: value doesn't fit in 80 bits"); + if (value > type(uint80).max) { + revert SafeCastOverflowedUintDowncast(80, value); + } return uint80(value); } @@ -404,7 +468,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint72(uint256 value) internal pure returns (uint72) { - require(value <= type(uint72).max, "SafeCast: value doesn't fit in 72 bits"); + if (value > type(uint72).max) { + revert SafeCastOverflowedUintDowncast(72, value); + } return uint72(value); } @@ -421,7 +487,9 @@ library SafeCast { * _Available since v2.5._ */ function toUint64(uint256 value) internal pure returns (uint64) { - require(value <= type(uint64).max, "SafeCast: value doesn't fit in 64 bits"); + if (value > type(uint64).max) { + revert SafeCastOverflowedUintDowncast(64, value); + } return uint64(value); } @@ -438,7 +506,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint56(uint256 value) internal pure returns (uint56) { - require(value <= type(uint56).max, "SafeCast: value doesn't fit in 56 bits"); + if (value > type(uint56).max) { + revert SafeCastOverflowedUintDowncast(56, value); + } return uint56(value); } @@ -455,7 +525,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint48(uint256 value) internal pure returns (uint48) { - require(value <= type(uint48).max, "SafeCast: value doesn't fit in 48 bits"); + if (value > type(uint48).max) { + revert SafeCastOverflowedUintDowncast(48, value); + } return uint48(value); } @@ -472,7 +544,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint40(uint256 value) internal pure returns (uint40) { - require(value <= type(uint40).max, "SafeCast: value doesn't fit in 40 bits"); + if (value > type(uint40).max) { + revert SafeCastOverflowedUintDowncast(40, value); + } return uint40(value); } @@ -489,7 +563,9 @@ library SafeCast { * _Available since v2.5._ */ function toUint32(uint256 value) internal pure returns (uint32) { - require(value <= type(uint32).max, "SafeCast: value doesn't fit in 32 bits"); + if (value > type(uint32).max) { + revert SafeCastOverflowedUintDowncast(32, value); + } return uint32(value); } @@ -506,7 +582,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint24(uint256 value) internal pure returns (uint24) { - require(value <= type(uint24).max, "SafeCast: value doesn't fit in 24 bits"); + if (value > type(uint24).max) { + revert SafeCastOverflowedUintDowncast(24, value); + } return uint24(value); } @@ -523,7 +601,9 @@ library SafeCast { * _Available since v2.5._ */ function toUint16(uint256 value) internal pure returns (uint16) { - require(value <= type(uint16).max, "SafeCast: value doesn't fit in 16 bits"); + if (value > type(uint16).max) { + revert SafeCastOverflowedUintDowncast(16, value); + } return uint16(value); } @@ -540,7 +620,9 @@ library SafeCast { * _Available since v2.5._ */ function toUint8(uint256 value) internal pure returns (uint8) { - require(value <= type(uint8).max, "SafeCast: value doesn't fit in 8 bits"); + if (value > type(uint8).max) { + revert SafeCastOverflowedUintDowncast(8, value); + } return uint8(value); } @@ -554,7 +636,9 @@ library SafeCast { * _Available since v3.0._ */ function toUint256(int256 value) internal pure returns (uint256) { - require(value >= 0, "SafeCast: value must be positive"); + if (value < 0) { + revert SafeCastOverflowedIntToUint(value); + } return uint256(value); } @@ -573,7 +657,9 @@ library SafeCast { */ function toInt248(int256 value) internal pure returns (int248 downcasted) { downcasted = int248(value); - require(downcasted == value, "SafeCast: value doesn't fit in 248 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(248, value); + } } /** @@ -591,7 +677,9 @@ library SafeCast { */ function toInt240(int256 value) internal pure returns (int240 downcasted) { downcasted = int240(value); - require(downcasted == value, "SafeCast: value doesn't fit in 240 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(240, value); + } } /** @@ -609,7 +697,9 @@ library SafeCast { */ function toInt232(int256 value) internal pure returns (int232 downcasted) { downcasted = int232(value); - require(downcasted == value, "SafeCast: value doesn't fit in 232 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(232, value); + } } /** @@ -627,7 +717,9 @@ library SafeCast { */ function toInt224(int256 value) internal pure returns (int224 downcasted) { downcasted = int224(value); - require(downcasted == value, "SafeCast: value doesn't fit in 224 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(224, value); + } } /** @@ -645,7 +737,9 @@ library SafeCast { */ function toInt216(int256 value) internal pure returns (int216 downcasted) { downcasted = int216(value); - require(downcasted == value, "SafeCast: value doesn't fit in 216 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(216, value); + } } /** @@ -663,7 +757,9 @@ library SafeCast { */ function toInt208(int256 value) internal pure returns (int208 downcasted) { downcasted = int208(value); - require(downcasted == value, "SafeCast: value doesn't fit in 208 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(208, value); + } } /** @@ -681,7 +777,9 @@ library SafeCast { */ function toInt200(int256 value) internal pure returns (int200 downcasted) { downcasted = int200(value); - require(downcasted == value, "SafeCast: value doesn't fit in 200 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(200, value); + } } /** @@ -699,7 +797,9 @@ library SafeCast { */ function toInt192(int256 value) internal pure returns (int192 downcasted) { downcasted = int192(value); - require(downcasted == value, "SafeCast: value doesn't fit in 192 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(192, value); + } } /** @@ -717,7 +817,9 @@ library SafeCast { */ function toInt184(int256 value) internal pure returns (int184 downcasted) { downcasted = int184(value); - require(downcasted == value, "SafeCast: value doesn't fit in 184 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(184, value); + } } /** @@ -735,7 +837,9 @@ library SafeCast { */ function toInt176(int256 value) internal pure returns (int176 downcasted) { downcasted = int176(value); - require(downcasted == value, "SafeCast: value doesn't fit in 176 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(176, value); + } } /** @@ -753,7 +857,9 @@ library SafeCast { */ function toInt168(int256 value) internal pure returns (int168 downcasted) { downcasted = int168(value); - require(downcasted == value, "SafeCast: value doesn't fit in 168 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(168, value); + } } /** @@ -771,7 +877,9 @@ library SafeCast { */ function toInt160(int256 value) internal pure returns (int160 downcasted) { downcasted = int160(value); - require(downcasted == value, "SafeCast: value doesn't fit in 160 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(160, value); + } } /** @@ -789,7 +897,9 @@ library SafeCast { */ function toInt152(int256 value) internal pure returns (int152 downcasted) { downcasted = int152(value); - require(downcasted == value, "SafeCast: value doesn't fit in 152 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(152, value); + } } /** @@ -807,7 +917,9 @@ library SafeCast { */ function toInt144(int256 value) internal pure returns (int144 downcasted) { downcasted = int144(value); - require(downcasted == value, "SafeCast: value doesn't fit in 144 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(144, value); + } } /** @@ -825,7 +937,9 @@ library SafeCast { */ function toInt136(int256 value) internal pure returns (int136 downcasted) { downcasted = int136(value); - require(downcasted == value, "SafeCast: value doesn't fit in 136 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(136, value); + } } /** @@ -843,7 +957,9 @@ library SafeCast { */ function toInt128(int256 value) internal pure returns (int128 downcasted) { downcasted = int128(value); - require(downcasted == value, "SafeCast: value doesn't fit in 128 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(128, value); + } } /** @@ -861,7 +977,9 @@ library SafeCast { */ function toInt120(int256 value) internal pure returns (int120 downcasted) { downcasted = int120(value); - require(downcasted == value, "SafeCast: value doesn't fit in 120 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(120, value); + } } /** @@ -879,7 +997,9 @@ library SafeCast { */ function toInt112(int256 value) internal pure returns (int112 downcasted) { downcasted = int112(value); - require(downcasted == value, "SafeCast: value doesn't fit in 112 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(112, value); + } } /** @@ -897,7 +1017,9 @@ library SafeCast { */ function toInt104(int256 value) internal pure returns (int104 downcasted) { downcasted = int104(value); - require(downcasted == value, "SafeCast: value doesn't fit in 104 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(104, value); + } } /** @@ -915,7 +1037,9 @@ library SafeCast { */ function toInt96(int256 value) internal pure returns (int96 downcasted) { downcasted = int96(value); - require(downcasted == value, "SafeCast: value doesn't fit in 96 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(96, value); + } } /** @@ -933,7 +1057,9 @@ library SafeCast { */ function toInt88(int256 value) internal pure returns (int88 downcasted) { downcasted = int88(value); - require(downcasted == value, "SafeCast: value doesn't fit in 88 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(88, value); + } } /** @@ -951,7 +1077,9 @@ library SafeCast { */ function toInt80(int256 value) internal pure returns (int80 downcasted) { downcasted = int80(value); - require(downcasted == value, "SafeCast: value doesn't fit in 80 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(80, value); + } } /** @@ -969,7 +1097,9 @@ library SafeCast { */ function toInt72(int256 value) internal pure returns (int72 downcasted) { downcasted = int72(value); - require(downcasted == value, "SafeCast: value doesn't fit in 72 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(72, value); + } } /** @@ -987,7 +1117,9 @@ library SafeCast { */ function toInt64(int256 value) internal pure returns (int64 downcasted) { downcasted = int64(value); - require(downcasted == value, "SafeCast: value doesn't fit in 64 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(64, value); + } } /** @@ -1005,7 +1137,9 @@ library SafeCast { */ function toInt56(int256 value) internal pure returns (int56 downcasted) { downcasted = int56(value); - require(downcasted == value, "SafeCast: value doesn't fit in 56 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(56, value); + } } /** @@ -1023,7 +1157,9 @@ library SafeCast { */ function toInt48(int256 value) internal pure returns (int48 downcasted) { downcasted = int48(value); - require(downcasted == value, "SafeCast: value doesn't fit in 48 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(48, value); + } } /** @@ -1041,7 +1177,9 @@ library SafeCast { */ function toInt40(int256 value) internal pure returns (int40 downcasted) { downcasted = int40(value); - require(downcasted == value, "SafeCast: value doesn't fit in 40 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(40, value); + } } /** @@ -1059,7 +1197,9 @@ library SafeCast { */ function toInt32(int256 value) internal pure returns (int32 downcasted) { downcasted = int32(value); - require(downcasted == value, "SafeCast: value doesn't fit in 32 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(32, value); + } } /** @@ -1077,7 +1217,9 @@ library SafeCast { */ function toInt24(int256 value) internal pure returns (int24 downcasted) { downcasted = int24(value); - require(downcasted == value, "SafeCast: value doesn't fit in 24 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(24, value); + } } /** @@ -1095,7 +1237,9 @@ library SafeCast { */ function toInt16(int256 value) internal pure returns (int16 downcasted) { downcasted = int16(value); - require(downcasted == value, "SafeCast: value doesn't fit in 16 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(16, value); + } } /** @@ -1113,7 +1257,9 @@ library SafeCast { */ function toInt8(int256 value) internal pure returns (int8 downcasted) { downcasted = int8(value); - require(downcasted == value, "SafeCast: value doesn't fit in 8 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(8, value); + } } /** @@ -1127,7 +1273,9 @@ library SafeCast { */ function toInt256(uint256 value) internal pure returns (int256) { // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive - require(value <= uint256(type(int256).max), "SafeCast: value doesn't fit in an int256"); + if (value > uint256(type(int256).max)) { + revert SafeCastOverflowedUintToInt(value); + } return int256(value); } } diff --git a/contracts/utils/structs/Checkpoints.sol b/contracts/utils/structs/Checkpoints.sol index a0c45f65979..7608d787ec3 100644 --- a/contracts/utils/structs/Checkpoints.sol +++ b/contracts/utils/structs/Checkpoints.sol @@ -17,6 +17,11 @@ import "../math/SafeCast.sol"; * _Available since v4.5._ */ library Checkpoints { + /** + * @dev A value was attempted to be inserted on a past checkpoint. + */ + error CheckpointUnorderedInsertion(); + struct Trace224 { Checkpoint224[] _checkpoints; } @@ -126,7 +131,9 @@ library Checkpoints { Checkpoint224 memory last = _unsafeAccess(self, pos - 1); // Checkpoint keys must be non-decreasing. - require(last._key <= key, "Checkpoint: decreasing keys"); + if (last._key > key) { + revert CheckpointUnorderedInsertion(); + } // Update or push new checkpoint if (last._key == key) { @@ -309,7 +316,9 @@ library Checkpoints { Checkpoint160 memory last = _unsafeAccess(self, pos - 1); // Checkpoint keys must be non-decreasing. - require(last._key <= key, "Checkpoint: decreasing keys"); + if (last._key > key) { + revert CheckpointUnorderedInsertion(); + } // Update or push new checkpoint if (last._key == key) { diff --git a/contracts/utils/structs/DoubleEndedQueue.sol b/contracts/utils/structs/DoubleEndedQueue.sol index be6e3898ffb..69db700407e 100644 --- a/contracts/utils/structs/DoubleEndedQueue.sol +++ b/contracts/utils/structs/DoubleEndedQueue.sol @@ -22,12 +22,12 @@ library DoubleEndedQueue { /** * @dev An operation (e.g. {front}) couldn't be completed due to the queue being empty. */ - error Empty(); + error QueueEmpty(); /** * @dev An operation (e.g. {at}) couldn't be completed due to an index being out of bounds. */ - error OutOfBounds(); + error QueueOutOfBounds(); /** * @dev Indices are signed integers because the queue can grow in any direction. They are 128 bits so begin and end @@ -61,10 +61,10 @@ library DoubleEndedQueue { /** * @dev Removes the item at the end of the queue and returns it. * - * Reverts with `Empty` if the queue is empty. + * Reverts with `QueueEmpty` if the queue is empty. */ function popBack(Bytes32Deque storage deque) internal returns (bytes32 value) { - if (empty(deque)) revert Empty(); + if (empty(deque)) revert QueueEmpty(); int128 backIndex; unchecked { backIndex = deque._end - 1; @@ -89,10 +89,10 @@ library DoubleEndedQueue { /** * @dev Removes the item at the beginning of the queue and returns it. * - * Reverts with `Empty` if the queue is empty. + * Reverts with `QueueEmpty` if the queue is empty. */ function popFront(Bytes32Deque storage deque) internal returns (bytes32 value) { - if (empty(deque)) revert Empty(); + if (empty(deque)) revert QueueEmpty(); int128 frontIndex = deque._begin; value = deque._data[frontIndex]; delete deque._data[frontIndex]; @@ -104,10 +104,10 @@ library DoubleEndedQueue { /** * @dev Returns the item at the beginning of the queue. * - * Reverts with `Empty` if the queue is empty. + * Reverts with `QueueEmpty` if the queue is empty. */ function front(Bytes32Deque storage deque) internal view returns (bytes32 value) { - if (empty(deque)) revert Empty(); + if (empty(deque)) revert QueueEmpty(); int128 frontIndex = deque._begin; return deque._data[frontIndex]; } @@ -115,10 +115,10 @@ library DoubleEndedQueue { /** * @dev Returns the item at the end of the queue. * - * Reverts with `Empty` if the queue is empty. + * Reverts with `QueueEmpty` if the queue is empty. */ function back(Bytes32Deque storage deque) internal view returns (bytes32 value) { - if (empty(deque)) revert Empty(); + if (empty(deque)) revert QueueEmpty(); int128 backIndex; unchecked { backIndex = deque._end - 1; @@ -130,12 +130,12 @@ library DoubleEndedQueue { * @dev Return the item at a position in the queue given by `index`, with the first item at 0 and last item at * `length(deque) - 1`. * - * Reverts with `OutOfBounds` if the index is out of bounds. + * Reverts with `QueueOutOfBounds` if the index is out of bounds. */ function at(Bytes32Deque storage deque, uint256 index) internal view returns (bytes32 value) { // int256(deque._begin) is a safe upcast int128 idx = SafeCast.toInt128(int256(deque._begin) + SafeCast.toInt256(index)); - if (idx >= deque._end) revert OutOfBounds(); + if (idx >= deque._end) revert QueueOutOfBounds(); return deque._data[idx]; } diff --git a/contracts/utils/structs/EnumerableMap.sol b/contracts/utils/structs/EnumerableMap.sol index 4bd18055dbf..a474e82b3e0 100644 --- a/contracts/utils/structs/EnumerableMap.sol +++ b/contracts/utils/structs/EnumerableMap.sol @@ -57,6 +57,11 @@ library EnumerableMap { // This means that we can only create new EnumerableMaps for types that fit // in bytes32. + /** + * @dev Query for a nonexistent map key. + */ + error EnumerableMapNonexistentKey(bytes32 key); + struct Bytes32ToBytes32Map { // Storage of keys EnumerableSet.Bytes32Set _keys; @@ -136,7 +141,9 @@ library EnumerableMap { */ function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) { bytes32 value = map._values[key]; - require(value != 0 || contains(map, key), "EnumerableMap: nonexistent key"); + if (value == 0 && !contains(map, key)) { + revert EnumerableMapNonexistentKey(key); + } return value; } diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index b3b51abfcc2..b46a8d5a36b 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -45,6 +45,8 @@ To keep your system secure, you should **always** use the installed code as-is, Please report any security issues you find via our https://www.immunefi.com/bounty/openzeppelin[bug bounty program on Immunefi] or directly to security@openzeppelin.org. +The https://contracts.openzeppelin.com/security[Security Center] contains more details about the secure development process. + [[next-steps]] == Learn More diff --git a/hardhat.config.js b/hardhat.config.js index 66367e96e02..6cb8b91441f 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -40,7 +40,7 @@ const argv = require('yargs/yargs')() compiler: { alias: 'compileVersion', type: 'string', - default: '0.8.19', + default: '0.8.20', }, coinmarketcap: { alias: 'coinmarketcapApiKey', diff --git a/package-lock.json b/package-lock.json index edf1e9284e6..d4cb52694fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openzeppelin-solidity", - "version": "4.9.0", + "version": "4.9.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "openzeppelin-solidity", - "version": "4.9.0", + "version": "4.9.2", "license": "MIT", "devDependencies": { "@changesets/changelog-github": "^0.4.8", diff --git a/package.json b/package.json index c070915f9e9..37e8f871012 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openzeppelin-solidity", "description": "Secure Smart Contract library for Solidity", - "version": "4.9.0", + "version": "4.9.2", "files": [ "/contracts/**/*.sol", "/build/contracts/*.json", diff --git a/scripts/checks/compare-layout.js b/scripts/checks/compare-layout.js index 7b7df992862..4368b77fb37 100644 --- a/scripts/checks/compare-layout.js +++ b/scripts/checks/compare-layout.js @@ -10,6 +10,7 @@ for (const name in oldLayout) { if (name in newLayout) { const report = getStorageUpgradeReport(oldLayout[name], newLayout[name], {}); if (!report.ok) { + console.log(`Storage layout incompatilibity found in ${name}:`); console.log(report.explain()); process.exitCode = 1; } diff --git a/scripts/checks/inheritance-ordering.js b/scripts/checks/inheritance-ordering.js index 45c707e6fcd..72aa37ef7b3 100755 --- a/scripts/checks/inheritance-ordering.js +++ b/scripts/checks/inheritance-ordering.js @@ -13,7 +13,7 @@ for (const artifact of artifacts) { const linearized = []; for (const source in solcOutput.contracts) { - if (source.includes('/mocks/')) { + if (['contracts-exposed/', 'contracts/mocks/'].some(pattern => source.startsWith(pattern))) { continue; } diff --git a/scripts/generate/templates/Checkpoints.js b/scripts/generate/templates/Checkpoints.js index c85cfcbb37f..d28134ce726 100644 --- a/scripts/generate/templates/Checkpoints.js +++ b/scripts/generate/templates/Checkpoints.js @@ -19,6 +19,13 @@ import "../math/SafeCast.sol"; */ `; +const errors = `\ + /** + * @dev A value was attempted to be inserted on a past checkpoint. + */ + error CheckpointUnorderedInsertion(); +`; + const template = opts => `\ struct ${opts.historyTypeName} { ${opts.checkpointTypeName}[] ${opts.checkpointFieldName}; @@ -145,7 +152,9 @@ function _insert( ${opts.checkpointTypeName} memory last = _unsafeAccess(self, pos - 1); // Checkpoint keys must be non-decreasing. - require(last.${opts.keyFieldName} <= key, "Checkpoint: decreasing keys"); + if(last.${opts.keyFieldName} > key) { + revert CheckpointUnorderedInsertion(); + } // Update or push new checkpoint if (last.${opts.keyFieldName} == key) { @@ -226,6 +235,7 @@ function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos) module.exports = format( header.trimEnd(), 'library Checkpoints {', + errors, OPTS.flatMap(opts => template(opts)), '}', ); diff --git a/scripts/generate/templates/EnumerableMap.js b/scripts/generate/templates/EnumerableMap.js index 13d3d8686b1..8899f481994 100644 --- a/scripts/generate/templates/EnumerableMap.js +++ b/scripts/generate/templates/EnumerableMap.js @@ -66,6 +66,11 @@ const defaultMap = () => `\ // This means that we can only create new EnumerableMaps for types that fit // in bytes32. +/** + * @dev Query for a nonexistent map key. + */ +error EnumerableMapNonexistentKey(bytes32 key); + struct Bytes32ToBytes32Map { // Storage of keys EnumerableSet.Bytes32Set _keys; @@ -149,7 +154,9 @@ function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view retu */ function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) { bytes32 value = map._values[key]; - require(value != 0 || contains(map, key), "EnumerableMap: nonexistent key"); + if(value == 0 && !contains(map, key)) { + revert EnumerableMapNonexistentKey(key); + } return value; } diff --git a/scripts/generate/templates/SafeCast.js b/scripts/generate/templates/SafeCast.js index 0d78a2ca34a..6a4a80c2b45 100644 --- a/scripts/generate/templates/SafeCast.js +++ b/scripts/generate/templates/SafeCast.js @@ -77,6 +77,28 @@ pragma solidity ^0.8.19; */ `; +const errors = `\ + /** + * @dev Value doesn't fit in an uint of \`bits\` size. + */ + error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value); + + /** + * @dev An int value doesn't fit in an uint of \`bits\` size. + */ + error SafeCastOverflowedIntToUint(int256 value); + + /** + * @dev Value doesn't fit in an int of \`bits\` size. + */ + error SafeCastOverflowedIntDowncast(uint8 bits, int256 value); + + /** + * @dev An uint value doesn't fit in an int of \`bits\` size. + */ + error SafeCastOverflowedUintToInt(uint256 value); +`; + const toUintDownCast = length => `\ /** * @dev Returns the downcasted uint${length} from uint256, reverting on @@ -91,7 +113,9 @@ const toUintDownCast = length => `\ * _Available since v${version('toUint(uint)', length)}._ */ function toUint${length}(uint256 value) internal pure returns (uint${length}) { - require(value <= type(uint${length}).max, "SafeCast: value doesn't fit in ${length} bits"); + if (value > type(uint${length}).max) { + revert SafeCastOverflowedUintDowncast(${length}, value); + } return uint${length}(value); } `; @@ -113,7 +137,9 @@ const toIntDownCast = length => `\ */ function toInt${length}(int256 value) internal pure returns (int${length} downcasted) { downcasted = int${length}(value); - require(downcasted == value, "SafeCast: value doesn't fit in ${length} bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(${length}, value); + } } `; /* eslint-enable max-len */ @@ -130,7 +156,9 @@ const toInt = length => `\ */ function toInt${length}(uint${length} value) internal pure returns (int${length}) { // Note: Unsafe cast below is okay because \`type(int${length}).max\` is guaranteed to be positive - require(value <= uint${length}(type(int${length}).max), "SafeCast: value doesn't fit in an int${length}"); + if (value > uint${length}(type(int${length}).max)) { + revert SafeCastOverflowedUintToInt(value); + } return int${length}(value); } `; @@ -146,7 +174,9 @@ const toUint = length => `\ * _Available since v${version('toUint(int)', length)}._ */ function toUint${length}(int${length} value) internal pure returns (uint${length}) { - require(value >= 0, "SafeCast: value must be positive"); + if (value < 0) { + revert SafeCastOverflowedIntToUint(value); + } return uint${length}(value); } `; @@ -155,6 +185,7 @@ function toUint${length}(int${length} value) internal pure returns (uint${length module.exports = format( header.trimEnd(), 'library SafeCast {', + errors, [...LENGTHS.map(toUintDownCast), toUint(256), ...LENGTHS.map(toIntDownCast), toInt(256)], '}', ); diff --git a/scripts/generate/templates/StorageSlot.js b/scripts/generate/templates/StorageSlot.js index b51affc22f8..3e2263a0ccd 100644 --- a/scripts/generate/templates/StorageSlot.js +++ b/scripts/generate/templates/StorageSlot.js @@ -38,7 +38,7 @@ pragma solidity ^0.8.19; * } * * function _setImplementation(address newImplementation) internal { - * require(newImplementation.code.length > 0, "ERC1967: new implementation is not a contract"); + * require(newImplementation.code.length > 0); * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; * } * } diff --git a/scripts/release/workflow/prepare-release-merge.sh b/scripts/release/workflow/prepare-release-merge.sh deleted file mode 100644 index 4e6da5145dd..00000000000 --- a/scripts/release/workflow/prepare-release-merge.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# Define merge branch name -MERGE_BRANCH=merge/$GITHUB_REF_NAME - -# Create the branch and force to start from ref -git checkout -B "$MERGE_BRANCH" "$GITHUB_REF_NAME" - -# Get deleted changesets in this branch that might conflict with master -# --diff-filter=D - Only deleted files -readarray -t DELETED_CHANGESETS < <(git diff origin/master --diff-filter=D --name-only -- '.changeset/*.md') - -# Merge master, which will take those files cherry-picked. Auto-resolve conflicts favoring master. -# Ignore conflicts that can't be resolved. -git merge origin/master -m "Merge master to $GITHUB_REF_NAME" -X theirs || true - -# Remove the originally deleted changesets to correctly sync with master -rm -f "${DELETED_CHANGESETS[@]}" - -# Only git add deleted files -git ls-files --deleted .changeset/ | xargs git add - -# Allow empty here since there may be no changes if `rm -f` failed for all changesets -git commit --allow-empty -m "Sync changesets with master" -git push -f origin "$MERGE_BRANCH" diff --git a/scripts/release/workflow/publish.sh b/scripts/release/workflow/publish.sh index 41a9975cb55..e490e5d0078 100644 --- a/scripts/release/workflow/publish.sh +++ b/scripts/release/workflow/publish.sh @@ -2,19 +2,25 @@ set -euo pipefail +PACKAGE_JSON_NAME="$(tar xfO "$TARBALL" package/package.json | jq -r .name)" +PACKAGE_JSON_VERSION="$(tar xfO "$TARBALL" package/package.json | jq -r .version)" + # Intentionally escape $ to avoid interpolation and writing the token to disk echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc # Actual publish npm publish "$TARBALL" --tag "$TAG" +# Clean up tags delete_tag() { - PACKAGE_JSON_NAME="$(tar xfO "$TARBALL" package/package.json | jq -r .name)" npm dist-tag rm "$PACKAGE_JSON_NAME" "$1" } if [ "$TAG" = tmp ]; then delete_tag "$TAG" elif [ "$TAG" = latest ]; then - delete_tag next + # Delete the next tag if it exists and is a prerelease for what is currently being published + if npm dist-tag ls "$PACKAGE_JSON_NAME" | grep -q "next: $PACKAGE_JSON_VERSION"; then + delete_tag next + fi fi diff --git a/scripts/release/workflow/state.js b/scripts/release/workflow/state.js index 5cfaafb8689..914e8de0222 100644 --- a/scripts/release/workflow/state.js +++ b/scripts/release/workflow/state.js @@ -47,7 +47,7 @@ function shouldRunMerge({ hasPendingChangesets, prBackExists, }) { - return isReleaseBranch && isPush && !prerelease && isCurrentFinalVersion && !hasPendingChangesets && prBackExists; + return isReleaseBranch && isPush && !prerelease && isCurrentFinalVersion && !hasPendingChangesets && !prBackExists; } async function getState({ github, context, core }) { @@ -79,7 +79,7 @@ async function getState({ github, context, core }) { state: 'open', }); - state.prBackExists = prs.length === 0; + state.prBackExists = prs.length !== 0; state.isPublishedOnNpm = await isPublishedOnNpm(packageName, version); diff --git a/scripts/upgradeable/transpile.sh b/scripts/upgradeable/transpile.sh index e3aa31cc03c..fbffe844e34 100644 --- a/scripts/upgradeable/transpile.sh +++ b/scripts/upgradeable/transpile.sh @@ -29,7 +29,7 @@ npx @openzeppelin/upgrade-safe-transpiler@latest -D \ -x 'contracts/proxy/**/*' \ -x '!contracts/proxy/Clones.sol' \ -x '!contracts/proxy/ERC1967/ERC1967Storage.sol' \ - -x '!contracts/proxy/ERC1967/ERC1967Upgrade.sol' \ + -x '!contracts/proxy/ERC1967/ERC1967Utils.sol' \ -x '!contracts/proxy/utils/UUPSUpgradeable.sol' \ -x '!contracts/proxy/beacon/IBeacon.sol' \ -p 'contracts/**/presets/**/*' diff --git a/scripts/upgradeable/upgradeable.patch b/scripts/upgradeable/upgradeable.patch index b29e7d12bed..ecd0a3711d7 100644 --- a/scripts/upgradeable/upgradeable.patch +++ b/scripts/upgradeable/upgradeable.patch @@ -59,10 +59,10 @@ index ff596b0c..00000000 - - diff --git a/README.md b/README.md -index 9fc95518..53130e3c 100644 +index 9d1c405b..c264e29c 100644 --- a/README.md +++ b/README.md -@@ -16,17 +16,20 @@ +@@ -19,6 +19,9 @@ :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. @@ -72,6 +72,8 @@ index 9fc95518..53130e3c 100644 ## Overview ### Installation +@@ -26,7 +29,7 @@ + #### Hardhat, Truffle (npm) ``` -$ npm install @openzeppelin/contracts @@ -79,13 +81,16 @@ index 9fc95518..53130e3c 100644 ``` OpenZeppelin Contracts features a [stable API](https://docs.openzeppelin.com/contracts/releases-stability#api-stability), which means that your contracts won't break unexpectedly when upgrading to a newer minor version. +@@ -38,7 +41,7 @@ OpenZeppelin Contracts features a [stable API](https://docs.openzeppelin.com/con + > **Warning** Foundry installs the latest version initially, but subsequent `forge update` commands will use the `master` branch. --An alternative to npm is to use the GitHub repository (`openzeppelin/openzeppelin-contracts`) to retrieve the contracts. When doing this, make sure to specify the tag for a release such as `v4.5.0`, instead of using the `master` branch. -+An alternative to npm is to use the GitHub repository (`openzeppelin/openzeppelin-contracts-upgradeable`) to retrieve the contracts. When doing this, make sure to specify the tag for a release such as `v4.5.0`, instead of using the `master` branch. + ``` +-$ forge install OpenZeppelin/openzeppelin-contracts ++$ forge install OpenZeppelin/openzeppelin-contracts-upgradeable + ``` ### Usage - -@@ -35,10 +38,11 @@ Once installed, you can use the contracts in the library by importing them: +@@ -48,10 +51,11 @@ Once installed, you can use the contracts in the library by importing them: ```solidity pragma solidity ^0.8.19; @@ -101,7 +106,7 @@ index 9fc95518..53130e3c 100644 } ``` diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol -index bb70d19f..38513771 100644 +index ebdf0a33..8888803e 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -18,6 +18,8 @@ import "../utils/Context.sol"; @@ -114,7 +119,7 @@ index bb70d19f..38513771 100644 contract VestingWallet is Context { event EtherReleased(uint256 amount); diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol -index 64431711..885f0e42 100644 +index 5d8318f4..ef3cde55 100644 --- a/contracts/governance/extensions/GovernorVotes.sol +++ b/contracts/governance/extensions/GovernorVotes.sol @@ -10,6 +10,8 @@ import "../../interfaces/IERC5805.sol"; @@ -127,7 +132,7 @@ index 64431711..885f0e42 100644 abstract contract GovernorVotes is Governor { IERC5805 public immutable token; diff --git a/contracts/package.json b/contracts/package.json -index 55e70b17..ceefb984 100644 +index df141192..1cf90ad1 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,5 +1,5 @@ @@ -135,7 +140,7 @@ index 55e70b17..ceefb984 100644 - "name": "@openzeppelin/contracts", + "name": "@openzeppelin/contracts-upgradeable", "description": "Secure Smart Contract library for Solidity", - "version": "4.8.2", + "version": "4.9.2", "files": [ @@ -13,7 +13,7 @@ }, @@ -147,7 +152,7 @@ index 55e70b17..ceefb984 100644 "keywords": [ "solidity", diff --git a/contracts/token/ERC20/extensions/ERC20Capped.sol b/contracts/token/ERC20/extensions/ERC20Capped.sol -index 16f830d1..9ef98148 100644 +index 41e9ce5c..1d910dfa 100644 --- a/contracts/token/ERC20/extensions/ERC20Capped.sol +++ b/contracts/token/ERC20/extensions/ERC20Capped.sol @@ -7,6 +7,8 @@ import "../ERC20.sol"; @@ -160,20 +165,20 @@ index 16f830d1..9ef98148 100644 abstract contract ERC20Capped is ERC20 { uint256 private immutable _cap; diff --git a/contracts/token/ERC20/extensions/ERC20Permit.sol b/contracts/token/ERC20/extensions/ERC20Permit.sol -index a357199b..9dc8e894 100644 +index 4378eb7c..1da9e731 100644 --- a/contracts/token/ERC20/extensions/ERC20Permit.sol +++ b/contracts/token/ERC20/extensions/ERC20Permit.sol -@@ -18,6 +18,8 @@ import "../../../utils/Counters.sol"; +@@ -18,6 +18,8 @@ import "../../../utils/Nonces.sol"; * need to send a transaction, and thus is not required to hold Ether at all. * * _Available since v3.4._ + * + * @custom:storage-size 51 */ - abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712 { - using Counters for Counters.Counter; + abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { + // solhint-disable-next-line var-name-mixedcase diff --git a/contracts/token/ERC20/extensions/ERC20Wrapper.sol b/contracts/token/ERC20/extensions/ERC20Wrapper.sol -index bfe782e4..7264fe32 100644 +index 389965e9..66436b14 100644 --- a/contracts/token/ERC20/extensions/ERC20Wrapper.sol +++ b/contracts/token/ERC20/extensions/ERC20Wrapper.sol @@ -14,6 +14,8 @@ import "../utils/SafeERC20.sol"; @@ -186,7 +191,7 @@ index bfe782e4..7264fe32 100644 abstract contract ERC20Wrapper is ERC20 { IERC20 private immutable _underlying; diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol -index 6a4e1cad..55d8eced 100644 +index 2628014f..7d5193c8 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -4,7 +4,6 @@ @@ -268,7 +273,7 @@ index 6a4e1cad..55d8eced 100644 } /** -@@ -129,14 +114,80 @@ abstract contract EIP712 is IERC5267 { +@@ -128,6 +113,10 @@ abstract contract EIP712 is IERC5267 { uint256[] memory extensions ) { @@ -278,34 +283,34 @@ index 6a4e1cad..55d8eced 100644 + return ( hex"0f", // 01111 -- _name.toStringWithFallback(_nameFallback), -- _version.toStringWithFallback(_versionFallback), -+ _EIP712Name(), -+ _EIP712Version(), - block.chainid, - address(this), - bytes32(0), - new uint256[](0) - ); - } -+ -+ /** -+ * @dev The name parameter for the EIP712 domain. -+ * + _EIP712Name(), +@@ -142,26 +131,62 @@ abstract contract EIP712 is IERC5267 { + /** + * @dev The name parameter for the EIP712 domain. + * +- * NOTE: By default this function reads _name which is an immutable value. +- * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). +- * +- * _Available since v5.0._ + * NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs + * are a concern. -+ */ -+ function _EIP712Name() internal virtual view returns (string memory) { + */ +- // solhint-disable-next-line func-name-mixedcase +- function _EIP712Name() internal view returns (string memory) { +- return _name.toStringWithFallback(_nameFallback); ++ function _EIP712Name() internal view virtual returns (string memory) { + return _name; -+ } -+ -+ /** -+ * @dev The version parameter for the EIP712 domain. -+ * + } + + /** + * @dev The version parameter for the EIP712 domain. + * +- * NOTE: By default this function reads _version which is an immutable value. +- * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). + * NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs + * are a concern. + */ -+ function _EIP712Version() internal virtual view returns (string memory) { ++ function _EIP712Version() internal view virtual returns (string memory) { + return _version; + } + @@ -332,9 +337,13 @@ index 6a4e1cad..55d8eced 100644 + + /** + * @dev The hash of the version parameter for the EIP712 domain. -+ * + * +- * _Available since v5.0._ + * NOTE: In previous versions this function was virtual. In this version you should override `_EIP712Version` instead. -+ */ + */ +- // solhint-disable-next-line func-name-mixedcase +- function _EIP712Version() internal view returns (string memory) { +- return _version.toStringWithFallback(_versionFallback); + function _EIP712VersionHash() internal view returns (bytes32) { + string memory version = _EIP712Version(); + if (bytes(version).length > 0) { @@ -349,13 +358,13 @@ index 6a4e1cad..55d8eced 100644 + return keccak256(""); + } + } -+ } + } } diff --git a/package.json b/package.json -index 8458dd61..b4672240 100644 +index 37e8f871..d098669f 100644 --- a/package.json +++ b/package.json -@@ -36,7 +36,7 @@ +@@ -33,7 +33,7 @@ }, "repository": { "type": "git", @@ -365,7 +374,7 @@ index 8458dd61..b4672240 100644 "keywords": [ "solidity", diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js -index 54a4e772..ba4602ed 100644 +index 7ea535b7..32e3a370 100644 --- a/test/utils/cryptography/EIP712.test.js +++ b/test/utils/cryptography/EIP712.test.js @@ -47,26 +47,6 @@ contract('EIP712', function (accounts) { diff --git a/test/access/AccessControl.behavior.js b/test/access/AccessControl.behavior.js index 3e61616a743..b1729c5d6ac 100644 --- a/test/access/AccessControl.behavior.js +++ b/test/access/AccessControl.behavior.js @@ -1,4 +1,5 @@ -const { expectEvent, expectRevert, constants, BN } = require('@openzeppelin/test-helpers'); +const { expectEvent, constants, BN } = require('@openzeppelin/test-helpers'); +const { expectRevertCustomError } = require('../helpers/customError'); const { expect } = require('chai'); const { time } = require('@nomicfoundation/hardhat-network-helpers'); @@ -12,7 +13,7 @@ const ROLE = web3.utils.soliditySha3('ROLE'); const OTHER_ROLE = web3.utils.soliditySha3('OTHER_ROLE'); const ZERO = web3.utils.toBN(0); -function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, otherAdmin) { +function shouldBehaveLikeAccessControl(admin, authorized, other, otherAdmin) { shouldSupportInterfaces(['AccessControl']); describe('default admin', function () { @@ -35,9 +36,10 @@ function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, ot }); it('non-admin cannot grant role to other accounts', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.grantRole(ROLE, authorized, { from: other }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], ); }); @@ -69,9 +71,10 @@ function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, ot }); it('non-admin cannot revoke role', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.revokeRole(ROLE, authorized, { from: other }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], ); }); @@ -103,9 +106,10 @@ function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, ot }); it('only the sender can renounce their roles', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.renounceRole(ROLE, authorized, { from: admin }), - `${errorPrefix}: can only renounce roles for self`, + 'AccessControlBadConfirmation', + [], ); }); @@ -146,16 +150,18 @@ function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, ot }); it("a role's previous admins no longer grant roles", async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.grantRole(ROLE, authorized, { from: admin }), - `${errorPrefix}: account ${admin.toLowerCase()} is missing role ${OTHER_ROLE}`, + 'AccessControlUnauthorizedAccount', + [admin.toLowerCase(), OTHER_ROLE], ); }); it("a role's previous admins no longer revoke roles", async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.revokeRole(ROLE, authorized, { from: admin }), - `${errorPrefix}: account ${admin.toLowerCase()} is missing role ${OTHER_ROLE}`, + 'AccessControlUnauthorizedAccount', + [admin.toLowerCase(), OTHER_ROLE], ); }); }); @@ -170,22 +176,24 @@ function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, ot }); it("revert if sender doesn't have role #1", async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.methods['$_checkRole(bytes32)'](ROLE, { from: other }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, ROLE], ); }); it("revert if sender doesn't have role #2", async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.methods['$_checkRole(bytes32)'](OTHER_ROLE, { from: authorized }), - `${errorPrefix}: account ${authorized.toLowerCase()} is missing role ${OTHER_ROLE}`, + 'AccessControlUnauthorizedAccount', + [authorized.toLowerCase(), OTHER_ROLE], ); }); }); } -function shouldBehaveLikeAccessControlEnumerable(errorPrefix, admin, authorized, other, otherAdmin, otherAuthorized) { +function shouldBehaveLikeAccessControlEnumerable(admin, authorized, other, otherAdmin, otherAuthorized) { shouldSupportInterfaces(['AccessControlEnumerable']); describe('enumerating', function () { @@ -215,18 +223,9 @@ function shouldBehaveLikeAccessControlEnumerable(errorPrefix, admin, authorized, }); } -function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defaultAdmin, newDefaultAdmin, other) { +function shouldBehaveLikeAccessControlDefaultAdminRules(delay, defaultAdmin, newDefaultAdmin, other) { shouldSupportInterfaces(['AccessControlDefaultAdminRules']); - function expectNoEvent(receipt, eventName) { - try { - expectEvent(receipt, eventName); - throw new Error(`${eventName} event found`); - } catch (err) { - expect(err.message).to.eq(`No '${eventName}' events found: expected false to equal true`); - } - } - for (const getter of ['owner', 'defaultAdmin']) { describe(`${getter}()`, function () { it('has a default set to the initial default admin', async function () { @@ -366,30 +365,34 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa }); it('should revert if granting default admin role', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }), - `${errorPrefix}: can't directly grant default admin role`, + 'AccessControlEnforcedDefaultAdminRules', + [], ); }); it('should revert if revoking default admin role', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.revokeRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }), - `${errorPrefix}: can't directly revoke default admin role`, + 'AccessControlEnforcedDefaultAdminRules', + [], ); }); it("should revert if defaultAdmin's admin is changed", async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.$_setRoleAdmin(DEFAULT_ADMIN_ROLE, defaultAdmin), - `${errorPrefix}: can't violate default admin rules`, + 'AccessControlEnforcedDefaultAdminRules', + [], ); }); it('should not grant the default admin role twice', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin), - `${errorPrefix}: default admin already granted`, + 'AccessControlEnforcedDefaultAdminRules', + [], ); }); @@ -398,9 +401,10 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa let acceptSchedule; it('reverts if called by non default admin accounts', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: other }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], ); }); @@ -456,7 +460,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa await this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }); const receipt = await this.accessControl.beginDefaultAdminTransfer(other, { from: newDefaultAdmin }); - expectNoEvent(receipt, 'DefaultAdminTransferCanceled'); + expectEvent.notEmitted(receipt, 'DefaultAdminTransferCanceled'); }); }); @@ -506,9 +510,10 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa it('should revert if caller is not pending default admin', async function () { await time.setNextBlockTimestamp(acceptSchedule.addn(1)); - await expectRevert( + await expectRevertCustomError( this.accessControl.acceptDefaultAdminTransfer({ from: other }), - `${errorPrefix}: pending admin must accept`, + 'AccessControlInvalidDefaultAdmin', + [other], ); }); @@ -549,9 +554,10 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa ]) { it(`should revert if block.timestamp is ${tag} to schedule`, async function () { await time.setNextBlockTimestamp(acceptSchedule.toNumber() + fromSchedule); - await expectRevert( + await expectRevertCustomError( this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }), - `${errorPrefix}: transfer delay not passed`, + 'AccessControlEnforcedDefaultAdminDelay', + [acceptSchedule], ); }); } @@ -560,9 +566,10 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa describe('cancels a default admin transfer', function () { it('reverts if called by non default admin accounts', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.cancelDefaultAdminTransfer({ from: other }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], ); }); @@ -600,9 +607,10 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa await time.setNextBlockTimestamp(acceptSchedule.addn(1)); // Previous pending default admin should not be able to accept after cancellation. - await expectRevert( + await expectRevertCustomError( this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }), - `${errorPrefix}: pending admin must accept`, + 'AccessControlInvalidDefaultAdmin', + [newDefaultAdmin], ); }); }); @@ -615,7 +623,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa expect(newAdmin).to.equal(constants.ZERO_ADDRESS); expect(schedule).to.be.bignumber.equal(ZERO); - expectNoEvent(receipt, 'DefaultAdminTransferCanceled'); + expectEvent.notEmitted(receipt, 'DefaultAdminTransferCanceled'); }); }); }); @@ -634,9 +642,10 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa it('reverts if caller is not default admin', async function () { await time.setNextBlockTimestamp(delayPassed); - await expectRevert( + await expectRevertCustomError( this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, other, { from: defaultAdmin }), - `${errorPrefix}: can only renounce roles for self`, + 'AccessControlBadConfirmation', + [], ); }); @@ -693,9 +702,10 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa ]) { it(`reverts if block.timestamp is ${tag} to schedule`, async function () { await time.setNextBlockTimestamp(delayNotPassed.toNumber() + fromSchedule); - await expectRevert( + await expectRevertCustomError( this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }), - `${errorPrefix}: only can renounce in two delayed steps`, + 'AccessControlEnforcedDefaultAdminDelay', + [expectedSchedule], ); }); } @@ -704,11 +714,12 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa describe('changes delay', function () { it('reverts if called by non default admin accounts', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.changeDefaultAdminDelay(time.duration.hours(4), { from: other, }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], ); }); @@ -792,7 +803,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa from: defaultAdmin, }); - const eventMatcher = passed ? expectNoEvent : expectEvent; + const eventMatcher = passed ? expectEvent.notEmitted : expectEvent; eventMatcher(receipt, 'DefaultAdminDelayChangeCanceled'); }); } @@ -803,9 +814,10 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa describe('rollbacks a delay change', function () { it('reverts if called by non default admin accounts', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.rollbackDefaultAdminDelay({ from: other }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], ); }); @@ -841,7 +853,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa const receipt = await this.accessControl.rollbackDefaultAdminDelay({ from: defaultAdmin }); - const eventMatcher = passed ? expectNoEvent : expectEvent; + const eventMatcher = passed ? expectEvent.notEmitted : expectEvent; eventMatcher(receipt, 'DefaultAdminDelayChangeCanceled'); }); } diff --git a/test/access/AccessControl.test.js b/test/access/AccessControl.test.js index 90efad3d013..14463b5052e 100644 --- a/test/access/AccessControl.test.js +++ b/test/access/AccessControl.test.js @@ -8,5 +8,5 @@ contract('AccessControl', function (accounts) { await this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, accounts[0]); }); - shouldBehaveLikeAccessControl('AccessControl', ...accounts); + shouldBehaveLikeAccessControl(...accounts); }); diff --git a/test/access/AccessControlDefaultAdminRules.test.js b/test/access/AccessControlDefaultAdminRules.test.js index be112481edb..b8eae322088 100644 --- a/test/access/AccessControlDefaultAdminRules.test.js +++ b/test/access/AccessControlDefaultAdminRules.test.js @@ -16,10 +16,11 @@ contract('AccessControlDefaultAdminRules', function (accounts) { it('initial admin not zero', async function () { await expectRevert( AccessControlDefaultAdminRules.new(delay, constants.ZERO_ADDRESS), - 'AccessControl: 0 default admin', + 'AccessControlInvalidDefaultAdmin', + [constants.ZERO_ADDRESS], ); }); - shouldBehaveLikeAccessControl('AccessControl', ...accounts); - shouldBehaveLikeAccessControlDefaultAdminRules('AccessControl', delay, ...accounts); + shouldBehaveLikeAccessControl(...accounts); + shouldBehaveLikeAccessControlDefaultAdminRules(delay, ...accounts); }); diff --git a/test/access/AccessControlEnumerable.test.js b/test/access/AccessControlEnumerable.test.js index 2aa59f4c071..0e1879700d0 100644 --- a/test/access/AccessControlEnumerable.test.js +++ b/test/access/AccessControlEnumerable.test.js @@ -12,6 +12,6 @@ contract('AccessControl', function (accounts) { await this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, accounts[0]); }); - shouldBehaveLikeAccessControl('AccessControl', ...accounts); - shouldBehaveLikeAccessControlEnumerable('AccessControl', ...accounts); + shouldBehaveLikeAccessControl(...accounts); + shouldBehaveLikeAccessControlEnumerable(...accounts); }); diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index 07b8764a56b..079d694d730 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -1,4 +1,6 @@ -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent } = require('@openzeppelin/test-helpers'); +const { expectRevertCustomError } = require('../helpers/customError'); + const { ZERO_ADDRESS } = constants; const { expect } = require('chai'); @@ -25,13 +27,18 @@ contract('Ownable', function (accounts) { }); it('prevents non-owners from transferring', async function () { - await expectRevert(this.ownable.transferOwnership(other, { from: other }), 'Ownable: caller is not the owner'); + await expectRevertCustomError( + this.ownable.transferOwnership(other, { from: other }), + 'OwnableUnauthorizedAccount', + [other], + ); }); it('guards ownership against stuck state', async function () { - await expectRevert( + await expectRevertCustomError( this.ownable.transferOwnership(ZERO_ADDRESS, { from: owner }), - 'Ownable: new owner is the zero address', + 'OwnableInvalidOwner', + [ZERO_ADDRESS], ); }); }); @@ -45,7 +52,9 @@ contract('Ownable', function (accounts) { }); it('prevents non-owners from renouncement', async function () { - await expectRevert(this.ownable.renounceOwnership({ from: other }), 'Ownable: caller is not the owner'); + await expectRevertCustomError(this.ownable.renounceOwnership({ from: other }), 'OwnableUnauthorizedAccount', [ + other, + ]); }); it('allows to recover access using the internal _transferOwnership', async function () { diff --git a/test/access/Ownable2Step.test.js b/test/access/Ownable2Step.test.js index dfda6b7089e..bdbac48fa12 100644 --- a/test/access/Ownable2Step.test.js +++ b/test/access/Ownable2Step.test.js @@ -1,6 +1,7 @@ -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent } = require('@openzeppelin/test-helpers'); const { ZERO_ADDRESS } = constants; const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); const Ownable2Step = artifacts.require('$Ownable2Step'); @@ -29,14 +30,15 @@ contract('Ownable2Step', function (accounts) { it('guards transfer against invalid user', async function () { await this.ownable2Step.transferOwnership(accountA, { from: owner }); - await expectRevert( + await expectRevertCustomError( this.ownable2Step.acceptOwnership({ from: accountB }), - 'Ownable2Step: caller is not the new owner', + 'OwnableUnauthorizedAccount', + [accountB], ); }); }); - it('renouncing ownership', async function () { + describe('renouncing ownership', async function () { it('changes owner after renouncing ownership', async function () { await this.ownable2Step.renounceOwnership({ from: owner }); // If renounceOwnership is removed from parent an alternative is needed ... @@ -50,18 +52,19 @@ contract('Ownable2Step', function (accounts) { expect(await this.ownable2Step.pendingOwner()).to.equal(accountA); await this.ownable2Step.renounceOwnership({ from: owner }); expect(await this.ownable2Step.pendingOwner()).to.equal(ZERO_ADDRESS); - await expectRevert( + await expectRevertCustomError( this.ownable2Step.acceptOwnership({ from: accountA }), - 'Ownable2Step: caller is not the new owner', + 'OwnableUnauthorizedAccount', + [accountA], ); }); it('allows to recover access using the internal _transferOwnership', async function () { - await this.ownable.renounceOwnership({ from: owner }); - const receipt = await this.ownable.$_transferOwnership(accountA); + await this.ownable2Step.renounceOwnership({ from: owner }); + const receipt = await this.ownable2Step.$_transferOwnership(accountA); expectEvent(receipt, 'OwnershipTransferred'); - expect(await this.ownable.owner()).to.equal(accountA); + expect(await this.ownable2Step.owner()).to.equal(accountA); }); }); }); diff --git a/test/finance/VestingWallet.test.js b/test/finance/VestingWallet.test.js index 09205a99cd4..91ca04da06b 100644 --- a/test/finance/VestingWallet.test.js +++ b/test/finance/VestingWallet.test.js @@ -1,7 +1,8 @@ -const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, time } = require('@openzeppelin/test-helpers'); const { web3 } = require('@openzeppelin/test-helpers/src/setup'); const { expect } = require('chai'); const { BNmin } = require('../helpers/math'); +const { expectRevertCustomError } = require('../helpers/customError'); const VestingWallet = artifacts.require('VestingWallet'); const ERC20 = artifacts.require('$ERC20'); @@ -20,9 +21,10 @@ contract('VestingWallet', function (accounts) { }); it('rejects zero address for beneficiary', async function () { - await expectRevert( + await expectRevertCustomError( VestingWallet.new(constants.ZERO_ADDRESS, this.start, duration), - 'VestingWallet: beneficiary is zero address', + 'VestingWalletInvalidBeneficiary', + [constants.ZERO_ADDRESS], ); }); diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js index 96feaf3a7d9..909c386862d 100644 --- a/test/governance/Governor.test.js +++ b/test/governance/Governor.test.js @@ -6,11 +6,13 @@ const { fromRpcSig } = require('ethereumjs-util'); const Enums = require('../helpers/enums'); const { getDomain, domainType } = require('../helpers/eip712'); -const { GovernorHelper } = require('../helpers/governance'); +const { GovernorHelper, proposalStatesToBitMap } = require('../helpers/governance'); const { clockFromReceipt } = require('../helpers/time'); +const { expectRevertCustomError } = require('../helpers/customError'); const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); const { shouldBehaveLikeEIP6372 } = require('./utils/EIP6372.behavior'); +const { ZERO_BYTES32 } = require('@openzeppelin/test-helpers/src/constants'); const Governor = artifacts.require('$GovernorMock'); const CallReceiver = artifacts.require('CallReceiverMock'); @@ -237,32 +239,39 @@ contract('Governor', function (accounts) { describe('on propose', function () { it('if proposal already exists', async function () { await this.helper.propose(); - await expectRevert(this.helper.propose(), 'Governor: proposal already exists'); + await expectRevertCustomError(this.helper.propose(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Pending, + ZERO_BYTES32, + ]); }); }); describe('on vote', function () { it('if proposal does not exist', async function () { - await expectRevert( + await expectRevertCustomError( this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: unknown proposal id', + 'GovernorNonexistentProposal', + [this.proposal.id], ); }); it('if voting has not started', async function () { await this.helper.propose(); - await expectRevert( + await expectRevertCustomError( this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: vote not currently active', + 'GovernorUnexpectedProposalState', + [this.proposal.id, Enums.ProposalState.Pending, proposalStatesToBitMap([Enums.ProposalState.Active])], ); }); it('if support value is invalid', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await expectRevert( + await expectRevertCustomError( this.helper.vote({ support: web3.utils.toBN('255') }), - 'GovernorVotingSimple: invalid value for enum VoteType', + 'GovernorInvalidVoteType', + [], ); }); @@ -270,50 +279,64 @@ contract('Governor', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await expectRevert( + await expectRevertCustomError( this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'GovernorVotingSimple: vote already cast', + 'GovernorAlreadyCastVote', + [voter1], ); }); it('if voting is over', async function () { await this.helper.propose(); await this.helper.waitForDeadline(); - await expectRevert( + await expectRevertCustomError( this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: vote not currently active', + 'GovernorUnexpectedProposalState', + [this.proposal.id, Enums.ProposalState.Defeated, proposalStatesToBitMap([Enums.ProposalState.Active])], ); }); }); describe('on execute', function () { it('if proposal does not exist', async function () { - await expectRevert(this.helper.execute(), 'Governor: unknown proposal id'); + await expectRevertCustomError(this.helper.execute(), 'GovernorNonexistentProposal', [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 expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Active, + 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 expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Active, + 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 expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Active, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); }); it('if receiver revert without reason', async function () { - this.proposal = this.helper.setProposal( + this.helper.setProposal( [ { target: this.receiver.address, @@ -327,11 +350,11 @@ contract('Governor', function (accounts) { await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); await this.helper.waitForDeadline(); - await expectRevert(this.helper.execute(), 'Governor: call reverted without message'); + await expectRevertCustomError(this.helper.execute(), 'FailedInnerCall', []); }); it('if receiver revert with reason', async function () { - this.proposal = this.helper.setProposal( + this.helper.setProposal( [ { target: this.receiver.address, @@ -354,14 +377,20 @@ contract('Governor', function (accounts) { await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); await this.helper.waitForDeadline(); await this.helper.execute(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Executed, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); }); }); }); describe('state', function () { it('Unset', async function () { - await expectRevert(this.mock.state(this.proposal.id), 'Governor: unknown proposal id'); + await expectRevertCustomError(this.mock.state(this.proposal.id), 'GovernorNonexistentProposal', [ + this.proposal.id, + ]); }); it('Pending & Active', async function () { @@ -404,7 +433,9 @@ contract('Governor', function (accounts) { describe('cancel', function () { describe('internal', function () { it('before proposal', async function () { - await expectRevert(this.helper.cancel('internal'), 'Governor: unknown proposal id'); + await expectRevertCustomError(this.helper.cancel('internal'), 'GovernorNonexistentProposal', [ + this.proposal.id, + ]); }); it('after proposal', async function () { @@ -414,9 +445,10 @@ contract('Governor', function (accounts) { expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); await this.helper.waitForSnapshot(); - await expectRevert( + await expectRevertCustomError( this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: vote not currently active', + 'GovernorUnexpectedProposalState', + [this.proposal.id, Enums.ProposalState.Canceled, proposalStatesToBitMap([Enums.ProposalState.Active])], ); }); @@ -429,7 +461,11 @@ contract('Governor', function (accounts) { expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); await this.helper.waitForDeadline(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Canceled, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); }); it('after deadline', async function () { @@ -441,7 +477,11 @@ contract('Governor', function (accounts) { await this.helper.cancel('internal'); expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Canceled, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); }); it('after execution', async function () { @@ -451,13 +491,22 @@ contract('Governor', function (accounts) { await this.helper.waitForDeadline(); await this.helper.execute(); - await expectRevert(this.helper.cancel('internal'), 'Governor: proposal not active'); + 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 }, + ), + ]); }); }); describe('public', function () { it('before proposal', async function () { - await expectRevert(this.helper.cancel('external'), 'Governor: unknown proposal id'); + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorNonexistentProposal', [ + this.proposal.id, + ]); }); it('after proposal', async function () { @@ -469,14 +518,20 @@ contract('Governor', function (accounts) { it('after proposal - restricted to proposer', async function () { await this.helper.propose(); - await expectRevert(this.helper.cancel('external', { from: owner }), 'Governor: only proposer can cancel'); + await expectRevertCustomError(this.helper.cancel('external', { from: owner }), 'GovernorOnlyProposer', [ + owner, + ]); }); it('after vote started', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(1); // snapshot + 1 block - await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel'); + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Active, + proposalStatesToBitMap([Enums.ProposalState.Pending]), + ]); }); it('after vote', async function () { @@ -484,7 +539,11 @@ contract('Governor', function (accounts) { await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel'); + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Active, + proposalStatesToBitMap([Enums.ProposalState.Pending]), + ]); }); it('after deadline', async function () { @@ -493,7 +552,11 @@ contract('Governor', function (accounts) { await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); await this.helper.waitForDeadline(); - await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel'); + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Succeeded, + proposalStatesToBitMap([Enums.ProposalState.Pending]), + ]); }); it('after execution', async function () { @@ -503,7 +566,11 @@ contract('Governor', function (accounts) { await this.helper.waitForDeadline(); await this.helper.execute(); - await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel'); + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Executed, + proposalStatesToBitMap([Enums.ProposalState.Pending]), + ]); }); }); }); @@ -511,7 +578,7 @@ contract('Governor', function (accounts) { describe('proposal length', function () { it('empty', async function () { this.helper.setProposal([], ''); - await expectRevert(this.helper.propose(), 'Governor: empty proposal'); + await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [0, 0, 0]); }); it('mismatch #1', async function () { @@ -523,7 +590,7 @@ contract('Governor', function (accounts) { }, '', ); - await expectRevert(this.helper.propose(), 'Governor: invalid proposal length'); + await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [0, 1, 1]); }); it('mismatch #2', async function () { @@ -535,7 +602,7 @@ contract('Governor', function (accounts) { }, '', ); - await expectRevert(this.helper.propose(), 'Governor: invalid proposal length'); + await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [1, 1, 0]); }); it('mismatch #3', async function () { @@ -547,7 +614,7 @@ contract('Governor', function (accounts) { }, '', ); - await expectRevert(this.helper.propose(), 'Governor: invalid proposal length'); + await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [1, 0, 1]); }); }); @@ -636,15 +703,23 @@ contract('Governor', function (accounts) { describe('onlyGovernance updates', function () { it('setVotingDelay is protected', async function () { - await expectRevert(this.mock.setVotingDelay('0'), 'Governor: onlyGovernance'); + await expectRevertCustomError(this.mock.setVotingDelay('0', { from: owner }), 'GovernorOnlyExecutor', [ + owner, + ]); }); it('setVotingPeriod is protected', async function () { - await expectRevert(this.mock.setVotingPeriod('32'), 'Governor: onlyGovernance'); + await expectRevertCustomError(this.mock.setVotingPeriod('32', { from: owner }), 'GovernorOnlyExecutor', [ + owner, + ]); }); it('setProposalThreshold is protected', async function () { - await expectRevert(this.mock.setProposalThreshold('1000000000000000000'), 'Governor: onlyGovernance'); + await expectRevertCustomError( + this.mock.setProposalThreshold('1000000000000000000', { from: owner }), + 'GovernorOnlyExecutor', + [owner], + ); }); it('can setVotingDelay through governance', async function () { @@ -690,11 +765,12 @@ contract('Governor', function (accounts) { }); it('cannot setVotingPeriod to 0 through governance', async function () { + const votingPeriod = 0; this.helper.setProposal( [ { target: this.mock.address, - data: this.mock.contract.methods.setVotingPeriod('0').encodeABI(), + data: this.mock.contract.methods.setVotingPeriod(votingPeriod).encodeABI(), }, ], '', @@ -705,7 +781,7 @@ contract('Governor', function (accounts) { await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); await this.helper.waitForDeadline(); - await expectRevert(this.helper.execute(), 'GovernorSettings: voting period too low'); + await expectRevertCustomError(this.helper.execute(), 'GovernorInvalidVotingPeriod', [votingPeriod]); }); it('can setProposalThreshold to 0 through governance', async function () { diff --git a/test/governance/TimelockController.test.js b/test/governance/TimelockController.test.js index e9ddfaf47db..d8fcdce6ca7 100644 --- a/test/governance/TimelockController.test.js +++ b/test/governance/TimelockController.test.js @@ -4,6 +4,8 @@ const { ZERO_ADDRESS, ZERO_BYTES32 } = constants; const { expect } = require('chai'); 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'); @@ -182,7 +184,7 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); - await expectRevert( + await expectRevertCustomError( this.mock.schedule( this.operation.target, this.operation.value, @@ -192,12 +194,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: proposer }, ), - 'TimelockController: operation already scheduled', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Unset], ); }); it('prevent non-proposer from committing', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.schedule( this.operation.target, this.operation.value, @@ -207,12 +210,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: other }, ), - `AccessControl: account ${other.toLowerCase()} is missing role ${PROPOSER_ROLE}`, + `AccessControlUnauthorizedAccount`, + [other, PROPOSER_ROLE], ); }); it('enforce minimum delay', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.schedule( this.operation.target, this.operation.value, @@ -222,7 +226,8 @@ contract('TimelockController', function (accounts) { MINDELAY - 1, { from: proposer }, ), - 'TimelockController: insufficient delay', + 'TimelockInsufficientDelay', + [MINDELAY, MINDELAY - 1], ); }); @@ -252,7 +257,7 @@ contract('TimelockController', function (accounts) { }); it('revert if operation is not scheduled', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.execute( this.operation.target, this.operation.value, @@ -261,7 +266,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -279,7 +285,7 @@ contract('TimelockController', function (accounts) { }); it('revert if execution comes too early 1/2', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.execute( this.operation.target, this.operation.value, @@ -288,7 +294,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -296,7 +303,7 @@ contract('TimelockController', function (accounts) { const timestamp = await this.mock.getTimestamp(this.operation.id); await time.increaseTo(timestamp - 5); // -1 is too tight, test sometime fails - await expectRevert( + await expectRevertCustomError( this.mock.execute( this.operation.target, this.operation.value, @@ -305,7 +312,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -334,7 +342,7 @@ contract('TimelockController', function (accounts) { }); it('prevent non-executor from revealing', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.execute( this.operation.target, this.operation.value, @@ -343,7 +351,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: other }, ), - `AccessControl: account ${other.toLowerCase()} is missing role ${EXECUTOR_ROLE}`, + `AccessControlUnauthorizedAccount`, + [other, EXECUTOR_ROLE], ); }); @@ -389,7 +398,7 @@ contract('TimelockController', function (accounts) { await reentrant.enableRentrancy(this.mock.address, data); // Expect to fail - await expectRevert( + await expectRevertCustomError( this.mock.execute( reentrantOperation.target, reentrantOperation.value, @@ -398,7 +407,8 @@ contract('TimelockController', function (accounts) { reentrantOperation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [reentrantOperation.id, OperationState.Ready], ); // Disable reentrancy @@ -484,7 +494,7 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); - await expectRevert( + await expectRevertCustomError( this.mock.scheduleBatch( this.operation.targets, this.operation.values, @@ -494,12 +504,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: proposer }, ), - 'TimelockController: operation already scheduled', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Unset], ); }); it('length of batch parameter must match #1', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.scheduleBatch( this.operation.targets, [], @@ -509,12 +520,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: proposer }, ), - 'TimelockController: length mismatch', + 'TimelockInvalidOperationLength', + [this.operation.targets.length, this.operation.payloads.length, 0], ); }); it('length of batch parameter must match #1', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.scheduleBatch( this.operation.targets, this.operation.values, @@ -524,12 +536,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: proposer }, ), - 'TimelockController: length mismatch', + 'TimelockInvalidOperationLength', + [this.operation.targets.length, 0, this.operation.payloads.length], ); }); it('prevent non-proposer from committing', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.scheduleBatch( this.operation.targets, this.operation.values, @@ -539,12 +552,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: other }, ), - `AccessControl: account ${other.toLowerCase()} is missing role ${PROPOSER_ROLE}`, + `AccessControlUnauthorizedAccount`, + [other, PROPOSER_ROLE], ); }); it('enforce minimum delay', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.scheduleBatch( this.operation.targets, this.operation.values, @@ -554,7 +568,8 @@ contract('TimelockController', function (accounts) { MINDELAY - 1, { from: proposer }, ), - 'TimelockController: insufficient delay', + 'TimelockInsufficientDelay', + [MINDELAY, MINDELAY - 1], ); }); }); @@ -571,7 +586,7 @@ contract('TimelockController', function (accounts) { }); it('revert if operation is not scheduled', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, this.operation.values, @@ -580,7 +595,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -598,7 +614,7 @@ contract('TimelockController', function (accounts) { }); it('revert if execution comes too early 1/2', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, this.operation.values, @@ -607,7 +623,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -615,7 +632,7 @@ contract('TimelockController', function (accounts) { const timestamp = await this.mock.getTimestamp(this.operation.id); await time.increaseTo(timestamp - 5); // -1 is to tight, test sometime fails - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, this.operation.values, @@ -624,7 +641,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -655,7 +673,7 @@ contract('TimelockController', function (accounts) { }); it('prevent non-executor from revealing', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, this.operation.values, @@ -664,12 +682,13 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: other }, ), - `AccessControl: account ${other.toLowerCase()} is missing role ${EXECUTOR_ROLE}`, + `AccessControlUnauthorizedAccount`, + [other, EXECUTOR_ROLE], ); }); it('length mismatch #1', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( [], this.operation.values, @@ -678,12 +697,13 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: length mismatch', + 'TimelockInvalidOperationLength', + [0, this.operation.payloads.length, this.operation.values.length], ); }); it('length mismatch #2', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, [], @@ -692,12 +712,13 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: length mismatch', + 'TimelockInvalidOperationLength', + [this.operation.targets.length, this.operation.payloads.length, 0], ); }); it('length mismatch #3', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, this.operation.values, @@ -706,7 +727,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: length mismatch', + 'TimelockInvalidOperationLength', + [this.operation.targets.length, 0, this.operation.values.length], ); }); @@ -752,7 +774,7 @@ contract('TimelockController', function (accounts) { await reentrant.enableRentrancy(this.mock.address, data); // Expect to fail - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( reentrantBatchOperation.targets, reentrantBatchOperation.values, @@ -761,7 +783,8 @@ contract('TimelockController', function (accounts) { reentrantBatchOperation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [reentrantBatchOperation.id, OperationState.Ready], ); // Disable reentrancy @@ -796,7 +819,7 @@ contract('TimelockController', function (accounts) { [0, 0, 0], [ this.callreceivermock.contract.methods.mockFunction().encodeABI(), - this.callreceivermock.contract.methods.mockFunctionThrows().encodeABI(), + this.callreceivermock.contract.methods.mockFunctionRevertsNoReason().encodeABI(), this.callreceivermock.contract.methods.mockFunction().encodeABI(), ], ZERO_BYTES32, @@ -813,7 +836,7 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); await time.increase(MINDELAY); - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( operation.targets, operation.values, @@ -822,7 +845,8 @@ contract('TimelockController', function (accounts) { operation.salt, { from: executor }, ), - 'TimelockController: underlying transaction reverted', + 'FailedInnerCall', + [], ); }); }); @@ -854,16 +878,18 @@ contract('TimelockController', function (accounts) { }); it('cannot cancel invalid operation', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.cancel(constants.ZERO_BYTES32, { from: canceller }), - 'TimelockController: operation cannot be cancelled', + 'TimelockUnexpectedOperationState', + [constants.ZERO_BYTES32, OperationState.Pending], ); }); it('prevent non-canceller from canceling', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.cancel(this.operation.id, { from: other }), - `AccessControl: account ${other.toLowerCase()} is missing role ${CANCELLER_ROLE}`, + `AccessControlUnauthorizedAccount`, + [other, CANCELLER_ROLE], ); }); }); @@ -871,7 +897,7 @@ contract('TimelockController', function (accounts) { describe('maintenance', function () { it('prevent unauthorized maintenance', async function () { - await expectRevert(this.mock.updateDelay(0, { from: other }), 'TimelockController: caller must be timelock'); + await expectRevertCustomError(this.mock.updateDelay(0, { from: other }), 'TimelockUnauthorizedCaller', [other]); }); it('timelock scheduled maintenance', async function () { @@ -946,7 +972,7 @@ contract('TimelockController', function (accounts) { }); it('cannot execute before dependency', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.execute( this.operation2.target, this.operation2.value, @@ -955,7 +981,8 @@ contract('TimelockController', function (accounts) { this.operation2.salt, { from: executor }, ), - 'TimelockController: missing dependency', + 'TimelockUnexecutedPredecessor', + [this.operation1.id], ); }); @@ -1032,11 +1059,12 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); await time.increase(MINDELAY); - await expectRevert( + await expectRevertCustomError( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, }), - 'TimelockController: underlying transaction reverted', + 'FailedInnerCall', + [], ); }); @@ -1059,11 +1087,11 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); await time.increase(MINDELAY); - await expectRevert( + // 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, }), - 'TimelockController: underlying transaction reverted', ); }); @@ -1086,12 +1114,13 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); await time.increase(MINDELAY); - await expectRevert( + await expectRevertCustomError( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, - gas: '70000', + gas: '100000', }), - 'TimelockController: underlying transaction reverted', + 'FailedInnerCall', + [], ); }); @@ -1154,11 +1183,12 @@ contract('TimelockController', function (accounts) { 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 expectRevert( + await expectRevertCustomError( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, }), - 'TimelockController: underlying transaction reverted', + 'FailedInnerCall', + [], ); expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); @@ -1188,11 +1218,12 @@ contract('TimelockController', function (accounts) { 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 expectRevert( + await expectRevertCustomError( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, }), - 'TimelockController: underlying transaction reverted', + 'FailedInnerCall', + [], ); expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); diff --git a/test/governance/compatibility/GovernorCompatibilityBravo.test.js b/test/governance/compatibility/GovernorCompatibilityBravo.test.js index 9c45277d18f..4182dfb4eb2 100644 --- a/test/governance/compatibility/GovernorCompatibilityBravo.test.js +++ b/test/governance/compatibility/GovernorCompatibilityBravo.test.js @@ -1,9 +1,10 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const RLP = require('rlp'); const Enums = require('../../helpers/enums'); const { GovernorHelper } = require('../../helpers/governance'); const { clockFromReceipt } = require('../../helpers/time'); +const { expectRevertCustomError } = require('../../helpers/customError'); const Timelock = artifacts.require('CompTimelock'); const Governor = artifacts.require('$GovernorCompatibilityBravoMock'); @@ -38,6 +39,16 @@ contract('GovernorCompatibilityBravo', function (accounts) { const proposalThreshold = web3.utils.toWei('10'); const value = web3.utils.toWei('1'); + const votes = { + [owner]: tokenSupply, + [proposer]: proposalThreshold, + [voter1]: web3.utils.toWei('10'), + [voter2]: web3.utils.toWei('7'), + [voter3]: web3.utils.toWei('5'), + [voter4]: web3.utils.toWei('2'), + [other]: 0, + }; + for (const { mode, Token } of TOKENS) { describe(`using ${Token._json.contractName}`, function () { beforeEach(async function () { @@ -65,11 +76,11 @@ contract('GovernorCompatibilityBravo', function (accounts) { 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: proposer, value: proposalThreshold }, { from: owner }); - 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 }); + await this.helper.delegate({ token: this.token, to: proposer, value: votes[proposer] }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter1, value: votes[voter1] }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: votes[voter2] }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: votes[voter3] }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: votes[voter4] }, { from: owner }); // default proposal this.proposal = this.helper.setProposal( @@ -182,9 +193,10 @@ contract('GovernorCompatibilityBravo', function (accounts) { await this.helper.propose({ from: proposer }); await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await expectRevert( + await expectRevertCustomError( this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'GovernorCompatibilityBravo: vote already cast', + 'GovernorAlreadyCastVote', + [voter1], ); }); @@ -226,36 +238,43 @@ contract('GovernorCompatibilityBravo', function (accounts) { it('with inconsistent array size for selector and arguments', async function () { const target = this.receiver.address; + const signatures = ['mockFunction()']; // One signature + const data = ['0x', this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI()]; // Two data entries this.helper.setProposal( { targets: [target, target], values: [0, 0], - signatures: ['mockFunction()'], // One signature - data: ['0x', this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI()], // Two data entries + signatures, + data, }, '', ); - await expectRevert(this.helper.propose({ from: proposer }), 'GovernorBravo: invalid signatures length'); + await expectRevertCustomError(this.helper.propose({ from: proposer }), 'GovernorInvalidSignaturesLength', [ + signatures.length, + data.length, + ]); }); describe('should revert', function () { describe('on propose', function () { it('if proposal does not meet proposalThreshold', async function () { - await expectRevert( - this.helper.propose({ from: other }), - 'Governor: proposer votes below proposal threshold', - ); + await expectRevertCustomError(this.helper.propose({ from: other }), 'GovernorInsufficientProposerVotes', [ + other, + votes[other], + proposalThreshold, + ]); }); }); describe('on vote', function () { - it('if vote type is invalide', async function () { + it('if vote type is invalid', async function () { await this.helper.propose({ from: proposer }); await this.helper.waitForSnapshot(); - await expectRevert( + await expectRevertCustomError( this.helper.vote({ support: 5 }, { from: voter1 }), - 'GovernorCompatibilityBravo: invalid vote type', + 'GovernorInvalidVoteType', + [], ); }); }); @@ -275,7 +294,11 @@ contract('GovernorCompatibilityBravo', function (accounts) { it('cannot cancel is proposer is still above threshold', async function () { await this.helper.propose({ from: proposer }); - await expectRevert(this.helper.cancel('external'), 'GovernorBravo: proposer above threshold'); + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorInsufficientProposerVotes', [ + proposer, + votes[proposer], + proposalThreshold, + ]); }); }); }); diff --git a/test/governance/extensions/GovernorPreventLateQuorum.test.js b/test/governance/extensions/GovernorPreventLateQuorum.test.js index 4df5adb1c42..17ae05a73fb 100644 --- a/test/governance/extensions/GovernorPreventLateQuorum.test.js +++ b/test/governance/extensions/GovernorPreventLateQuorum.test.js @@ -1,9 +1,10 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); 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'); @@ -158,7 +159,11 @@ contract('GovernorPreventLateQuorum', function (accounts) { describe('onlyGovernance updates', function () { it('setLateQuorumVoteExtension is protected', async function () { - await expectRevert(this.mock.setLateQuorumVoteExtension(0), 'Governor: onlyGovernance'); + await expectRevertCustomError( + this.mock.setLateQuorumVoteExtension(0, { from: owner }), + 'GovernorOnlyExecutor', + [owner], + ); }); it('can setLateQuorumVoteExtension through governance', async function () { diff --git a/test/governance/extensions/GovernorTimelockCompound.test.js b/test/governance/extensions/GovernorTimelockCompound.test.js index 2cbce26000b..c406baf8dde 100644 --- a/test/governance/extensions/GovernorTimelockCompound.test.js +++ b/test/governance/extensions/GovernorTimelockCompound.test.js @@ -3,13 +3,16 @@ const { expect } = require('chai'); const RLP = require('rlp'); const Enums = require('../../helpers/enums'); -const { GovernorHelper } = require('../../helpers/governance'); +const { GovernorHelper, proposalStatesToBitMap } = require('../../helpers/governance'); +const { expectRevertCustomError } = require('../../helpers/customError'); const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); 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'); function makeContractAddress(creator, nonce) { return web3.utils.toChecksumAddress( @@ -129,7 +132,11 @@ contract('GovernorTimelockCompound', function (accounts) { await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); await this.helper.waitForDeadline(); await this.helper.queue(); - await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Queued, + proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ]); }); it('if proposal contains duplicate calls', async function () { @@ -137,17 +144,14 @@ contract('GovernorTimelockCompound', function (accounts) { target: this.token.address, data: this.token.contract.methods.approve(this.receiver.address, constants.MAX_UINT256).encodeABI(), }; - this.helper.setProposal([action, action], ''); + 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.waitForDeadline(); - await expectRevert( - this.helper.queue(), - 'GovernorTimelockCompound: identical proposal action already queued', - ); - await expectRevert(this.helper.execute(), 'GovernorTimelockCompound: proposal not yet queued'); + await expectRevertCustomError(this.helper.queue(), 'GovernorAlreadyQueuedProposal', [id]); + await expectRevertCustomError(this.helper.execute(), 'GovernorNotQueuedProposal', [id]); }); }); @@ -160,7 +164,7 @@ contract('GovernorTimelockCompound', function (accounts) { expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); - await expectRevert(this.helper.execute(), 'GovernorTimelockCompound: proposal not yet queued'); + await expectRevertCustomError(this.helper.execute(), 'GovernorNotQueuedProposal', [this.proposal.id]); }); it('if too early', async function () { @@ -188,7 +192,11 @@ contract('GovernorTimelockCompound', function (accounts) { expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Expired); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Expired, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); }); it('if already executed', async function () { @@ -199,7 +207,75 @@ contract('GovernorTimelockCompound', function (accounts) { await this.helper.queue(); await this.helper.waitForEta(); await this.helper.execute(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Executed, + 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); + + beforeEach(async function () { + this.token = await ERC721.new(name, symbol); + await this.token.$_mint(owner, tokenId); + }); + + it("can't receive an ERC721 safeTransfer", async function () { + await expectRevertCustomError( + this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner }), + '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), + }; + + beforeEach(async function () { + this.token = await ERC1155.new(uri); + await this.token.$_mintBatch(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, + ...Object.entries(tokenIds)[0], // id + amount + '0x', + { from: owner }, + ), + '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', + [], + ); + }); }); }); }); @@ -214,7 +290,11 @@ contract('GovernorTimelockCompound', function (accounts) { expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Canceled, + proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ]); }); it('cancel after queue prevents executing', async function () { @@ -227,7 +307,11 @@ contract('GovernorTimelockCompound', function (accounts) { expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Canceled, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); }); }); @@ -238,9 +322,12 @@ contract('GovernorTimelockCompound', function (accounts) { }); it('is protected', async function () { - await expectRevert( - this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()), - 'Governor: onlyGovernance', + await expectRevertCustomError( + this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI(), { + from: owner, + }), + 'GovernorOnlyExecutor', + [owner], ); }); @@ -285,7 +372,11 @@ contract('GovernorTimelockCompound', function (accounts) { }); it('is protected', async function () { - await expectRevert(this.mock.updateTimelock(this.newTimelock.address), 'Governor: onlyGovernance'); + await expectRevertCustomError( + this.mock.updateTimelock(this.newTimelock.address, { from: owner }), + 'GovernorOnlyExecutor', + [owner], + ); }); it('can be executed through governance to', async function () { diff --git a/test/governance/extensions/GovernorTimelockControl.test.js b/test/governance/extensions/GovernorTimelockControl.test.js index af57ba90b58..3265dfa6898 100644 --- a/test/governance/extensions/GovernorTimelockControl.test.js +++ b/test/governance/extensions/GovernorTimelockControl.test.js @@ -2,13 +2,16 @@ const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/te const { expect } = require('chai'); const Enums = require('../../helpers/enums'); -const { GovernorHelper } = require('../../helpers/governance'); +const { GovernorHelper, proposalStatesToBitMap } = require('../../helpers/governance'); +const { expectRevertCustomError } = require('../../helpers/customError'); const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); 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 TOKENS = [ { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, @@ -135,297 +138,204 @@ 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.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.waitForDeadline(); - const txQueue = await this.helper.queue(); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); + await this.helper.queue(); + await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Queued, + proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ]); + }); + }); - expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { - id: this.proposal.timelockid, - }); + describe('on execute', function () { + 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); - 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'); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + + await expectRevertCustomError(this.helper.execute(), 'TimelockUnexpectedOperationState', [ + this.proposal.timelockid, + Enums.OperationState.Ready, + ]); }); - describe('should revert', function () { - describe('on queue', function () { - 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.waitForDeadline(); - await this.helper.queue(); - await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); - }); - }); + 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.waitForDeadline(); + await this.helper.queue(); - describe('on execute', function () { - 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); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); - - await expectRevert(this.helper.execute(), 'TimelockController: operation is not 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.waitForDeadline(); - await this.helper.queue(); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); - - await expectRevert(this.helper.execute(), 'TimelockController: operation is not 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.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - await this.helper.execute(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - - 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.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - - await this.timelock.executeBatch( - ...this.proposal.shortProposal.slice(0, 3), - '0x0', - this.proposal.shortProposal[3], - ); - - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - }); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + + await expectRevertCustomError(this.helper.execute(), 'TimelockUnexpectedOperationState', [ + this.proposal.timelockid, + Enums.OperationState.Ready, + ]); }); - describe('cancel', function () { - 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.waitForDeadline(); + 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.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]), + ]); + }); - expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + 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.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); - }); + await this.timelock.executeBatch( + ...this.proposal.shortProposal.slice(0, 3), + '0x0', + this.proposal.shortProposal[3], + ); - 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.waitForDeadline(); - await this.helper.queue(); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Executed, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); + }); + }); + }); - expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + describe('cancel', function () { + 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.waitForDeadline(); + + expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: 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.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); + 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.waitForDeadline(); + await this.helper.queue(); + + expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: 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]), + ]); + }); - 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.waitForDeadline(); - await this.helper.queue(); + 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.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.be.bignumber.equal(Enums.ProposalState.Queued); - expectEvent(await this.timelock.cancel(this.proposal.timelockid, { from: owner }), 'Cancelled', { - id: this.proposal.timelockid, - }); + expectEvent(await this.timelock.cancel(this.proposal.timelockid, { from: owner }), 'Cancelled', { + id: 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.be.bignumber.equal(Enums.ProposalState.Canceled); + }); + }); + + describe('onlyGovernance', function () { + describe('relay', function () { + beforeEach(async function () { + await this.token.$_mint(this.mock.address, 1); }); - describe('onlyGovernance', function () { - describe('relay', function () { - beforeEach(async function () { - await this.token.$_mint(this.mock.address, 1); - }); - - it('is protected', async function () { - await expectRevert( - this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()), - 'Governor: onlyGovernance', - ); - }); - - 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(), - }, - ], - '', - ); - - 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.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 expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { - from: this.mock.address, - to: other, - value: '1', - }); - }); - - 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) - - this.helper.setProposal( - [ - { - target: this.mock.address, - value: t2g, - data: this.mock.contract.methods.relay(other, g2o, '0x').encodeABI(), - }, - ], - '', - ); - - 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.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)); - }); - - it('protected against other proposers', async function () { - await this.timelock.schedule( - this.mock.address, - web3.utils.toWei('0'), - this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(), - constants.ZERO_BYTES32, - constants.ZERO_BYTES32, - 3600, - { from: owner }, - ); - - await time.increase(3600); - - await expectRevert( - this.timelock.execute( - this.mock.address, - web3.utils.toWei('0'), - this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(), - constants.ZERO_BYTES32, - constants.ZERO_BYTES32, - { from: owner }, - ), - 'TimelockController: underlying transaction reverted', - ); - }); - }); + 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], + ); + }); + + 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(), + }, + ], + '', + ); + + 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.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); - describe('updateTimelock', function () { - beforeEach(async function () { - this.newTimelock = await Timelock.new( - 3600, - [this.mock.address], - [this.mock.address], - constants.ZERO_ADDRESS, - ); - }); - - it('is protected', async function () { - await expectRevert(this.mock.updateTimelock(this.newTimelock.address), 'Governor: onlyGovernance'); - }); - - 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(), - }, - ], - '', - ); - - 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.queue(); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - expectEvent(txExecute, 'TimelockChange', { - oldTimelock: this.timelock.address, - newTimelock: this.newTimelock.address, - }); - - expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); - }); + await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { + from: this.mock.address, + to: other, + value: '1', }); }); - it('clear queue of pending governor calls', async function () { + 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) + this.helper.setProposal( [ { target: this.mock.address, - data: this.mock.contract.methods.nonGovernanceFunction().encodeABI(), + value: t2g, + data: this.mock.contract.methods.relay(other, g2o, '0x').encodeABI(), }, ], '', ); + 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 }); @@ -434,11 +344,164 @@ contract('GovernorTimelockControl', function (accounts) { await this.helper.waitForEta(); await this.helper.execute(); - // This path clears _governanceCall as part of the afterExecute call, - // but we have not way to check that the cleanup actually happened other - // then coverage reports. + 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)); + }); + + 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; + const delay = 3600; + + await this.timelock.schedule(target, value, data, predecessor, salt, delay, { from: owner }); + + await time.increase(3600); + + await expectRevertCustomError( + this.timelock.execute(target, value, data, predecessor, salt, { from: owner }), + 'QueueEmpty', // Bubbled up from Governor + [], + ); }); }); + + describe('updateTimelock', function () { + beforeEach(async function () { + this.newTimelock = await Timelock.new( + 3600, + [this.mock.address], + [this.mock.address], + constants.ZERO_ADDRESS, + ); + }); + + it('is protected', async function () { + await expectRevertCustomError( + this.mock.updateTimelock(this.newTimelock.address, { from: owner }), + 'GovernorOnlyExecutor', + [owner], + ); + }); + + 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(), + }, + ], + '', + ); + + 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.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + expectEvent(txExecute, 'TimelockChange', { + oldTimelock: this.timelock.address, + newTimelock: this.newTimelock.address, + }); + + expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); + }); + }); + + describe('on safe receive', function () { + describe('ERC721', function () { + const name = 'Non Fungible Token'; + const symbol = 'NFT'; + const tokenId = web3.utils.toBN(1); + + beforeEach(async function () { + this.token = await ERC721.new(name, symbol); + await this.token.$_mint(owner, tokenId); + }); + + it("can't receive an ERC721 safeTransfer", async function () { + await expectRevertCustomError( + this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner }), + '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), + }; + + beforeEach(async function () { + this.token = await ERC1155.new(uri); + await this.token.$_mintBatch(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, + ...Object.entries(tokenIds)[0], // id + amount + '0x', + { from: owner }, + ), + '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', + [], + ); + }); + }); + }); + }); + + it('clear queue of pending governor calls', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.nonGovernanceFunction().encodeABI(), + }, + ], + '', + ); + + 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.queue(); + await this.helper.waitForEta(); + await this.helper.execute(); + + // This path clears _governanceCall as part of the afterExecute call, + // but we have not way to check that the cleanup actually happened other + // then coverage reports. }); }); } diff --git a/test/governance/extensions/GovernorVotesQuorumFraction.test.js b/test/governance/extensions/GovernorVotesQuorumFraction.test.js index a69338ad8c9..ece9c78d6f1 100644 --- a/test/governance/extensions/GovernorVotesQuorumFraction.test.js +++ b/test/governance/extensions/GovernorVotesQuorumFraction.test.js @@ -1,9 +1,10 @@ -const { expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expectEvent, time } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const Enums = require('../../helpers/enums'); -const { GovernorHelper } = require('../../helpers/governance'); +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'); @@ -84,12 +85,20 @@ contract('GovernorVotesQuorumFraction', function (accounts) { await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); await this.helper.waitForDeadline(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Defeated, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); }); describe('onlyGovernance updates', function () { it('updateQuorumNumerator is protected', async function () { - await expectRevert(this.mock.updateQuorumNumerator(newRatio), 'Governor: onlyGovernance'); + await expectRevertCustomError( + this.mock.updateQuorumNumerator(newRatio, { from: owner }), + 'GovernorOnlyExecutor', + [owner], + ); }); it('can updateQuorumNumerator through governance', async function () { @@ -129,11 +138,12 @@ contract('GovernorVotesQuorumFraction', function (accounts) { }); it('cannot updateQuorumNumerator over the maximum', async function () { + const quorumNumerator = 101; this.helper.setProposal( [ { target: this.mock.address, - data: this.mock.contract.methods.updateQuorumNumerator('101').encodeABI(), + data: this.mock.contract.methods.updateQuorumNumerator(quorumNumerator).encodeABI(), }, ], '', @@ -144,10 +154,12 @@ contract('GovernorVotesQuorumFraction', function (accounts) { await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); await this.helper.waitForDeadline(); - await expectRevert( - this.helper.execute(), - 'GovernorVotesQuorumFraction: quorumNumerator over quorumDenominator', - ); + const quorumDenominator = await this.mock.quorumDenominator(); + + await expectRevertCustomError(this.helper.execute(), 'GovernorInvalidQuorumFraction', [ + quorumNumerator, + quorumDenominator, + ]); }); }); }); diff --git a/test/governance/utils/Votes.behavior.js b/test/governance/utils/Votes.behavior.js index 37062e19c6d..20ebdba4fbb 100644 --- a/test/governance/utils/Votes.behavior.js +++ b/test/governance/utils/Votes.behavior.js @@ -1,4 +1,4 @@ -const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, time } = require('@openzeppelin/test-helpers'); const { MAX_UINT256, ZERO_ADDRESS } = constants; @@ -9,6 +9,7 @@ const Wallet = require('ethereumjs-wallet').default; const { shouldBehaveLikeEIP6372 } = require('./EIP6372.behavior'); const { getDomain, domainType } = require('../../helpers/eip712'); const { clockFromReceipt } = require('../../helpers/time'); +const { expectRevertCustomError } = require('../../helpers/customError'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -176,7 +177,11 @@ function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungibl await this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s); - await expectRevert(this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s), 'Votes: invalid nonce'); + await expectRevertCustomError( + this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s), + 'InvalidAccountNonce', + [delegator.address, nonce + 1], + ); }); it('rejects bad delegatee', async function () { @@ -208,9 +213,10 @@ function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungibl delegator.getPrivateKey(), ); - await expectRevert( + await expectRevertCustomError( this.votes.delegateBySig(delegatee, nonce + 1, MAX_UINT256, v, r, s), - 'Votes: invalid nonce', + 'InvalidAccountNonce', + [delegator.address, 0], ); }); @@ -226,7 +232,11 @@ function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungibl delegator.getPrivateKey(), ); - await expectRevert(this.votes.delegateBySig(delegatee, nonce, expiry, v, r, s), 'Votes: signature expired'); + await expectRevertCustomError( + this.votes.delegateBySig(delegatee, nonce, expiry, v, r, s), + 'VotesExpiredSignature', + [expiry], + ); }); }); }); @@ -237,7 +247,12 @@ function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungibl }); it('reverts if block number >= current block', async function () { - await expectRevert(this.votes.getPastTotalSupply(5e10), 'future lookup'); + const timepoint = 5e10; + const clock = await this.votes.clock(); + await expectRevertCustomError(this.votes.getPastTotalSupply(timepoint), 'ERC5805FutureLookup', [ + timepoint, + clock, + ]); }); it('returns 0 if there are no checkpoints', async function () { @@ -285,7 +300,10 @@ function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungibl 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 expectRevert(this.votes.getPastTotalSupply(t5.timepoint + 1), 'Votes: future lookup'); + await expectRevertCustomError(this.votes.getPastTotalSupply(t5.timepoint + 1), 'ERC5805FutureLookup', [ + t5.timepoint + 1, // timepoint + t5.timepoint + 1, // clock + ]); }); }); @@ -300,7 +318,12 @@ function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungibl describe('getPastVotes', function () { it('reverts if block number >= current block', async function () { - await expectRevert(this.votes.getPastVotes(accounts[2], 5e10), 'future lookup'); + const clock = await this.votes.clock(); + const timepoint = 5e10; // far in the future + await expectRevertCustomError(this.votes.getPastVotes(accounts[2], timepoint), 'ERC5805FutureLookup', [ + timepoint, + clock, + ]); }); it('returns 0 if there are no checkpoints', async function () { diff --git a/test/governance/utils/Votes.test.js b/test/governance/utils/Votes.test.js index 184ce0cec3f..b2b80f9fe18 100644 --- a/test/governance/utils/Votes.test.js +++ b/test/governance/utils/Votes.test.js @@ -1,7 +1,8 @@ -const { constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { clockFromReceipt } = require('../../helpers/time'); const { BNsum } = require('../../helpers/math'); +const { expectRevertCustomError } = require('../../helpers/customError'); require('array.prototype.at/auto'); @@ -45,7 +46,11 @@ contract('Votes', function (accounts) { it('reverts if block number >= current block', async function () { const lastTxTimepoint = await clockFromReceipt[mode](this.txs.at(-1).receipt); - await expectRevert(this.votes.getPastTotalSupply(lastTxTimepoint + 1), 'Votes: future lookup'); + const clock = await this.votes.clock(); + await expectRevertCustomError(this.votes.getPastTotalSupply(lastTxTimepoint + 1), 'ERC5805FutureLookup', [ + lastTxTimepoint + 1, + clock, + ]); }); it('delegates', async function () { diff --git a/test/helpers/customError.js b/test/helpers/customError.js index 3cfcd7277ea..e38170b78af 100644 --- a/test/helpers/customError.js +++ b/test/helpers/customError.js @@ -1,22 +1,44 @@ -const { config } = require('hardhat'); - -const optimizationsEnabled = config.solidity.compilers.some(c => c.settings.optimizer.enabled); +const { expect } = require('chai'); /** Revert handler that supports custom errors. */ -async function expectRevertCustomError(promise, reason) { - try { - await promise; - expect.fail("Expected promise to throw but it didn't"); - } catch (revert) { - if (reason) { - if (optimizationsEnabled) { - // Optimizations currently mess with Hardhat's decoding of custom errors - expect(revert.message).to.include.oneOf([reason, 'unrecognized return data or custom error']); - } else { - expect(revert.message).to.include(reason); - } - } +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)' + + // We trim out anything inside the single quotes as comma-separated values + const [, error] = message.match(/'(.*)'/); + + // Attempt to parse as an error + const match = error.match(/(?\w+)\((?.*)\)/); + if (!match) { + expect.fail(`Couldn't parse "${error}" as a custom error`); + } + // 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 = { diff --git a/test/helpers/enums.js b/test/helpers/enums.js index cc650abf41d..d4a4fdbd0b7 100644 --- a/test/helpers/enums.js +++ b/test/helpers/enums.js @@ -7,4 +7,5 @@ module.exports = { ProposalState: Enum('Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'), VoteType: Enum('Against', 'For', 'Abstain'), Rounding: Enum('Down', 'Up', 'Zero'), + OperationState: Enum('Unset', 'Pending', 'Ready', 'Done'), }; diff --git a/test/helpers/governance.js b/test/helpers/governance.js index 4b38b7588e4..665c21605a3 100644 --- a/test/helpers/governance.js +++ b/test/helpers/governance.js @@ -1,4 +1,5 @@ const { forward } = require('../helpers/time'); +const { ProposalState } = require('./enums'); function zip(...args) { return Array(Math.max(...args.map(array => array.length))) @@ -196,6 +197,44 @@ class GovernorHelper { } } +/** + * 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; + } + } + + if (options.inverted) { + const mask = 2 ** statesCount - 1; + result = result ^ mask; + } + + const hex = web3.utils.numberToHex(result); + return web3.utils.padLeft(hex, 64); +} + module.exports = { GovernorHelper, + proposalStatesToBitMap, }; diff --git a/test/metatx/ERC2771Context.test.js b/test/metatx/ERC2771Context.test.js index 6c298d3d9f0..3dd4b41532d 100644 --- a/test/metatx/ERC2771Context.test.js +++ b/test/metatx/ERC2771Context.test.js @@ -6,14 +6,16 @@ const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const ERC2771ContextMock = artifacts.require('ERC2771ContextMock'); -const MinimalForwarder = artifacts.require('MinimalForwarder'); +const ERC2771Forwarder = artifacts.require('ERC2771Forwarder'); const ContextMockCaller = artifacts.require('ContextMockCaller'); const { shouldBehaveLikeRegularContext } = require('../utils/Context.behavior'); contract('ERC2771Context', function (accounts) { + const MAX_UINT48 = web3.utils.toBN(1).shln(48).subn(1).toString(); + beforeEach(async function () { - this.forwarder = await MinimalForwarder.new(); + this.forwarder = await ERC2771Forwarder.new('ERC2771Forwarder'); this.recipient = await ERC2771ContextMock.new(this.forwarder.address); this.domain = await getDomain(this.forwarder); @@ -25,6 +27,7 @@ contract('ERC2771Context', function (accounts) { { name: 'value', type: 'uint256' }, { name: 'gas', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint48' }, { name: 'data', type: 'bytes' }, ], }; @@ -63,14 +66,17 @@ contract('ERC2771Context', function (accounts) { to: this.recipient.address, value: '0', gas: '100000', - nonce: (await this.forwarder.getNonce(this.sender)).toString(), + nonce: (await this.forwarder.nonces(this.sender)).toString(), + deadline: MAX_UINT48, data, }; - const sign = ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), { data: { ...this.data, message: req } }); - expect(await this.forwarder.verify(req, sign)).to.equal(true); + req.signature = ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), { + data: { ...this.data, message: req }, + }); + expect(await this.forwarder.verify(req)).to.equal(true); - const { tx } = await this.forwarder.execute(req, sign); + const { tx } = await this.forwarder.execute(req); await expectEvent.inTransaction(tx, ERC2771ContextMock, 'Sender', { sender: this.sender }); }); }); @@ -86,14 +92,17 @@ contract('ERC2771Context', function (accounts) { to: this.recipient.address, value: '0', gas: '100000', - nonce: (await this.forwarder.getNonce(this.sender)).toString(), + nonce: (await this.forwarder.nonces(this.sender)).toString(), + deadline: MAX_UINT48, data, }; - const sign = ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), { data: { ...this.data, message: req } }); - expect(await this.forwarder.verify(req, sign)).to.equal(true); + req.signature = ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), { + data: { ...this.data, message: req }, + }); + expect(await this.forwarder.verify(req)).to.equal(true); - const { tx } = await this.forwarder.execute(req, sign); + const { tx } = await this.forwarder.execute(req); await expectEvent.inTransaction(tx, ERC2771ContextMock, 'Data', { data, integerValue, stringValue }); }); }); diff --git a/test/metatx/ERC2771Forwarder.test.js b/test/metatx/ERC2771Forwarder.test.js new file mode 100644 index 00000000000..fa84ccdc301 --- /dev/null +++ b/test/metatx/ERC2771Forwarder.test.js @@ -0,0 +1,433 @@ +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 { expect } = require('chai'); + +const ERC2771Forwarder = artifacts.require('ERC2771Forwarder'); +const CallReceiverMock = artifacts.require('CallReceiverMock'); + +contract('ERC2771Forwarder', function (accounts) { + const [, refundReceiver, another] = accounts; + + const tamperedValues = { + from: another, + to: another, + value: web3.utils.toWei('0.5'), + data: '0x1742', + deadline: 0xdeadbeef, + }; + + 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.request = { + from: this.alice.address, + to: constants.ZERO_ADDRESS, + value: '0', + gas: '100000', + data: '0x', + deadline: this.timestamp.toNumber() + 60, // 1 minute + }; + this.requestData = { + ...this.request, + nonce: (await this.forwarder.nonces(this.alice.address)).toString(), + }; + + this.forgeData = request => ({ + types: this.types, + domain: this.domain, + primaryType: 'ForwardRequest', + message: { ...this.requestData, ...request }, + }); + this.sign = (privateKey, request) => + ethSigUtil.signTypedMessage(privateKey, { + data: this.forgeData(request), + }); + + this.requestData.signature = this.sign(this.alice.getPrivateKey()); + }); + + context('verify', function () { + context('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), + ); + }); + }); + + context('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); + }); + } + + 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); + }); + + 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); + }); + }); + }); + + 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), + ); + }); + + it('emits an event and consumes nonce for a successful request', async function () { + const receipt = await this.forwarder.execute(this.requestData); + expectEvent(receipt, '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), + ); + }); + + it('reverts with an unsuccessful request', async function () { + const receiver = await CallReceiverMock.new(); + const req = { + ...this.requestData, + to: receiver.address, + data: receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(), + }; + req.signature = this.sign(this.alice.getPrivateKey(), req); + await expectRevertCustomError(this.forwarder.execute(req), 'FailedInnerCall', []); + }); + }); + + context('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], + ); + }); + } + + 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, + ]); + }); + + 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, + ]); + }); + + 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]); + }); + }); + + it('bubbles out of gas', async function () { + const receiver = await CallReceiverMock.new(); + const gasAvailable = 100000; + this.requestData.to = receiver.address; + this.requestData.data = receiver.contract.methods.mockFunctionOutOfGas().encodeABI(); + this.requestData.gas = 1000000; + + this.requestData.signature = this.sign(this.alice.getPrivateKey()); + + await expectRevert.assertion(this.forwarder.execute(this.requestData, { gas: gasAvailable })); + + const { transactions } = await web3.eth.getBlock('latest'); + const { gasUsed } = await web3.eth.getTransactionReceipt(transactions[0]); + + expect(gasUsed).to.be.equal(gasAvailable); + }); + }); + + context('executeBatch', function () { + const batchValue = requestDatas => requestDatas.reduce((value, request) => value + Number(request.value), 0); + + 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); + }); + + context('with valid requests', function () { + beforeEach(async function () { + for (const request of this.requestDatas) { + 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) { + expectEvent(this.receipt, 'ExecutedForwardRequest', { + signer: request.from, + nonce: web3.utils.toBN(request.nonce), + success: 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)); + } + }); + }); + + context('with tampered requests', function () { + beforeEach(async function () { + this.idx = 1; // Tampered idx + }); + + 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], + ); + }); + + context('when the refund receiver is the zero address', function () { + beforeEach(function () { + this.refundReceiver = constants.ZERO_ADDRESS; + }); + + 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, + ], + ); + }); + } + + 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 }); + + // 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, + ], + ); + }); + + 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], + ); + }); + }); + + context('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); + }); + + 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); + }); + } + + 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 }); + 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); + }); + + 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], + ); + + 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); + }); + + 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)), + ); + + // 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), + ); + }); + }); + }); + }); +}); diff --git a/test/metatx/MinimalForwarder.test.js b/test/metatx/MinimalForwarder.test.js deleted file mode 100644 index 4884cc760bb..00000000000 --- a/test/metatx/MinimalForwarder.test.js +++ /dev/null @@ -1,169 +0,0 @@ -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; -const { getDomain, domainType } = require('../helpers/eip712'); - -const { expectRevert, constants } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); - -const MinimalForwarder = artifacts.require('MinimalForwarder'); -const CallReceiverMock = artifacts.require('CallReceiverMock'); - -contract('MinimalForwarder', function (accounts) { - beforeEach(async function () { - this.forwarder = await MinimalForwarder.new(); - - 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: 'data', type: 'bytes' }, - ], - }; - }); - - context('with message', function () { - beforeEach(async function () { - this.wallet = Wallet.generate(); - this.sender = web3.utils.toChecksumAddress(this.wallet.getAddressString()); - this.req = { - from: this.sender, - to: constants.ZERO_ADDRESS, - value: '0', - gas: '100000', - nonce: Number(await this.forwarder.getNonce(this.sender)), - data: '0x', - }; - this.sign = () => - ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), { - data: { - types: this.types, - domain: this.domain, - primaryType: 'ForwardRequest', - message: this.req, - }, - }); - }); - - context('verify', function () { - context('valid signature', function () { - beforeEach(async function () { - expect(await this.forwarder.getNonce(this.req.from)).to.be.bignumber.equal(web3.utils.toBN(this.req.nonce)); - }); - - it('success', async function () { - expect(await this.forwarder.verify(this.req, this.sign())).to.be.equal(true); - }); - - afterEach(async function () { - expect(await this.forwarder.getNonce(this.req.from)).to.be.bignumber.equal(web3.utils.toBN(this.req.nonce)); - }); - }); - - context('invalid signature', function () { - it('tampered from', async function () { - expect(await this.forwarder.verify({ ...this.req, from: accounts[0] }, this.sign())).to.be.equal(false); - }); - it('tampered to', async function () { - expect(await this.forwarder.verify({ ...this.req, to: accounts[0] }, this.sign())).to.be.equal(false); - }); - it('tampered value', async function () { - expect(await this.forwarder.verify({ ...this.req, value: web3.utils.toWei('1') }, this.sign())).to.be.equal( - false, - ); - }); - it('tampered nonce', async function () { - expect(await this.forwarder.verify({ ...this.req, nonce: this.req.nonce + 1 }, this.sign())).to.be.equal( - false, - ); - }); - it('tampered data', async function () { - expect(await this.forwarder.verify({ ...this.req, data: '0x1742' }, this.sign())).to.be.equal(false); - }); - it('tampered signature', async function () { - const tamperedsign = web3.utils.hexToBytes(this.sign()); - tamperedsign[42] ^= 0xff; - expect(await this.forwarder.verify(this.req, web3.utils.bytesToHex(tamperedsign))).to.be.equal(false); - }); - }); - }); - - context('execute', function () { - context('valid signature', function () { - beforeEach(async function () { - expect(await this.forwarder.getNonce(this.req.from)).to.be.bignumber.equal(web3.utils.toBN(this.req.nonce)); - }); - - it('success', async function () { - await this.forwarder.execute(this.req, this.sign()); // expect to not revert - }); - - afterEach(async function () { - expect(await this.forwarder.getNonce(this.req.from)).to.be.bignumber.equal( - web3.utils.toBN(this.req.nonce + 1), - ); - }); - }); - - context('invalid signature', function () { - it('tampered from', async function () { - await expectRevert( - this.forwarder.execute({ ...this.req, from: accounts[0] }, this.sign()), - 'MinimalForwarder: signature does not match request', - ); - }); - it('tampered to', async function () { - await expectRevert( - this.forwarder.execute({ ...this.req, to: accounts[0] }, this.sign()), - 'MinimalForwarder: signature does not match request', - ); - }); - it('tampered value', async function () { - await expectRevert( - this.forwarder.execute({ ...this.req, value: web3.utils.toWei('1') }, this.sign()), - 'MinimalForwarder: signature does not match request', - ); - }); - it('tampered nonce', async function () { - await expectRevert( - this.forwarder.execute({ ...this.req, nonce: this.req.nonce + 1 }, this.sign()), - 'MinimalForwarder: signature does not match request', - ); - }); - it('tampered data', async function () { - await expectRevert( - this.forwarder.execute({ ...this.req, data: '0x1742' }, this.sign()), - 'MinimalForwarder: signature does not match request', - ); - }); - it('tampered signature', async function () { - const tamperedsign = web3.utils.hexToBytes(this.sign()); - tamperedsign[42] ^= 0xff; - await expectRevert( - this.forwarder.execute(this.req, web3.utils.bytesToHex(tamperedsign)), - 'MinimalForwarder: signature does not match request', - ); - }); - }); - - it('bubble out of gas', async function () { - const receiver = await CallReceiverMock.new(); - const gasAvailable = 100000; - this.req.to = receiver.address; - this.req.data = receiver.contract.methods.mockFunctionOutOfGas().encodeABI(); - this.req.gas = 1000000; - - await expectRevert.assertion(this.forwarder.execute(this.req, this.sign(), { gas: gasAvailable })); - - const { transactions } = await web3.eth.getBlock('latest'); - const { gasUsed } = await web3.eth.getTransactionReceipt(transactions[0]); - - expect(gasUsed).to.be.equal(gasAvailable); - }); - }); - }); -}); diff --git a/test/proxy/Clones.test.js b/test/proxy/Clones.test.js index 947b2ed957f..2edd1999c87 100644 --- a/test/proxy/Clones.test.js +++ b/test/proxy/Clones.test.js @@ -1,7 +1,9 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { computeCreate2Address } = require('../helpers/create2'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); + const shouldBehaveLikeClone = require('./Clones.behaviour'); const Clones = artifacts.require('$Clones'); @@ -36,7 +38,7 @@ contract('Clones', function (accounts) { // deploy once expectEvent(await factory.$cloneDeterministic(implementation, salt), 'return$cloneDeterministic'); // deploy twice - await expectRevert(factory.$cloneDeterministic(implementation, salt), 'ERC1167: create2 failed'); + await expectRevertCustomError(factory.$cloneDeterministic(implementation, salt), 'ERC1167FailedCreateClone', []); }); it('address prediction', async function () { diff --git a/test/proxy/beacon/BeaconProxy.test.js b/test/proxy/beacon/BeaconProxy.test.js index 68db10ddc06..63d98239760 100644 --- a/test/proxy/beacon/BeaconProxy.test.js +++ b/test/proxy/beacon/BeaconProxy.test.js @@ -1,6 +1,8 @@ const { expectRevert } = require('@openzeppelin/test-helpers'); const { getSlot, BeaconSlot } = require('../../helpers/erc1967'); +const { expectRevertCustomError } = require('../../helpers/customError'); + const { expect } = require('chai'); const UpgradeableBeacon = artifacts.require('UpgradeableBeacon'); @@ -15,7 +17,7 @@ contract('BeaconProxy', function (accounts) { describe('bad beacon is not accepted', async function () { it('non-contract beacon', async function () { - await expectRevert(BeaconProxy.new(anotherAccount, '0x'), 'ERC1967: new beacon is not a contract'); + await expectRevertCustomError(BeaconProxy.new(anotherAccount, '0x'), 'ERC1967InvalidBeacon', [anotherAccount]); }); it('non-compliant beacon', async function () { @@ -25,7 +27,10 @@ contract('BeaconProxy', function (accounts) { it('non-contract implementation', async function () { const beacon = await BadBeaconNotContract.new(); - await expectRevert(BeaconProxy.new(beacon.address, '0x'), 'ERC1967: beacon implementation is not a contract'); + const implementation = await beacon.implementation(); + await expectRevertCustomError(BeaconProxy.new(beacon.address, '0x'), 'ERC1967InvalidImplementation', [ + implementation, + ]); }); }); diff --git a/test/proxy/beacon/UpgradeableBeacon.test.js b/test/proxy/beacon/UpgradeableBeacon.test.js index c19b250b9e3..4c58f1740b6 100644 --- a/test/proxy/beacon/UpgradeableBeacon.test.js +++ b/test/proxy/beacon/UpgradeableBeacon.test.js @@ -1,6 +1,8 @@ -const { expectRevert, expectEvent } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../helpers/customError'); + const UpgradeableBeacon = artifacts.require('UpgradeableBeacon'); const Implementation1 = artifacts.require('Implementation1'); const Implementation2 = artifacts.require('Implementation2'); @@ -9,10 +11,7 @@ contract('UpgradeableBeacon', function (accounts) { const [owner, other] = accounts; it('cannot be created with non-contract implementation', async function () { - await expectRevert( - UpgradeableBeacon.new(accounts[0], owner), - 'UpgradeableBeacon: implementation is not a contract', - ); + await expectRevertCustomError(UpgradeableBeacon.new(other, owner), 'BeaconInvalidImplementation', [other]); }); context('once deployed', async function () { @@ -33,15 +32,16 @@ contract('UpgradeableBeacon', function (accounts) { }); it('cannot be upgraded to a non-contract', async function () { - await expectRevert( - this.beacon.upgradeTo(other, { from: owner }), - 'UpgradeableBeacon: implementation is not a contract', - ); + await expectRevertCustomError(this.beacon.upgradeTo(other, { from: owner }), 'BeaconInvalidImplementation', [ + other, + ]); }); it('cannot be upgraded by other account', async function () { const v2 = await Implementation2.new(); - await expectRevert(this.beacon.upgradeTo(v2.address, { from: other }), 'Ownable: caller is not the owner'); + await expectRevertCustomError(this.beacon.upgradeTo(v2.address, { from: other }), 'OwnableUnauthorizedAccount', [ + other, + ]); }); }); }); diff --git a/test/proxy/transparent/ProxyAdmin.test.js b/test/proxy/transparent/ProxyAdmin.test.js index 85b6695dff0..23f7ce9b286 100644 --- a/test/proxy/transparent/ProxyAdmin.test.js +++ b/test/proxy/transparent/ProxyAdmin.test.js @@ -1,5 +1,4 @@ const { expectRevert } = require('@openzeppelin/test-helpers'); -const { getAddressInSlot, ImplementationSlot, AdminSlot } = require('../../helpers/erc1967'); const { expect } = require('chai'); const ImplV1 = artifacts.require('DummyImplementation'); const ImplV2 = artifacts.require('DummyImplementationV2'); @@ -7,8 +6,11 @@ const ProxyAdmin = artifacts.require('ProxyAdmin'); const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy'); const ITransparentUpgradeableProxy = artifacts.require('ITransparentUpgradeableProxy'); +const { getAddressInSlot, ImplementationSlot } = require('../../helpers/erc1967'); +const { expectRevertCustomError } = require('../../helpers/customError'); + contract('ProxyAdmin', function (accounts) { - const [proxyAdminOwner, newAdmin, anotherAccount] = accounts; + const [proxyAdminOwner, anotherAccount] = accounts; before('set implementations', async function () { this.implementationV1 = await ImplV1.new(); @@ -30,28 +32,13 @@ contract('ProxyAdmin', function (accounts) { expect(await this.proxyAdmin.owner()).to.equal(proxyAdminOwner); }); - describe('#changeProxyAdmin', function () { - it('fails to change proxy admin if its not the proxy owner', async function () { - await expectRevert( - this.proxyAdmin.changeProxyAdmin(this.proxy.address, newAdmin, { from: anotherAccount }), - 'caller is not the owner', - ); - }); - - it('changes proxy admin', async function () { - await this.proxyAdmin.changeProxyAdmin(this.proxy.address, newAdmin, { from: proxyAdminOwner }); - - const newProxyAdmin = await getAddressInSlot(this.proxy, AdminSlot); - expect(newProxyAdmin).to.be.equal(newAdmin); - }); - }); - describe('#upgrade', function () { context('with unauthorized account', function () { it('fails to upgrade', async function () { - await expectRevert( + await expectRevertCustomError( this.proxyAdmin.upgrade(this.proxy.address, this.implementationV2.address, { from: anotherAccount }), - 'caller is not the owner', + 'OwnableUnauthorizedAccount', + [anotherAccount], ); }); }); @@ -70,11 +57,12 @@ contract('ProxyAdmin', function (accounts) { context('with unauthorized account', function () { it('fails to upgrade', async function () { const callData = new ImplV1('').contract.methods.initializeNonPayableWithValue(1337).encodeABI(); - await expectRevert( + await expectRevertCustomError( this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, { from: anotherAccount, }), - 'caller is not the owner', + 'OwnableUnauthorizedAccount', + [anotherAccount], ); }); }); diff --git a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js index 4012cfffce0..1a03b84db5c 100644 --- a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js +++ b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js @@ -1,6 +1,7 @@ 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 { expect } = require('chai'); const { web3 } = require('hardhat'); @@ -46,6 +47,32 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx }); }); + describe('proxy admin', function () { + it('emits AdminChanged event during construction', async function () { + expectEvent.inConstruction(this.proxy, 'AdminChanged', { + previousAdmin: ZERO_ADDRESS, + newAdmin: proxyAdminAddress, + }); + }); + + it('sets the admin in the storage', async function () { + expect(await getAddressInSlot(this.proxy, AdminSlot)).to.be.equal(proxyAdminAddress); + }); + + it('can overwrite the admin by the implementation', async function () { + const dummy = new DummyImplementation(this.proxyAddress); + await dummy.unsafeOverrideAdmin(anotherAccount); + const ERC1967AdminSlotValue = await getAddressInSlot(this.proxy, AdminSlot); + expect(ERC1967AdminSlotValue).to.be.equal(anotherAccount); + + // Still allows previous admin to execute admin operations + expect(ERC1967AdminSlotValue).to.not.equal(proxyAdminAddress); + expectEvent(await this.proxy.upgradeTo(this.implementationV1, { from: proxyAdminAddress }), 'Upgraded', { + implementation: this.implementationV1, + }); + }); + }); + describe('upgradeTo', function () { describe('when the sender is the admin', function () { const from = proxyAdminAddress; @@ -67,10 +94,9 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx describe('when the given implementation is the zero address', function () { it('reverts', async function () { - await expectRevert( - this.proxy.upgradeTo(ZERO_ADDRESS, { from }), - 'ERC1967: new implementation is not a contract', - ); + await expectRevertCustomError(this.proxy.upgradeTo(ZERO_ADDRESS, { from }), 'ERC1967InvalidImplementation', [ + ZERO_ADDRESS, + ]); }); }); }); @@ -258,79 +284,48 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx }); }); - describe('changeAdmin', function () { - describe('when the new proposed admin is not the zero address', function () { - const newAdmin = anotherAccount; - - describe('when the sender is the admin', function () { - beforeEach('transferring', async function () { - this.receipt = await this.proxy.changeAdmin(newAdmin, { from: proxyAdminAddress }); - }); - - it('assigns new proxy admin', async function () { - const newProxyAdmin = await getAddressInSlot(this.proxy, AdminSlot); - expect(newProxyAdmin).to.be.equal(anotherAccount); - }); - - it('emits an event', function () { - expectEvent(this.receipt, 'AdminChanged', { - previousAdmin: proxyAdminAddress, - newAdmin: newAdmin, - }); - }); - }); - - describe('when the sender is not the admin', function () { - it('reverts', async function () { - await expectRevert.unspecified(this.proxy.changeAdmin(newAdmin, { from: anotherAccount })); - }); - }); - }); - - describe('when the new proposed admin is the zero address', function () { - it('reverts', async function () { - await expectRevert( - this.proxy.changeAdmin(ZERO_ADDRESS, { from: proxyAdminAddress }), - 'ERC1967: new admin is the zero address', - ); - }); - }); - }); - describe('transparent proxy', function () { beforeEach('creating proxy', async function () { const initializeData = Buffer.from(''); - this.impl = await ClashingImplementation.new(); - this.proxy = await createProxy(this.impl.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner }); + this.clashingImplV0 = (await ClashingImplementation.new()).address; + this.clashingImplV1 = (await ClashingImplementation.new()).address; + this.proxy = await createProxy(this.clashingImplV0, proxyAdminAddress, initializeData, { + from: proxyAdminOwner, + }); this.clashing = new ClashingImplementation(this.proxy.address); }); it('proxy admin cannot call delegated functions', async function () { - await expectRevert( + await expectRevertCustomError( this.clashing.delegatedFunction({ from: proxyAdminAddress }), - 'TransparentUpgradeableProxy: admin cannot fallback to proxy target', + 'ProxyDeniedAdminAccess', + [], ); }); describe('when function names clash', function () { - it('when sender is proxy admin should run the proxy function', async function () { - const receipt = await this.proxy.changeAdmin(anotherAccount, { from: proxyAdminAddress, value: 0 }); - expectEvent(receipt, 'AdminChanged'); + it('executes the proxy function if the sender is the admin', async function () { + const receipt = await this.proxy.upgradeTo(this.clashingImplV1, { from: proxyAdminAddress, value: 0 }); + expectEvent(receipt, 'Upgraded', { implementation: this.clashingImplV1 }); }); - it('when sender is other should delegate to implementation', async function () { - const receipt = await this.proxy.changeAdmin(anotherAccount, { from: anotherAccount, value: 0 }); - expectEvent.notEmitted(receipt, 'AdminChanged'); + it('delegates the call to implementation when sender is not the admin', async function () { + const receipt = await this.proxy.upgradeTo(this.clashingImplV1, { from: anotherAccount, value: 0 }); + expectEvent.notEmitted(receipt, 'Upgraded'); expectEvent.inTransaction(receipt.tx, this.clashing, 'ClashingImplementationCall'); }); - it('when sender is proxy admin value should not be accepted', async function () { - await expectRevert.unspecified(this.proxy.changeAdmin(anotherAccount, { from: proxyAdminAddress, value: 1 })); + it('requires 0 value calling upgradeTo by proxy admin', async function () { + await expectRevertCustomError( + this.proxy.upgradeTo(this.clashingImplV1, { from: proxyAdminAddress, value: 1 }), + 'ProxyNonPayableFunction', + [], + ); }); - it('when sender is other value should be accepted', async function () { - const receipt = await this.proxy.changeAdmin(anotherAccount, { from: anotherAccount, value: 1 }); - expectEvent.notEmitted(receipt, 'AdminChanged'); + it('allows calling with value if sender is not the admin', async function () { + const receipt = await this.proxy.upgradeTo(this.clashingImplV1, { from: anotherAccount, value: 1 }); + expectEvent.notEmitted(receipt, 'Upgraded'); expectEvent.inTransaction(receipt.tx, this.clashing, 'ClashingImplementationCall'); }); }); diff --git a/test/proxy/utils/Initializable.test.js b/test/proxy/utils/Initializable.test.js index 39c820b9d60..e3e0fc02f77 100644 --- a/test/proxy/utils/Initializable.test.js +++ b/test/proxy/utils/Initializable.test.js @@ -1,5 +1,6 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../helpers/customError'); const InitializableMock = artifacts.require('InitializableMock'); const ConstructorInitializableMock = artifacts.require('ConstructorInitializableMock'); @@ -40,13 +41,13 @@ contract('Initializable', function () { }); it('initializer does not run again', async function () { - await expectRevert(this.contract.initialize(), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.initialize(), 'AlreadyInitialized', []); }); }); describe('nested under an initializer', function () { it('initializer modifier reverts', async function () { - await expectRevert(this.contract.initializerNested(), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.initializerNested(), 'AlreadyInitialized', []); }); it('onlyInitializing modifier succeeds', async function () { @@ -56,7 +57,7 @@ contract('Initializable', function () { }); it('cannot call onlyInitializable function outside the scope of an initializable function', async function () { - await expectRevert(this.contract.initializeOnlyInitializing(), 'Initializable: contract is not initializing'); + await expectRevertCustomError(this.contract.initializeOnlyInitializing(), 'NotInitializing', []); }); }); @@ -98,9 +99,9 @@ contract('Initializable', function () { it('cannot nest reinitializers', async function () { expect(await this.contract.counter()).to.be.bignumber.equal('0'); - await expectRevert(this.contract.nestedReinitialize(2, 2), 'Initializable: contract is already initialized'); - await expectRevert(this.contract.nestedReinitialize(2, 3), 'Initializable: contract is already initialized'); - await expectRevert(this.contract.nestedReinitialize(3, 2), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.nestedReinitialize(2, 2), 'AlreadyInitialized', []); + await expectRevertCustomError(this.contract.nestedReinitialize(2, 3), 'AlreadyInitialized', []); + await expectRevertCustomError(this.contract.nestedReinitialize(3, 2), 'AlreadyInitialized', []); }); it('can chain reinitializers', async function () { @@ -119,18 +120,18 @@ contract('Initializable', function () { describe('contract locking', function () { it('prevents initialization', async function () { await this.contract.disableInitializers(); - await expectRevert(this.contract.initialize(), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.initialize(), 'AlreadyInitialized', []); }); it('prevents re-initialization', async function () { await this.contract.disableInitializers(); - await expectRevert(this.contract.reinitialize(255), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.reinitialize(255), 'AlreadyInitialized', []); }); it('can lock contract after initialization', async function () { await this.contract.initialize(); await this.contract.disableInitializers(); - await expectRevert(this.contract.reinitialize(255), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.reinitialize(255), 'AlreadyInitialized', []); }); }); }); @@ -205,8 +206,8 @@ contract('Initializable', function () { describe('disabling initialization', function () { it('old and new patterns in bad sequence', async function () { - await expectRevert(DisableBad1.new(), 'Initializable: contract is already initialized'); - await expectRevert(DisableBad2.new(), 'Initializable: contract is initializing'); + await expectRevertCustomError(DisableBad1.new(), 'AlreadyInitialized', []); + await expectRevertCustomError(DisableBad2.new(), 'AlreadyInitialized', []); }); it('old and new patterns in good sequence', async function () { diff --git a/test/proxy/utils/UUPSUpgradeable.test.js b/test/proxy/utils/UUPSUpgradeable.test.js index b0c1b3f6f4b..6a8104248fb 100644 --- a/test/proxy/utils/UUPSUpgradeable.test.js +++ b/test/proxy/utils/UUPSUpgradeable.test.js @@ -1,12 +1,13 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); -const { web3 } = require('@openzeppelin/test-helpers/src/setup'); -const { getSlot, ImplementationSlot } = require('../../helpers/erc1967'); +const { expectEvent } = require('@openzeppelin/test-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 UUPSUpgradeableLegacyMock = artifacts.require('UUPSUpgradeableLegacyMock'); const NonUpgradeableMock = artifacts.require('NonUpgradeableMock'); +const UUPSUnsupportedProxiableUUID = artifacts.require('UUPSUnsupportedProxiableUUID'); +const Clones = artifacts.require('$Clones'); contract('UUPSUpgradeable', function () { before(async function () { @@ -14,6 +15,9 @@ contract('UUPSUpgradeable', function () { 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(); }); beforeEach(async function () { @@ -25,6 +29,7 @@ contract('UUPSUpgradeable', function () { const { receipt } = await this.instance.upgradeTo(this.implUpgradeOk.address); 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); }); it('upgrade to upgradeable implementation with call', async function () { @@ -36,50 +41,85 @@ contract('UUPSUpgradeable', function () { ); 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); expect(await this.instance.current()).to.be.bignumber.equal('1'); }); - it('upgrade to and unsafe upgradeable implementation', async function () { - const { receipt } = await this.instance.upgradeTo(this.implUpgradeUnsafe.address); - expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeUnsafe.address }); + it('calling upgradeTo on the implementation reverts', async function () { + await expectRevertCustomError( + this.implInitial.upgradeTo(this.implUpgradeOk.address), + 'UUPSUnauthorizedCallContext', + [], + ); }); - // delegate to a non existing upgradeTo function causes a low level revert - it('reject upgrade to non uups implementation', async function () { - await expectRevert( - this.instance.upgradeTo(this.implUpgradeNonUUPS.address), - 'ERC1967Upgrade: new implementation is not UUPS', + it('calling upgradeToAndCall on the implementation reverts', async function () { + await expectRevertCustomError( + this.implInitial.upgradeToAndCall( + this.implUpgradeOk.address, + this.implUpgradeOk.contract.methods.increment().encodeABI(), + ), + 'UUPSUnauthorizedCallContext', + [], ); }); - it('reject proxy address as implementation', async function () { - const { address } = await ERC1967Proxy.new(this.implInitial.address, '0x'); - const otherInstance = await UUPSUpgradeableMock.at(address); + 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.upgradeTo(this.implUpgradeUnsafe.address), + 'UUPSUnauthorizedCallContext', + [], + ); + }); - await expectRevert( - this.instance.upgradeTo(otherInstance.address), - 'ERC1967Upgrade: new implementation is not UUPS', + 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, + ); + + await expectRevertCustomError( + instance.upgradeToAndCall(this.implUpgradeUnsafe.address, '0x'), + 'UUPSUnauthorizedCallContext', + [], ); }); - it('can upgrade from legacy implementations', async function () { - const legacyImpl = await UUPSUpgradeableLegacyMock.new(); - const legacyInstance = await ERC1967Proxy.new(legacyImpl.address, '0x').then(({ address }) => - UUPSUpgradeableLegacyMock.at(address), + it('rejects upgrading to an unsupported UUID', async function () { + await expectRevertCustomError( + this.instance.upgradeTo(this.implUnsupportedUUID.address), + 'UUPSUnsupportedProxiableUUID', + [web3.utils.keccak256('invalid UUID')], ); + }); - const receipt = await legacyInstance.upgradeTo(this.implInitial.address); + it('upgrade to and unsafe upgradeable implementation', async function () { + const { receipt } = await this.instance.upgradeTo(this.implUpgradeUnsafe.address); + expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeUnsafe.address }); + expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.be.equal(this.implUpgradeUnsafe.address); + }); - const UpgradedEvents = receipt.logs.filter( - ({ address, event }) => address === legacyInstance.address && event === 'Upgraded', + // 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.upgradeTo(this.implUpgradeNonUUPS.address), + 'ERC1967InvalidImplementation', + [this.implUpgradeNonUUPS.address], ); - expect(UpgradedEvents.length).to.be.equal(1); + }); - expectEvent(receipt, 'Upgraded', { implementation: this.implInitial.address }); + it('reject proxy address as implementation', async function () { + const { address } = await ERC1967Proxy.new(this.implInitial.address, '0x'); + const otherInstance = await UUPSUpgradeableMock.at(address); - const implementationSlot = await getSlot(legacyInstance, ImplementationSlot); - const implementationAddress = web3.utils.toChecksumAddress(implementationSlot.substr(-40)); - expect(implementationAddress).to.be.equal(this.implInitial.address); + await expectRevertCustomError(this.instance.upgradeTo(otherInstance.address), 'ERC1967InvalidImplementation', [ + otherInstance.address, + ]); }); }); diff --git a/test/security/Pausable.test.js b/test/security/Pausable.test.js index 5cca11e47de..e60a62c749e 100644 --- a/test/security/Pausable.test.js +++ b/test/security/Pausable.test.js @@ -1,7 +1,8 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); - +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); + const PausableMock = artifacts.require('PausableMock'); contract('Pausable', function (accounts) { @@ -24,7 +25,7 @@ contract('Pausable', function (accounts) { }); it('cannot take drastic measure in non-pause', async function () { - await expectRevert(this.pausable.drasticMeasure(), 'Pausable: not paused'); + await expectRevertCustomError(this.pausable.drasticMeasure(), 'ExpectedPause', []); expect(await this.pausable.drasticMeasureTaken()).to.equal(false); }); @@ -38,7 +39,7 @@ contract('Pausable', function (accounts) { }); it('cannot perform normal process in pause', async function () { - await expectRevert(this.pausable.normalProcess(), 'Pausable: paused'); + await expectRevertCustomError(this.pausable.normalProcess(), 'EnforcedPause', []); }); it('can take a drastic measure in a pause', async function () { @@ -47,7 +48,7 @@ contract('Pausable', function (accounts) { }); it('reverts when re-pausing', async function () { - await expectRevert(this.pausable.pause(), 'Pausable: paused'); + await expectRevertCustomError(this.pausable.pause(), 'EnforcedPause', []); }); describe('unpausing', function () { @@ -72,11 +73,11 @@ contract('Pausable', function (accounts) { }); it('should prevent drastic measure', async function () { - await expectRevert(this.pausable.drasticMeasure(), 'Pausable: not paused'); + await expectRevertCustomError(this.pausable.drasticMeasure(), 'ExpectedPause', []); }); it('reverts when re-unpausing', async function () { - await expectRevert(this.pausable.unpause(), 'Pausable: not paused'); + await expectRevertCustomError(this.pausable.unpause(), 'ExpectedPause', []); }); }); }); diff --git a/test/security/ReentrancyGuard.test.js b/test/security/ReentrancyGuard.test.js index 1a80bc86005..15355c09851 100644 --- a/test/security/ReentrancyGuard.test.js +++ b/test/security/ReentrancyGuard.test.js @@ -1,7 +1,8 @@ const { expectRevert } = require('@openzeppelin/test-helpers'); - const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); + const ReentrancyMock = artifacts.require('ReentrancyMock'); const ReentrancyAttack = artifacts.require('ReentrancyAttack'); @@ -19,7 +20,7 @@ contract('ReentrancyGuard', function () { it('does not allow remote callback', async function () { const attacker = await ReentrancyAttack.new(); - await expectRevert(this.reentrancyMock.countAndCall(attacker.address), 'ReentrancyAttack: failed call'); + await expectRevert(this.reentrancyMock.countAndCall(attacker.address), 'ReentrancyAttack: failed call', []); }); it('_reentrancyGuardEntered should be true when guarded', async function () { @@ -34,10 +35,10 @@ contract('ReentrancyGuard', function () { // I put them here as documentation, and to monitor any changes // in the side-effects. it('does not allow local recursion', async function () { - await expectRevert(this.reentrancyMock.countLocalRecursive(10), 'ReentrancyGuard: reentrant call'); + await expectRevertCustomError(this.reentrancyMock.countLocalRecursive(10), 'ReentrancyGuardReentrantCall', []); }); it('does not allow indirect local recursion', async function () { - await expectRevert(this.reentrancyMock.countThisRecursive(10), 'ReentrancyMock: failed call'); + await expectRevert(this.reentrancyMock.countThisRecursive(10), 'ReentrancyMock: failed call', []); }); }); diff --git a/test/token/ERC1155/ERC1155.behavior.js b/test/token/ERC1155/ERC1155.behavior.js index c41c69c6255..4bf4a7319e7 100644 --- a/test/token/ERC1155/ERC1155.behavior.js +++ b/test/token/ERC1155/ERC1155.behavior.js @@ -1,11 +1,13 @@ const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS } = constants; - const { expect } = require('chai'); +const { ZERO_ADDRESS } = constants; 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); @@ -56,21 +58,19 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m describe('balanceOfBatch', function () { it("reverts when input arrays don't match up", async function () { - await expectRevert( - this.token.balanceOfBatch( - [firstTokenHolder, secondTokenHolder, firstTokenHolder, secondTokenHolder], - [firstTokenId, secondTokenId, unknownTokenId], - ), - 'ERC1155: accounts and ids length mismatch', - ); + const accounts1 = [firstTokenHolder, secondTokenHolder, firstTokenHolder, secondTokenHolder]; + const ids1 = [firstTokenId, secondTokenId, unknownTokenId]; + await expectRevertCustomError(this.token.balanceOfBatch(accounts1, ids1), 'ERC1155InvalidArrayLength', [ + accounts1.length, + ids1.length, + ]); - await expectRevert( - this.token.balanceOfBatch( - [firstTokenHolder, secondTokenHolder], - [firstTokenId, secondTokenId, unknownTokenId], - ), - 'ERC1155: accounts and ids length mismatch', - ); + const accounts2 = [firstTokenHolder, secondTokenHolder]; + const ids2 = [firstTokenId, secondTokenId, unknownTokenId]; + await expectRevertCustomError(this.token.balanceOfBatch(accounts2, ids2), 'ERC1155InvalidArrayLength', [ + accounts2.length, + ids2.length, + ]); }); it('should return 0 as the balance when one of the addresses is the zero address', async function () { @@ -152,9 +152,10 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts if attempting to approve self as an operator', async function () { - await expectRevert( + await expectRevertCustomError( this.token.setApprovalForAll(multiTokenHolder, true, { from: multiTokenHolder }), - 'ERC1155: setting approval status for self', + 'ERC1155InvalidOperator', + [multiTokenHolder], ); }); }); @@ -170,20 +171,22 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts when transferring more than balance', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(multiTokenHolder, recipient, firstTokenId, firstAmount.addn(1), '0x', { from: multiTokenHolder, }), - 'ERC1155: insufficient balance for transfer', + 'ERC1155InsufficientBalance', + [multiTokenHolder, firstAmount, firstAmount.addn(1), firstTokenId], ); }); it('reverts when transferring to zero address', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(multiTokenHolder, ZERO_ADDRESS, firstTokenId, firstAmount, '0x', { from: multiTokenHolder, }), - 'ERC1155: transfer to the zero address', + 'ERC1155InvalidReceiver', + [ZERO_ADDRESS], ); }); @@ -247,11 +250,12 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(multiTokenHolder, recipient, firstTokenId, firstAmount, '0x', { from: proxy, }), - 'ERC1155: caller is not token owner or approved', + 'ERC1155MissingApprovalForAll', + [proxy, multiTokenHolder], ); }); }); @@ -293,9 +297,8 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m beforeEach(async function () { this.receiver = await ERC1155ReceiverMock.new( RECEIVER_SINGLE_MAGIC_VALUE, - false, RECEIVER_BATCH_MAGIC_VALUE, - false, + RevertType.None, ); }); @@ -367,36 +370,96 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m context('to a receiver contract returning unexpected value', function () { beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new('0x00c0ffee', false, RECEIVER_BATCH_MAGIC_VALUE, false); + this.receiver = await ERC1155ReceiverMock.new('0x00c0ffee', RECEIVER_BATCH_MAGIC_VALUE, RevertType.None); }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { from: multiTokenHolder, }), - 'ERC1155: ERC1155Receiver rejected tokens', + 'ERC1155InvalidReceiver', + [this.receiver.address], ); }); }); context('to a receiver contract that reverts', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( - RECEIVER_SINGLE_MAGIC_VALUE, - true, - RECEIVER_BATCH_MAGIC_VALUE, - false, - ); + context('with a revert string', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.RevertWithMessage, + ); + }); + + it('reverts', async function () { + await expectRevert( + this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { + from: multiTokenHolder, + }), + 'ERC1155ReceiverMock: reverting on receive', + ); + }); }); - it('reverts', async function () { - await expectRevert( - this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { - from: multiTokenHolder, - }), - 'ERC1155ReceiverMock: reverting on receive', - ); + context('without a revert string', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.RevertWithoutMessage, + ); + }); + + it('reverts', async function () { + await expectRevertCustomError( + this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { + from: multiTokenHolder, + }), + 'ERC1155InvalidReceiver', + [this.receiver.address], + ); + }); + }); + + context('with a custom error', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.RevertWithCustomError, + ); + }); + + it('reverts', async function () { + await expectRevertCustomError( + this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { + from: multiTokenHolder, + }), + 'CustomError', + [RECEIVER_SINGLE_MAGIC_VALUE], + ); + }); + }); + + context('with a panic', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + 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, firstAmount, '0x', { + from: multiTokenHolder, + }), + ); + }); }); }); @@ -423,7 +486,7 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts when transferring amount more than any of balances', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom( multiTokenHolder, recipient, @@ -432,38 +495,36 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m '0x', { from: multiTokenHolder }, ), - 'ERC1155: insufficient balance for transfer', + 'ERC1155InsufficientBalance', + [multiTokenHolder, secondAmount, secondAmount.addn(1), secondTokenId], ); }); it("reverts when ids array length doesn't match amounts array length", async function () { - await expectRevert( - this.token.safeBatchTransferFrom( - multiTokenHolder, - recipient, - [firstTokenId], - [firstAmount, secondAmount], - '0x', - { from: multiTokenHolder }, - ), - 'ERC1155: ids and amounts length mismatch', + const ids1 = [firstTokenId]; + const amounts1 = [firstAmount, secondAmount]; + + await expectRevertCustomError( + this.token.safeBatchTransferFrom(multiTokenHolder, recipient, ids1, amounts1, '0x', { + from: multiTokenHolder, + }), + 'ERC1155InvalidArrayLength', + [ids1.length, amounts1.length], ); - await expectRevert( - this.token.safeBatchTransferFrom( - multiTokenHolder, - recipient, - [firstTokenId, secondTokenId], - [firstAmount], - '0x', - { from: multiTokenHolder }, - ), - 'ERC1155: ids and amounts length mismatch', + const ids2 = [firstTokenId, secondTokenId]; + const amounts2 = [firstAmount]; + await expectRevertCustomError( + this.token.safeBatchTransferFrom(multiTokenHolder, recipient, ids2, amounts2, '0x', { + from: multiTokenHolder, + }), + 'ERC1155InvalidArrayLength', + [ids2.length, amounts2.length], ); }); it('reverts when transferring to zero address', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom( multiTokenHolder, ZERO_ADDRESS, @@ -472,7 +533,16 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m '0x', { from: multiTokenHolder }, ), - 'ERC1155: transfer to the zero address', + 'ERC1155InvalidReceiver', + [ZERO_ADDRESS], + ); + }); + + it('reverts when transferring from zero address', async function () { + await expectRevertCustomError( + this.token.$_safeBatchTransferFrom(ZERO_ADDRESS, multiTokenHolder, [firstTokenId], [firstAmount], '0x'), + 'ERC1155InvalidSender', + [ZERO_ADDRESS], ); }); @@ -530,7 +600,7 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom( multiTokenHolder, recipient, @@ -539,7 +609,8 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m '0x', { from: proxy }, ), - 'ERC1155: caller is not token owner or approved', + 'ERC1155MissingApprovalForAll', + [proxy, multiTokenHolder], ); }); }); @@ -578,9 +649,8 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m beforeEach(async function () { this.receiver = await ERC1155ReceiverMock.new( RECEIVER_SINGLE_MAGIC_VALUE, - false, RECEIVER_BATCH_MAGIC_VALUE, - false, + RevertType.None, ); }); @@ -654,14 +724,13 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m beforeEach(async function () { this.receiver = await ERC1155ReceiverMock.new( RECEIVER_SINGLE_MAGIC_VALUE, - false, RECEIVER_SINGLE_MAGIC_VALUE, - false, + RevertType.None, ); }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom( multiTokenHolder, this.receiver.address, @@ -670,71 +739,107 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m '0x', { from: multiTokenHolder }, ), - 'ERC1155: ERC1155Receiver rejected tokens', + 'ERC1155InvalidReceiver', + [this.receiver.address], ); }); }); context('to a receiver contract that reverts', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( - RECEIVER_SINGLE_MAGIC_VALUE, - false, - RECEIVER_BATCH_MAGIC_VALUE, - true, - ); - }); + context('with a revert string', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + 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], - [firstAmount, secondAmount], - '0x', - { from: multiTokenHolder }, - ), - 'ERC1155ReceiverMock: reverting on batch receive', - ); + it('reverts', async function () { + await expectRevert( + this.token.safeBatchTransferFrom( + multiTokenHolder, + this.receiver.address, + [firstTokenId, secondTokenId], + [firstAmount, secondAmount], + '0x', + { from: multiTokenHolder }, + ), + 'ERC1155ReceiverMock: reverting on batch receive', + ); + }); }); - }); - context('to a receiver contract that reverts only on single transfers', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( - RECEIVER_SINGLE_MAGIC_VALUE, - true, - RECEIVER_BATCH_MAGIC_VALUE, - false, - ); + context('without a revert string', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.RevertWithoutMessage, + ); + }); - this.toWhom = this.receiver.address; - this.transferReceipt = await this.token.safeBatchTransferFrom( - multiTokenHolder, - this.receiver.address, - [firstTokenId, secondTokenId], - [firstAmount, secondAmount], - '0x', - { from: multiTokenHolder }, - ); - this.transferLogs = this.transferReceipt; + it('reverts', async function () { + await expectRevertCustomError( + this.token.safeBatchTransferFrom( + multiTokenHolder, + this.receiver.address, + [firstTokenId, secondTokenId], + [firstAmount, secondAmount], + '0x', + { from: multiTokenHolder }, + ), + 'ERC1155InvalidReceiver', + [this.receiver.address], + ); + }); }); - batchTransferWasSuccessful.call(this, { - operator: multiTokenHolder, - from: multiTokenHolder, - ids: [firstTokenId, secondTokenId], - values: [firstAmount, secondAmount], + context('with a custom error', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + 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], + [firstAmount, secondAmount], + '0x', + { from: multiTokenHolder }, + ), + 'CustomError', + [RECEIVER_SINGLE_MAGIC_VALUE], + ); + }); }); - it('calls onERC1155BatchReceived', async function () { - await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'BatchReceived', { - operator: multiTokenHolder, - from: multiTokenHolder, - // ids: [firstTokenId, secondTokenId], - // values: [firstAmount, secondAmount], - data: null, + context('with a panic', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + 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], + [firstAmount, secondAmount], + '0x', + { from: multiTokenHolder }, + ), + ); }); }); }); diff --git a/test/token/ERC1155/ERC1155.test.js b/test/token/ERC1155/ERC1155.test.js index 48197eeb562..23555dd5491 100644 --- a/test/token/ERC1155/ERC1155.test.js +++ b/test/token/ERC1155/ERC1155.test.js @@ -1,8 +1,10 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); const { ZERO_ADDRESS } = constants; const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../helpers/customError'); + const { shouldBehaveLikeERC1155 } = require('./ERC1155.behavior'); const ERC1155Mock = artifacts.require('$ERC1155'); @@ -30,9 +32,10 @@ contract('ERC1155', function (accounts) { describe('_mint', function () { it('reverts with a zero destination address', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_mint(ZERO_ADDRESS, tokenId, mintAmount, data), - 'ERC1155: mint to the zero address', + 'ERC1155InvalidReceiver', + [ZERO_ADDRESS], ); }); @@ -59,21 +62,24 @@ contract('ERC1155', function (accounts) { describe('_mintBatch', function () { it('reverts with a zero destination address', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_mintBatch(ZERO_ADDRESS, tokenBatchIds, mintAmounts, data), - 'ERC1155: mint to the zero address', + 'ERC1155InvalidReceiver', + [ZERO_ADDRESS], ); }); it('reverts if length of inputs do not match', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_mintBatch(tokenBatchHolder, tokenBatchIds, mintAmounts.slice(1), data), - 'ERC1155: ids and amounts length mismatch', + 'ERC1155InvalidArrayLength', + [tokenBatchIds.length, mintAmounts.length - 1], ); - await expectRevert( + await expectRevertCustomError( this.token.$_mintBatch(tokenBatchHolder, tokenBatchIds.slice(1), mintAmounts, data), - 'ERC1155: ids and amounts length mismatch', + 'ERC1155InvalidArrayLength', + [tokenBatchIds.length - 1, mintAmounts.length], ); }); @@ -107,22 +113,26 @@ contract('ERC1155', function (accounts) { describe('_burn', function () { it("reverts when burning the zero account's tokens", async function () { - await expectRevert(this.token.$_burn(ZERO_ADDRESS, tokenId, mintAmount), 'ERC1155: burn from the zero address'); + await expectRevertCustomError(this.token.$_burn(ZERO_ADDRESS, tokenId, mintAmount), 'ERC1155InvalidSender', [ + ZERO_ADDRESS, + ]); }); it('reverts when burning a non-existent token id', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_burn(tokenHolder, tokenId, mintAmount), - 'ERC1155: insufficient balance for transfer', + 'ERC1155InsufficientBalance', + [tokenHolder, 0, mintAmount, tokenId], ); }); it('reverts when burning more than available tokens', async function () { await this.token.$_mint(tokenHolder, tokenId, mintAmount, data, { from: operator }); - await expectRevert( + await expectRevertCustomError( this.token.$_burn(tokenHolder, tokenId, mintAmount.addn(1)), - 'ERC1155: insufficient balance for transfer', + 'ERC1155InsufficientBalance', + [tokenHolder, mintAmount, mintAmount.addn(1), tokenId], ); }); @@ -150,28 +160,32 @@ contract('ERC1155', function (accounts) { describe('_burnBatch', function () { it("reverts when burning the zero account's tokens", async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_burnBatch(ZERO_ADDRESS, tokenBatchIds, burnAmounts), - 'ERC1155: burn from the zero address', + 'ERC1155InvalidSender', + [ZERO_ADDRESS], ); }); it('reverts if length of inputs do not match', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds, burnAmounts.slice(1)), - 'ERC1155: ids and amounts length mismatch', + 'ERC1155InvalidArrayLength', + [tokenBatchIds.length, burnAmounts.length - 1], ); - await expectRevert( + await expectRevertCustomError( this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds.slice(1), burnAmounts), - 'ERC1155: ids and amounts length mismatch', + 'ERC1155InvalidArrayLength', + [tokenBatchIds.length - 1, burnAmounts.length], ); }); it('reverts when burning a non-existent token id', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds, burnAmounts), - 'ERC1155: insufficient balance for transfer', + 'ERC1155InsufficientBalance', + [tokenBatchHolder, 0, tokenBatchIds[0], burnAmounts[0]], ); }); diff --git a/test/token/ERC1155/extensions/ERC1155Burnable.test.js b/test/token/ERC1155/extensions/ERC1155Burnable.test.js index f80d9935ac6..65a2f95f4c1 100644 --- a/test/token/ERC1155/extensions/ERC1155Burnable.test.js +++ b/test/token/ERC1155/extensions/ERC1155Burnable.test.js @@ -1,7 +1,9 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); + const ERC1155Burnable = artifacts.require('$ERC1155Burnable'); contract('ERC1155Burnable', function (accounts) { @@ -34,9 +36,10 @@ contract('ERC1155Burnable', function (accounts) { }); it("unapproved accounts cannot burn the holder's tokens", async function () { - await expectRevert( + await expectRevertCustomError( this.token.burn(holder, tokenIds[0], amounts[0].subn(1), { from: other }), - 'ERC1155: caller is not token owner or approved', + 'ERC1155MissingApprovalForAll', + [other, holder], ); }); }); @@ -58,9 +61,10 @@ contract('ERC1155Burnable', function (accounts) { }); it("unapproved accounts cannot burn the holder's tokens", async function () { - await expectRevert( + await expectRevertCustomError( this.token.burnBatch(holder, tokenIds, [amounts[0].subn(1), amounts[1].subn(2)], { from: other }), - 'ERC1155: caller is not token owner or approved', + 'ERC1155MissingApprovalForAll', + [other, holder], ); }); }); diff --git a/test/token/ERC1155/extensions/ERC1155Pausable.test.js b/test/token/ERC1155/extensions/ERC1155Pausable.test.js index f4d5cedec57..b0ac54bdb74 100644 --- a/test/token/ERC1155/extensions/ERC1155Pausable.test.js +++ b/test/token/ERC1155/extensions/ERC1155Pausable.test.js @@ -1,6 +1,7 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC1155Pausable = artifacts.require('$ERC1155Pausable'); @@ -28,60 +29,64 @@ contract('ERC1155Pausable', function (accounts) { }); it('reverts when trying to safeTransferFrom from holder', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(holder, receiver, firstTokenId, firstTokenAmount, '0x', { from: holder }), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to safeTransferFrom from operator', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(holder, receiver, firstTokenId, firstTokenAmount, '0x', { from: operator }), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to safeBatchTransferFrom from holder', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom(holder, receiver, [firstTokenId], [firstTokenAmount], '0x', { from: holder }), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to safeBatchTransferFrom from operator', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom(holder, receiver, [firstTokenId], [firstTokenAmount], '0x', { from: operator, }), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to mint', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_mint(holder, secondTokenId, secondTokenAmount, '0x'), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to mintBatch', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_mintBatch(holder, [secondTokenId], [secondTokenAmount], '0x'), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to burn', async function () { - await expectRevert( - this.token.$_burn(holder, firstTokenId, firstTokenAmount), - 'ERC1155Pausable: token transfer while paused', - ); + await expectRevertCustomError(this.token.$_burn(holder, firstTokenId, firstTokenAmount), 'EnforcedPause', []); }); it('reverts when trying to burnBatch', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_burnBatch(holder, [firstTokenId], [firstTokenAmount]), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); diff --git a/test/token/ERC1155/utils/ERC1155Holder.test.js b/test/token/ERC1155/utils/ERC1155Holder.test.js index 864e89b5049..8d85416409e 100644 --- a/test/token/ERC1155/utils/ERC1155Holder.test.js +++ b/test/token/ERC1155/utils/ERC1155Holder.test.js @@ -1,6 +1,6 @@ const { BN } = require('@openzeppelin/test-helpers'); -const ERC1155Holder = artifacts.require('ERC1155Holder'); +const ERC1155Holder = artifacts.require('$ERC1155Holder'); const ERC1155 = artifacts.require('$ERC1155'); const { expect } = require('chai'); diff --git a/test/token/ERC20/ERC20.behavior.js b/test/token/ERC20/ERC20.behavior.js index 41e47f06528..bb2efda893c 100644 --- a/test/token/ERC20/ERC20.behavior.js +++ b/test/token/ERC20/ERC20.behavior.js @@ -1,8 +1,13 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { ZERO_ADDRESS, MAX_UINT256 } = constants; -function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipient, anotherAccount) { +const { expectRevertCustomError } = require('../../helpers/customError'); + +function shouldBehaveLikeERC20(initialSupply, accounts, opts = {}) { + const [initialHolder, recipient, anotherAccount] = accounts; + const { forcedApproval } = opts; + describe('total supply', function () { it('returns the total amount of tokens', async function () { expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply); @@ -24,7 +29,7 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi }); describe('transfer', function () { - shouldBehaveLikeERC20Transfer(errorPrefix, initialHolder, recipient, initialSupply, function (from, to, value) { + shouldBehaveLikeERC20Transfer(initialHolder, recipient, initialSupply, function (from, to, value) { return this.token.transfer(to, value, { from }); }); }); @@ -68,13 +73,22 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi }); }); - it('emits an approval event', async function () { - expectEvent(await this.token.transferFrom(tokenOwner, to, amount, { from: spender }), 'Approval', { - owner: tokenOwner, - spender: spender, - value: await this.token.allowance(tokenOwner, spender), + if (forcedApproval) { + it('emits an approval event', async function () { + expectEvent(await this.token.transferFrom(tokenOwner, to, amount, { from: spender }), 'Approval', { + owner: tokenOwner, + spender: spender, + value: await this.token.allowance(tokenOwner, spender), + }); }); - }); + } else { + it('does not emit an approval event', async function () { + expectEvent.notEmitted( + await this.token.transferFrom(tokenOwner, to, amount, { from: spender }), + 'Approval', + ); + }); + } }); describe('when the token owner does not have enough balance', function () { @@ -85,9 +99,10 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(tokenOwner, to, amount, { from: spender }), - `${errorPrefix}: transfer amount exceeds balance`, + 'ERC20InsufficientBalance', + [tokenOwner, amount - 1, amount], ); }); }); @@ -104,9 +119,10 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi const amount = initialSupply; it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(tokenOwner, to, amount, { from: spender }), - `${errorPrefix}: insufficient allowance`, + 'ERC20InsufficientAllowance', + [spender, allowance, amount], ); }); }); @@ -119,9 +135,10 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(tokenOwner, to, amount, { from: spender }), - `${errorPrefix}: transfer amount exceeds balance`, + 'ERC20InsufficientBalance', + [tokenOwner, amount - 1, amount], ); }); }); @@ -153,9 +170,10 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(tokenOwner, to, amount, { from: spender }), - `${errorPrefix}: transfer to the zero address`, + 'ERC20InvalidReceiver', + [ZERO_ADDRESS], ); }); }); @@ -167,31 +185,33 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi const to = recipient; it('reverts', async function () { - await expectRevert(this.token.transferFrom(tokenOwner, to, amount, { from: spender }), 'from the zero address'); + await expectRevertCustomError( + this.token.transferFrom(tokenOwner, to, amount, { from: spender }), + 'ERC20InvalidApprover', + [ZERO_ADDRESS], + ); }); }); }); describe('approve', function () { - shouldBehaveLikeERC20Approve( - errorPrefix, - initialHolder, - recipient, - initialSupply, - function (owner, spender, amount) { - return this.token.approve(spender, amount, { from: owner }); - }, - ); + shouldBehaveLikeERC20Approve(initialHolder, recipient, initialSupply, function (owner, spender, amount) { + return this.token.approve(spender, amount, { from: owner }); + }); }); } -function shouldBehaveLikeERC20Transfer(errorPrefix, from, to, balance, transfer) { +function shouldBehaveLikeERC20Transfer(from, to, balance, transfer) { describe('when the recipient is not the zero address', function () { describe('when the sender does not have enough balance', function () { const amount = balance.addn(1); it('reverts', async function () { - await expectRevert(transfer.call(this, from, to, amount), `${errorPrefix}: transfer amount exceeds balance`); + await expectRevertCustomError(transfer.call(this, from, to, amount), 'ERC20InsufficientBalance', [ + from, + balance, + amount, + ]); }); }); @@ -230,15 +250,14 @@ function shouldBehaveLikeERC20Transfer(errorPrefix, from, to, balance, transfer) describe('when the recipient is the zero address', function () { it('reverts', async function () { - await expectRevert( - transfer.call(this, from, ZERO_ADDRESS, balance), - `${errorPrefix}: transfer to the zero address`, - ); + await expectRevertCustomError(transfer.call(this, from, ZERO_ADDRESS, balance), 'ERC20InvalidReceiver', [ + ZERO_ADDRESS, + ]); }); }); } -function shouldBehaveLikeERC20Approve(errorPrefix, owner, spender, supply, approve) { +function shouldBehaveLikeERC20Approve(owner, spender, supply, approve) { describe('when the spender is not the zero address', function () { describe('when the sender has enough balance', function () { const amount = supply; @@ -307,10 +326,9 @@ function shouldBehaveLikeERC20Approve(errorPrefix, owner, spender, supply, appro describe('when the spender is the zero address', function () { it('reverts', async function () { - await expectRevert( - approve.call(this, owner, ZERO_ADDRESS, supply), - `${errorPrefix}: approve to the zero address`, - ); + await expectRevertCustomError(approve.call(this, owner, ZERO_ADDRESS, supply), `ERC20InvalidSpender`, [ + ZERO_ADDRESS, + ]); }); }); } diff --git a/test/token/ERC20/ERC20.test.js b/test/token/ERC20/ERC20.test.js index c291975780f..ef6d82f2bd6 100644 --- a/test/token/ERC20/ERC20.test.js +++ b/test/token/ERC20/ERC20.test.js @@ -7,350 +7,359 @@ const { shouldBehaveLikeERC20Transfer, shouldBehaveLikeERC20Approve, } = require('./ERC20.behavior'); +const { expectRevertCustomError } = require('../../helpers/customError'); -const ERC20 = artifacts.require('$ERC20'); -const ERC20Decimals = artifacts.require('$ERC20DecimalsMock'); +const TOKENS = [ + { Token: artifacts.require('$ERC20') }, + { Token: artifacts.require('$ERC20ApprovalMock'), forcedApproval: true }, +]; contract('ERC20', function (accounts) { - const [initialHolder, recipient, anotherAccount] = accounts; + const [initialHolder, recipient] = accounts; const name = 'My Token'; const symbol = 'MTKN'; - const initialSupply = new BN(100); - beforeEach(async function () { - this.token = await ERC20.new(name, symbol); - await this.token.$_mint(initialHolder, initialSupply); - }); - - it('has a name', async function () { - expect(await this.token.name()).to.equal(name); - }); - - it('has a symbol', async function () { - expect(await this.token.symbol()).to.equal(symbol); - }); - - it('has 18 decimals', async function () { - expect(await this.token.decimals()).to.be.bignumber.equal('18'); - }); - - describe('set decimals', function () { - const decimals = new BN(6); - - it('can set decimals during construction', async function () { - const token = await ERC20Decimals.new(name, symbol, decimals); - expect(await token.decimals()).to.be.bignumber.equal(decimals); - }); - }); - - shouldBehaveLikeERC20('ERC20', initialSupply, initialHolder, recipient, anotherAccount); + for (const { Token, forcedApproval } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + this.token = await Token.new(name, symbol); + await this.token.$_mint(initialHolder, initialSupply); + }); - describe('decrease allowance', function () { - describe('when the spender is not the zero address', function () { - const spender = recipient; + shouldBehaveLikeERC20(initialSupply, accounts, { forcedApproval }); - function shouldDecreaseApproval(amount) { - describe('when there was no approved amount before', function () { - it('reverts', async function () { - await expectRevert( - this.token.decreaseAllowance(spender, amount, { from: initialHolder }), - 'ERC20: decreased allowance below zero', - ); - }); - }); + it('has a name', async function () { + expect(await this.token.name()).to.equal(name); + }); - describe('when the spender had an approved amount', function () { - const approvedAmount = amount; + it('has a symbol', async function () { + expect(await this.token.symbol()).to.equal(symbol); + }); - beforeEach(async function () { - await this.token.approve(spender, approvedAmount, { from: initialHolder }); - }); + it('has 18 decimals', async function () { + expect(await this.token.decimals()).to.be.bignumber.equal('18'); + }); - it('emits an approval event', async function () { - expectEvent( - await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder }), - 'Approval', - { owner: initialHolder, spender: spender, value: new BN(0) }, - ); + describe('decrease allowance', function () { + describe('when the spender is not the zero address', function () { + const spender = recipient; + + function shouldDecreaseApproval(amount) { + describe('when there was no approved amount before', function () { + it('reverts', async function () { + const allowance = await this.token.allowance(initialHolder, spender); + await expectRevertCustomError( + this.token.decreaseAllowance(spender, amount, { from: initialHolder }), + 'ERC20FailedDecreaseAllowance', + [spender, allowance, amount], + ); + }); + }); + + describe('when the spender had an approved amount', function () { + const approvedAmount = amount; + + beforeEach(async function () { + await this.token.approve(spender, approvedAmount, { from: initialHolder }); + }); + + it('emits an approval event', async function () { + expectEvent( + await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder }), + 'Approval', + { owner: initialHolder, spender: spender, value: new BN(0) }, + ); + }); + + it('decreases the spender allowance subtracting the requested amount', async function () { + await this.token.decreaseAllowance(spender, approvedAmount.subn(1), { from: initialHolder }); + + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('1'); + }); + + it('sets the allowance to zero when all allowance is removed', async function () { + await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder }); + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('0'); + }); + + it('reverts when more than the full allowance is removed', async function () { + await expectRevertCustomError( + this.token.decreaseAllowance(spender, approvedAmount.addn(1), { from: initialHolder }), + 'ERC20FailedDecreaseAllowance', + [spender, approvedAmount, approvedAmount.addn(1)], + ); + }); + }); + } + + describe('when the sender has enough balance', function () { + const amount = initialSupply; + + shouldDecreaseApproval(amount); }); - it('decreases the spender allowance subtracting the requested amount', async function () { - await this.token.decreaseAllowance(spender, approvedAmount.subn(1), { from: initialHolder }); + describe('when the sender does not have enough balance', function () { + const amount = initialSupply.addn(1); - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('1'); + shouldDecreaseApproval(amount); }); + }); - it('sets the allowance to zero when all allowance is removed', async function () { - await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder }); - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('0'); - }); + describe('when the spender is the zero address', function () { + const amount = initialSupply; + const spender = ZERO_ADDRESS; - it('reverts when more than the full allowance is removed', async function () { - await expectRevert( - this.token.decreaseAllowance(spender, approvedAmount.addn(1), { from: initialHolder }), - 'ERC20: decreased allowance below zero', + it('reverts', async function () { + await expectRevertCustomError( + this.token.decreaseAllowance(spender, amount, { from: initialHolder }), + 'ERC20FailedDecreaseAllowance', + [spender, 0, amount], ); }); }); - } - - describe('when the sender has enough balance', function () { - const amount = initialSupply; - - shouldDecreaseApproval(amount); }); - describe('when the sender does not have enough balance', function () { - const amount = initialSupply.addn(1); + describe('increase allowance', function () { + const amount = initialSupply; - shouldDecreaseApproval(amount); - }); - }); + describe('when the spender is not the zero address', function () { + const spender = recipient; + + describe('when the sender has enough balance', function () { + it('emits an approval event', async function () { + expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', { + owner: initialHolder, + spender: spender, + value: amount, + }); + }); + + describe('when there was no approved amount before', function () { + it('approves the requested amount', async function () { + await this.token.increaseAllowance(spender, amount, { from: initialHolder }); + + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount); + }); + }); + + describe('when the spender had an approved amount', function () { + beforeEach(async function () { + await this.token.approve(spender, new BN(1), { from: initialHolder }); + }); + + it('increases the spender allowance adding the requested amount', async function () { + await this.token.increaseAllowance(spender, amount, { from: initialHolder }); + + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1)); + }); + }); + }); - describe('when the spender is the zero address', function () { - const amount = initialSupply; - const spender = ZERO_ADDRESS; + describe('when the sender does not have enough balance', function () { + const amount = initialSupply.addn(1); - it('reverts', async function () { - await expectRevert( - this.token.decreaseAllowance(spender, amount, { from: initialHolder }), - 'ERC20: decreased allowance below zero', - ); - }); - }); - }); + it('emits an approval event', async function () { + expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', { + owner: initialHolder, + spender: spender, + value: amount, + }); + }); - describe('increase allowance', function () { - const amount = initialSupply; + describe('when there was no approved amount before', function () { + it('approves the requested amount', async function () { + await this.token.increaseAllowance(spender, amount, { from: initialHolder }); - describe('when the spender is not the zero address', function () { - const spender = recipient; + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount); + }); + }); - describe('when the sender has enough balance', function () { - it('emits an approval event', async function () { - expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', { - owner: initialHolder, - spender: spender, - value: amount, - }); - }); + describe('when the spender had an approved amount', function () { + beforeEach(async function () { + await this.token.approve(spender, new BN(1), { from: initialHolder }); + }); - describe('when there was no approved amount before', function () { - it('approves the requested amount', async function () { - await this.token.increaseAllowance(spender, amount, { from: initialHolder }); + it('increases the spender allowance adding the requested amount', async function () { + await this.token.increaseAllowance(spender, amount, { from: initialHolder }); - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount); + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1)); + }); + }); }); }); - describe('when the spender had an approved amount', function () { - beforeEach(async function () { - await this.token.approve(spender, new BN(1), { from: initialHolder }); - }); - - it('increases the spender allowance adding the requested amount', async function () { - await this.token.increaseAllowance(spender, amount, { from: initialHolder }); + describe('when the spender is the zero address', function () { + const spender = ZERO_ADDRESS; - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1)); + it('reverts', async function () { + await expectRevertCustomError( + this.token.increaseAllowance(spender, amount, { from: initialHolder }), + 'ERC20InvalidSpender', + [ZERO_ADDRESS], + ); }); }); }); - describe('when the sender does not have enough balance', function () { - const amount = initialSupply.addn(1); + describe('_mint', function () { + const amount = new BN(50); + it('rejects a null account', async function () { + await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, amount), 'ERC20InvalidReceiver', [ + ZERO_ADDRESS, + ]); + }); - it('emits an approval event', async function () { - expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', { - owner: initialHolder, - spender: spender, - value: amount, - }); + 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)', + ); }); - describe('when there was no approved amount before', function () { - it('approves the requested amount', async function () { - await this.token.increaseAllowance(spender, amount, { from: initialHolder }); + describe('for a non zero account', function () { + beforeEach('minting', async function () { + this.receipt = await this.token.$_mint(recipient, amount); + }); - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount); + it('increments totalSupply', async function () { + const expectedSupply = initialSupply.add(amount); + expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply); }); - }); - describe('when the spender had an approved amount', function () { - beforeEach(async function () { - await this.token.approve(spender, new BN(1), { from: initialHolder }); + it('increments recipient balance', async function () { + expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(amount); }); - it('increases the spender allowance adding the requested amount', async function () { - await this.token.increaseAllowance(spender, amount, { from: initialHolder }); + it('emits Transfer event', async function () { + const event = expectEvent(this.receipt, 'Transfer', { from: ZERO_ADDRESS, to: recipient }); - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1)); + expect(event.args.value).to.be.bignumber.equal(amount); }); }); }); - }); - describe('when the spender is the zero address', function () { - const spender = ZERO_ADDRESS; - - it('reverts', async function () { - await expectRevert( - this.token.increaseAllowance(spender, amount, { from: initialHolder }), - 'ERC20: approve to the zero address', - ); - }); - }); - }); - - describe('_mint', function () { - const amount = new BN(50); - it('rejects a null account', async function () { - await expectRevert(this.token.$_mint(ZERO_ADDRESS, amount), 'ERC20: mint to the zero address'); - }); - - 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)', - ); - }); + describe('_burn', function () { + it('rejects a null account', async function () { + await expectRevertCustomError(this.token.$_burn(ZERO_ADDRESS, new BN(1)), 'ERC20InvalidSender', [ + ZERO_ADDRESS, + ]); + }); - describe('for a non zero account', function () { - beforeEach('minting', async function () { - this.receipt = await this.token.$_mint(recipient, amount); - }); + 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)], + ); + }); - it('increments totalSupply', async function () { - const expectedSupply = initialSupply.add(amount); - expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply); - }); + const describeBurn = function (description, amount) { + describe(description, function () { + beforeEach('burning', async function () { + this.receipt = await this.token.$_burn(initialHolder, amount); + }); - it('increments recipient balance', async function () { - expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(amount); - }); + it('decrements totalSupply', async function () { + const expectedSupply = initialSupply.sub(amount); + expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply); + }); - it('emits Transfer event', async function () { - const event = expectEvent(this.receipt, 'Transfer', { from: ZERO_ADDRESS, to: recipient }); + it('decrements initialHolder balance', async function () { + const expectedBalance = initialSupply.sub(amount); + expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(expectedBalance); + }); - expect(event.args.value).to.be.bignumber.equal(amount); - }); - }); - }); + it('emits Transfer event', async function () { + const event = expectEvent(this.receipt, 'Transfer', { from: initialHolder, to: ZERO_ADDRESS }); - describe('_burn', function () { - it('rejects a null account', async function () { - await expectRevert(this.token.$_burn(ZERO_ADDRESS, new BN(1)), 'ERC20: burn from the zero address'); - }); + expect(event.args.value).to.be.bignumber.equal(amount); + }); + }); + }; - describe('for a non zero account', function () { - it('rejects burning more than balance', async function () { - await expectRevert( - this.token.$_burn(initialHolder, initialSupply.addn(1)), - 'ERC20: transfer amount exceeds balance', - ); + describeBurn('for entire balance', initialSupply); + describeBurn('for less amount than balance', initialSupply.subn(1)); + }); }); - const describeBurn = function (description, amount) { - describe(description, function () { - beforeEach('burning', async function () { - this.receipt = await this.token.$_burn(initialHolder, amount); - }); + describe('_update', function () { + const amount = new BN(1); - it('decrements totalSupply', async function () { - const expectedSupply = initialSupply.sub(amount); - expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply); - }); + it('from is the zero address', async function () { + const balanceBefore = await this.token.balanceOf(initialHolder); + const totalSupply = await this.token.totalSupply(); - it('decrements initialHolder balance', async function () { - const expectedBalance = initialSupply.sub(amount); - expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(expectedBalance); + expectEvent(await this.token.$_update(ZERO_ADDRESS, initialHolder, amount), 'Transfer', { + from: ZERO_ADDRESS, + to: initialHolder, + value: amount, }); + expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.add(amount)); + expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.add(amount)); + }); - it('emits Transfer event', async function () { - const event = expectEvent(this.receipt, 'Transfer', { from: initialHolder, to: ZERO_ADDRESS }); + it('to is the zero address', async function () { + const balanceBefore = await this.token.balanceOf(initialHolder); + const totalSupply = await this.token.totalSupply(); - expect(event.args.value).to.be.bignumber.equal(amount); + expectEvent(await this.token.$_update(initialHolder, ZERO_ADDRESS, amount), 'Transfer', { + from: initialHolder, + to: ZERO_ADDRESS, + value: amount, }); + expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.sub(amount)); + expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.sub(amount)); }); - }; - - describeBurn('for entire balance', initialSupply); - describeBurn('for less amount than balance', initialSupply.subn(1)); - }); - }); - - describe('_update', function () { - const amount = new BN(1); - it('from is the zero address', async function () { - const balanceBefore = await this.token.balanceOf(initialHolder); - const totalSupply = await this.token.totalSupply(); + it('from and to are the zero address', async function () { + const totalSupply = await this.token.totalSupply(); - expectEvent(await this.token.$_update(ZERO_ADDRESS, initialHolder, amount), 'Transfer', { - from: ZERO_ADDRESS, - to: initialHolder, - value: amount, - }); - expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.add(amount)); - expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.add(amount)); - }); - - it('to is the zero address', async function () { - const balanceBefore = await this.token.balanceOf(initialHolder); - const totalSupply = await this.token.totalSupply(); - - expectEvent(await this.token.$_update(initialHolder, ZERO_ADDRESS, amount), 'Transfer', { - from: initialHolder, - to: ZERO_ADDRESS, - value: amount, - }); - expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.sub(amount)); - expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.sub(amount)); - }); - - it('from and to are the zero address', async function () { - const totalSupply = await this.token.totalSupply(); + await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, amount); - await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, amount); - - expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply); - expectEvent(await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, amount), 'Transfer', { - from: ZERO_ADDRESS, - to: ZERO_ADDRESS, - value: amount, + expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply); + expectEvent(await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, amount), 'Transfer', { + from: ZERO_ADDRESS, + to: ZERO_ADDRESS, + value: amount, + }); + }); }); - }); - }); - describe('_transfer', function () { - shouldBehaveLikeERC20Transfer('ERC20', initialHolder, recipient, initialSupply, function (from, to, amount) { - return this.token.$_transfer(from, to, amount); - }); + describe('_transfer', function () { + shouldBehaveLikeERC20Transfer(initialHolder, recipient, initialSupply, function (from, to, amount) { + return this.token.$_transfer(from, to, amount); + }); - describe('when the sender is the zero address', function () { - it('reverts', async function () { - await expectRevert( - this.token.$_transfer(ZERO_ADDRESS, recipient, initialSupply), - 'ERC20: transfer from the zero address', - ); + 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], + ); + }); + }); }); - }); - }); - describe('_approve', function () { - shouldBehaveLikeERC20Approve('ERC20', initialHolder, recipient, initialSupply, function (owner, spender, amount) { - return this.token.$_approve(owner, spender, amount); - }); + describe('_approve', function () { + shouldBehaveLikeERC20Approve(initialHolder, recipient, initialSupply, function (owner, spender, amount) { + return this.token.$_approve(owner, spender, amount); + }); - describe('when the owner is the zero address', function () { - it('reverts', async function () { - await expectRevert( - this.token.$_approve(ZERO_ADDRESS, recipient, initialSupply), - 'ERC20: approve from the zero address', - ); + 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], + ); + }); + }); }); }); - }); + } }); diff --git a/test/token/ERC20/extensions/ERC20Burnable.behavior.js b/test/token/ERC20/extensions/ERC20Burnable.behavior.js index 448dda4abc5..848e54b7986 100644 --- a/test/token/ERC20/extensions/ERC20Burnable.behavior.js +++ b/test/token/ERC20/extensions/ERC20Burnable.behavior.js @@ -1,7 +1,8 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +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 () { @@ -37,7 +38,11 @@ function shouldBehaveLikeERC20Burnable(owner, initialBalance, [burner]) { const amount = initialBalance.addn(1); it('reverts', async function () { - await expectRevert(this.token.burn(amount, { from: owner }), 'ERC20: transfer amount exceeds balance'); + await expectRevertCustomError(this.token.burn(amount, { from: owner }), 'ERC20InsufficientBalance', [ + owner, + initialBalance, + amount, + ]); }); }); }); @@ -83,9 +88,10 @@ function shouldBehaveLikeERC20Burnable(owner, initialBalance, [burner]) { it('reverts', async function () { await this.token.approve(burner, amount, { from: owner }); - await expectRevert( + await expectRevertCustomError( this.token.burnFrom(owner, amount, { from: burner }), - 'ERC20: transfer amount exceeds balance', + 'ERC20InsufficientBalance', + [owner, initialBalance, amount], ); }); }); @@ -95,9 +101,10 @@ function shouldBehaveLikeERC20Burnable(owner, initialBalance, [burner]) { it('reverts', async function () { await this.token.approve(burner, allowance, { from: owner }); - await expectRevert( + await expectRevertCustomError( this.token.burnFrom(owner, allowance.addn(1), { from: burner }), - 'ERC20: insufficient allowance', + 'ERC20InsufficientAllowance', + [burner, allowance, allowance.addn(1)], ); }); }); diff --git a/test/token/ERC20/extensions/ERC20Capped.behavior.js b/test/token/ERC20/extensions/ERC20Capped.behavior.js index 97bad1db192..c40e4fcc476 100644 --- a/test/token/ERC20/extensions/ERC20Capped.behavior.js +++ b/test/token/ERC20/extensions/ERC20Capped.behavior.js @@ -1,6 +1,5 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); - const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); function shouldBehaveLikeERC20Capped(accounts, cap) { describe('capped token', function () { @@ -17,12 +16,12 @@ function shouldBehaveLikeERC20Capped(accounts, cap) { it('fails to mint if the amount exceeds the cap', async function () { await this.token.$_mint(user, cap.subn(1)); - await expectRevert(this.token.$_mint(user, 2), 'ERC20Capped: cap exceeded'); + 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 expectRevert(this.token.$_mint(user, 1), 'ERC20Capped: cap exceeded'); + await expectRevertCustomError(this.token.$_mint(user, 1), 'ERC20ExceededCap', [cap.addn(1), cap]); }); }); } diff --git a/test/token/ERC20/extensions/ERC20Capped.test.js b/test/token/ERC20/extensions/ERC20Capped.test.js index a86d38c1abe..1f4a2bee3bc 100644 --- a/test/token/ERC20/extensions/ERC20Capped.test.js +++ b/test/token/ERC20/extensions/ERC20Capped.test.js @@ -1,5 +1,6 @@ -const { ether, expectRevert } = require('@openzeppelin/test-helpers'); +const { ether } = require('@openzeppelin/test-helpers'); const { shouldBehaveLikeERC20Capped } = require('./ERC20Capped.behavior'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC20Capped = artifacts.require('$ERC20Capped'); @@ -10,7 +11,7 @@ contract('ERC20Capped', function (accounts) { const symbol = 'MTKN'; it('requires a non-zero cap', async function () { - await expectRevert(ERC20Capped.new(name, symbol, 0), 'ERC20Capped: cap is 0'); + await expectRevertCustomError(ERC20Capped.new(name, symbol, 0), 'ERC20InvalidCap', [0]); }); context('once deployed', async function () { diff --git a/test/token/ERC20/extensions/ERC20FlashMint.test.js b/test/token/ERC20/extensions/ERC20FlashMint.test.js index ee9bedd2667..a646704e2d8 100644 --- a/test/token/ERC20/extensions/ERC20FlashMint.test.js +++ b/test/token/ERC20/extensions/ERC20FlashMint.test.js @@ -2,6 +2,7 @@ const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const { MAX_UINT256, ZERO_ADDRESS } = constants; const ERC20FlashMintMock = artifacts.require('$ERC20FlashMintMock'); @@ -37,7 +38,9 @@ contract('ERC20FlashMint', function (accounts) { }); it('token mismatch', async function () { - await expectRevert(this.token.flashFee(ZERO_ADDRESS, loanAmount), 'ERC20FlashMint: wrong token'); + await expectRevertCustomError(this.token.flashFee(ZERO_ADDRESS, loanAmount), 'ERC3156UnsupportedToken', [ + ZERO_ADDRESS, + ]); }); }); @@ -79,26 +82,29 @@ contract('ERC20FlashMint', function (accounts) { it('missing return value', async function () { const receiver = await ERC3156FlashBorrowerMock.new(false, true); - await expectRevert( + await expectRevertCustomError( this.token.flashLoan(receiver.address, this.token.address, loanAmount, '0x'), - 'ERC20FlashMint: invalid return value', + 'ERC3156InvalidReceiver', + [receiver.address], ); }); it('missing approval', async function () { const receiver = await ERC3156FlashBorrowerMock.new(true, false); - await expectRevert( + await expectRevertCustomError( this.token.flashLoan(receiver.address, this.token.address, loanAmount, '0x'), - 'ERC20: insufficient allowance', + 'ERC20InsufficientAllowance', + [this.token.address, 0, loanAmount], ); }); it('unavailable funds', async function () { const receiver = await ERC3156FlashBorrowerMock.new(true, true); const data = this.token.contract.methods.transfer(other, 10).encodeABI(); - await expectRevert( + await expectRevertCustomError( this.token.flashLoan(receiver.address, this.token.address, loanAmount, data), - 'ERC20: transfer amount exceeds balance', + 'ERC20InsufficientBalance', + [receiver.address, loanAmount - 10, loanAmount], ); }); diff --git a/test/token/ERC20/extensions/ERC20Pausable.test.js b/test/token/ERC20/extensions/ERC20Pausable.test.js index ead442b9929..72bfc776901 100644 --- a/test/token/ERC20/extensions/ERC20Pausable.test.js +++ b/test/token/ERC20/extensions/ERC20Pausable.test.js @@ -1,6 +1,7 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC20Pausable = artifacts.require('$ERC20Pausable'); @@ -39,9 +40,10 @@ contract('ERC20Pausable', function (accounts) { it('reverts when trying to transfer when paused', async function () { await this.token.$_pause(); - await expectRevert( + await expectRevertCustomError( this.token.transfer(recipient, initialSupply, { from: holder }), - 'ERC20Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); }); @@ -73,9 +75,10 @@ contract('ERC20Pausable', function (accounts) { it('reverts when trying to transfer from when paused', async function () { await this.token.$_pause(); - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(holder, recipient, allowance, { from: anotherAccount }), - 'ERC20Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); }); @@ -101,7 +104,7 @@ contract('ERC20Pausable', function (accounts) { it('reverts when trying to mint when paused', async function () { await this.token.$_pause(); - await expectRevert(this.token.$_mint(recipient, amount), 'ERC20Pausable: token transfer while paused'); + await expectRevertCustomError(this.token.$_mint(recipient, amount), 'EnforcedPause', []); }); }); @@ -126,7 +129,7 @@ contract('ERC20Pausable', function (accounts) { it('reverts when trying to burn when paused', async function () { await this.token.$_pause(); - await expectRevert(this.token.$_burn(holder, amount), 'ERC20Pausable: token transfer while paused'); + await expectRevertCustomError(this.token.$_burn(holder, amount), 'EnforcedPause', []); }); }); }); diff --git a/test/token/ERC20/extensions/draft-ERC20Permit.test.js b/test/token/ERC20/extensions/ERC20Permit.test.js similarity index 73% rename from test/token/ERC20/extensions/draft-ERC20Permit.test.js rename to test/token/ERC20/extensions/ERC20Permit.test.js index 33c43c479fd..388716d534e 100644 --- a/test/token/ERC20/extensions/draft-ERC20Permit.test.js +++ b/test/token/ERC20/extensions/ERC20Permit.test.js @@ -1,6 +1,6 @@ /* eslint-disable */ -const { BN, constants, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { BN, constants, time } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { MAX_UINT256 } = constants; @@ -12,13 +12,13 @@ const ERC20Permit = artifacts.require('$ERC20Permit'); const { Permit, getDomain, domainType, domainSeparator } = require('../../../helpers/eip712'); const { getChainId } = require('../../../helpers/chainid'); +const { expectRevertCustomError } = require('../../../helpers/customError'); contract('ERC20Permit', function (accounts) { const [initialHolder, spender] = accounts; const name = 'My Token'; const symbol = 'MTKN'; - const version = '1'; const initialSupply = new BN(100); @@ -65,15 +65,25 @@ contract('ERC20Permit', function (accounts) { }); it('rejects reused signature', async function () { - const { v, r, s } = await buildData(this.token) - .then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data })) - .then(fromRpcSig); + 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); - await expectRevert( + 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), - 'ERC20Permit: invalid signature', + 'ERC2612InvalidSigner', + [ethSigUtil.recoverTypedSignature({ data: typedMessage, sig }), owner], ); }); @@ -84,9 +94,10 @@ contract('ERC20Permit', function (accounts) { .then(data => ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data })) .then(fromRpcSig); - await expectRevert( + await expectRevertCustomError( this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC20Permit: invalid signature', + 'ERC2612InvalidSigner', + [await otherWallet.getAddressString(), owner], ); }); @@ -97,7 +108,11 @@ contract('ERC20Permit', function (accounts) { .then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data })) .then(fromRpcSig); - await expectRevert(this.token.permit(owner, spender, value, deadline, v, r, s), 'ERC20Permit: expired deadline'); + await expectRevertCustomError( + this.token.permit(owner, spender, value, deadline, v, r, s), + 'ERC2612ExpiredSignature', + [deadline], + ); }); }); }); diff --git a/test/token/ERC20/extensions/ERC20Votes.test.js b/test/token/ERC20/extensions/ERC20Votes.test.js index e4ff58cd956..714a98adcf6 100644 --- a/test/token/ERC20/extensions/ERC20Votes.test.js +++ b/test/token/ERC20/extensions/ERC20Votes.test.js @@ -1,6 +1,6 @@ /* eslint-disable */ -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent, time } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { MAX_UINT256, ZERO_ADDRESS } = constants; @@ -12,6 +12,7 @@ const Wallet = require('ethereumjs-wallet').default; const { batchInBlock } = require('../../../helpers/txpool'); const { getDomain, domainType, domainSeparator } = require('../../../helpers/eip712'); const { clock, clockFromReceipt } = require('../../../helpers/time'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -48,7 +49,10 @@ contract('ERC20Votes', function (accounts) { it('minting restriction', async function () { const amount = new BN('2').pow(new BN('224')); - await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes'); + await expectRevertCustomError(this.token.$_mint(holder, amount), 'ERC20ExceededSafeSupply', [ + amount, + amount.subn(1), + ]); }); it('recent checkpoints', async function () { @@ -164,9 +168,10 @@ contract('ERC20Votes', function (accounts) { await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - await expectRevert( + await expectRevertCustomError( this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), - 'Votes: invalid nonce', + 'InvalidAccountNonce', + [delegatorAddress, nonce + 1], ); }); @@ -185,15 +190,25 @@ contract('ERC20Votes', function (accounts) { }); it('rejects bad nonce', async function () { - const { v, r, s } = await buildData(this.token, { + const sig = await buildData(this.token, { delegatee: delegatorAddress, nonce, expiry: MAX_UINT256, - }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); + }).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 expectRevert( + await expectRevertCustomError( this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), - 'Votes: invalid nonce', + 'InvalidAccountNonce', + [ethSigUtil.recoverTypedSignature({ data: typedMessage, sig }), nonce], ); }); @@ -205,9 +220,10 @@ contract('ERC20Votes', function (accounts) { expiry, }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - await expectRevert( + await expectRevertCustomError( this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), - 'Votes: signature expired', + 'VotesExpiredSignature', + [expiry], ); }); }); @@ -414,7 +430,8 @@ contract('ERC20Votes', function (accounts) { describe('getPastVotes', function () { it('reverts if block number >= current block', async function () { - await expectRevert(this.token.getPastVotes(other1, 5e10), 'Votes: future lookup'); + const clock = await this.token.clock(); + await expectRevertCustomError(this.token.getPastVotes(other1, 5e10), 'ERC5805FutureLookup', [5e10, clock]); }); it('returns 0 if there are no checkpoints', async function () { @@ -502,7 +519,8 @@ contract('ERC20Votes', function (accounts) { }); it('reverts if block number >= current block', async function () { - await expectRevert(this.token.getPastTotalSupply(5e10), 'Votes: future lookup'); + const clock = await this.token.clock(); + await expectRevertCustomError(this.token.getPastTotalSupply(5e10), 'ERC5805FutureLookup', [5e10, clock]); }); it('returns 0 if there are no checkpoints', async function () { diff --git a/test/token/ERC20/extensions/ERC20Wrapper.test.js b/test/token/ERC20/extensions/ERC20Wrapper.test.js index 774a9cbda42..94415d088d9 100644 --- a/test/token/ERC20/extensions/ERC20Wrapper.test.js +++ b/test/token/ERC20/extensions/ERC20Wrapper.test.js @@ -1,15 +1,16 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { ZERO_ADDRESS, MAX_UINT256 } = constants; 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'); -contract('ERC20', function (accounts) { - const [initialHolder, recipient, anotherAccount] = accounts; +contract('ERC20Wrapper', function (accounts) { + const [initialHolder, receiver] = accounts; const name = 'My Token'; const symbol = 'MTKN'; @@ -66,23 +67,25 @@ contract('ERC20', function (accounts) { }); it('missing approval', async function () { - await expectRevert( + await expectRevertCustomError( this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }), - 'ERC20: insufficient allowance', + 'ERC20InsufficientAllowance', + [this.token.address, 0, initialSupply], ); }); it('missing balance', async function () { await this.underlying.approve(this.token.address, MAX_UINT256, { from: initialHolder }); - await expectRevert( + await expectRevertCustomError( this.token.depositFor(initialHolder, MAX_UINT256, { from: initialHolder }), - 'ERC20: transfer amount exceeds balance', + 'ERC20InsufficientBalance', + [initialHolder, initialSupply, MAX_UINT256], ); }); it('to other account', async function () { await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder }); - const { tx } = await this.token.depositFor(anotherAccount, 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, @@ -90,7 +93,7 @@ contract('ERC20', function (accounts) { }); await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: ZERO_ADDRESS, - to: anotherAccount, + to: receiver, value: initialSupply, }); }); @@ -103,9 +106,10 @@ contract('ERC20', function (accounts) { }); it('missing balance', async function () { - await expectRevert( + await expectRevertCustomError( this.token.withdrawTo(initialHolder, MAX_UINT256, { from: initialHolder }), - 'ERC20: transfer amount exceeds balance', + 'ERC20InsufficientBalance', + [initialHolder, initialSupply, MAX_UINT256], ); }); @@ -140,10 +144,10 @@ contract('ERC20', function (accounts) { }); it('to other account', async function () { - const { tx } = await this.token.withdrawTo(anotherAccount, initialSupply, { from: initialHolder }); + const { tx } = await this.token.withdrawTo(receiver, initialSupply, { from: initialHolder }); await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { from: this.token.address, - to: anotherAccount, + to: receiver, value: initialSupply, }); await expectEvent.inTransaction(tx, this.token, 'Transfer', { @@ -159,10 +163,10 @@ contract('ERC20', function (accounts) { await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder }); await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }); - const { tx } = await this.token.$_recover(anotherAccount); + const { tx } = await this.token.$_recover(receiver); await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: ZERO_ADDRESS, - to: anotherAccount, + to: receiver, value: '0', }); }); @@ -170,10 +174,10 @@ contract('ERC20', function (accounts) { it('something to recover', async function () { await this.underlying.transfer(this.token.address, initialSupply, { from: initialHolder }); - const { tx } = await this.token.$_recover(anotherAccount); + const { tx } = await this.token.$_recover(receiver); await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: ZERO_ADDRESS, - to: anotherAccount, + to: receiver, value: initialSupply, }); }); @@ -185,6 +189,6 @@ contract('ERC20', function (accounts) { await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }); }); - shouldBehaveLikeERC20('ERC20', initialSupply, initialHolder, recipient, anotherAccount); + shouldBehaveLikeERC20(initialSupply, accounts); }); }); diff --git a/test/token/ERC20/extensions/ERC4626.test.js b/test/token/ERC20/extensions/ERC4626.test.js index 55b3e5d2001..99d6009e4dc 100644 --- a/test/token/ERC20/extensions/ERC4626.test.js +++ b/test/token/ERC20/extensions/ERC4626.test.js @@ -2,9 +2,11 @@ const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-hel const { expect } = require('chai'); const { Enum } = require('../../../helpers/enums'); +const { expectRevertCustomError } = require('../../../helpers/customError'); 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'); @@ -219,6 +221,49 @@ contract('ERC4626', function (accounts) { }); }); + 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); + }); + + 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, + ]); + }); + + 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, + ]); + }); + + 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], + ); + }); + + 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], + ); + }); + }); + 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); @@ -635,9 +680,11 @@ contract('ERC4626', function (accounts) { }); it('withdraw with approval', async function () { - await expectRevert( + const assets = await this.vault.previewWithdraw(parseToken(1)); + await expectRevertCustomError( this.vault.withdraw(parseToken(1), recipient, holder, { from: other }), - 'ERC20: insufficient allowance', + 'ERC20InsufficientAllowance', + [other, 0, assets], ); await this.vault.withdraw(parseToken(1), recipient, holder, { from: spender }); @@ -677,9 +724,10 @@ contract('ERC4626', function (accounts) { }); it('redeem with approval', async function () { - await expectRevert( + await expectRevertCustomError( this.vault.redeem(parseShare(100), recipient, holder, { from: other }), - 'ERC20: insufficient allowance', + 'ERC20InsufficientAllowance', + [other, 0, parseShare(100)], ); await this.vault.redeem(parseShare(100), recipient, holder, { from: spender }); @@ -845,6 +893,9 @@ contract('ERC4626', function (accounts) { 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'); } @@ -868,6 +919,9 @@ contract('ERC4626', function (accounts) { 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'); } @@ -879,6 +933,9 @@ contract('ERC4626', function (accounts) { 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'); @@ -900,6 +957,9 @@ contract('ERC4626', function (accounts) { 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'); } @@ -924,6 +984,9 @@ contract('ERC4626', function (accounts) { 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 } @@ -936,6 +999,9 @@ contract('ERC4626', function (accounts) { 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 @@ -957,6 +1023,9 @@ contract('ERC4626', function (accounts) { 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'); } @@ -979,6 +1048,9 @@ contract('ERC4626', function (accounts) { 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'); } @@ -1002,6 +1074,9 @@ contract('ERC4626', function (accounts) { 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'); } @@ -1024,6 +1099,9 @@ contract('ERC4626', function (accounts) { 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 } diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index b0daf438412..eb6e267550b 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -8,6 +8,7 @@ const ERC20PermitNoRevertMock = artifacts.require('$ERC20PermitNoRevertMock'); const ERC20ForceApproveMock = artifacts.require('$ERC20ForceApproveMock'); const { getDomain, domainType, Permit } = require('../../../helpers/eip712'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const { fromRpcSig } = require('ethereumjs-util'); const ethSigUtil = require('eth-sig-util'); @@ -17,7 +18,7 @@ const name = 'ERC20Mock'; const symbol = 'ERC20Mock'; contract('SafeERC20', function (accounts) { - const [hasNoCode] = accounts; + const [hasNoCode, receiver, spender] = accounts; before(async function () { this.mock = await SafeERC20.new(); @@ -28,7 +29,35 @@ contract('SafeERC20', function (accounts) { this.token = { address: hasNoCode }; }); - shouldRevertOnAllCalls(accounts, 'Address: call to non-contract'); + it('reverts on transfer', async function () { + await expectRevertCustomError(this.mock.$safeTransfer(this.token.address, receiver, 0), 'AddressEmptyCode', [ + 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], + ); + }); + + 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)); + }); + + 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)); + }); + + it('reverts on forceApprove', async function () { + await expectRevertCustomError(this.mock.$forceApprove(this.token.address, spender, 0), 'AddressEmptyCode', [ + this.token.address, + ]); + }); }); describe('with token that returns false on all calls', function () { @@ -36,7 +65,45 @@ contract('SafeERC20', function (accounts) { this.token = await ERC20ReturnFalseMock.new(name, symbol); }); - shouldRevertOnAllCalls(accounts, 'SafeERC20: ERC20 operation did not succeed'); + it('reverts on transfer', async function () { + await expectRevertCustomError( + this.mock.$safeTransfer(this.token.address, receiver, 0), + 'SafeERC20FailedOperation', + [this.token.address], + ); + }); + + it('reverts on transferFrom', async function () { + await expectRevertCustomError( + this.mock.$safeTransferFrom(this.token.address, this.mock.address, receiver, 0), + 'SafeERC20FailedOperation', + [this.token.address], + ); + }); + + it('reverts on increaseAllowance', async function () { + await expectRevertCustomError( + this.mock.$safeIncreaseAllowance(this.token.address, spender, 0), + 'SafeERC20FailedOperation', + [this.token.address], + ); + }); + + it('reverts on decreaseAllowance', async function () { + await expectRevertCustomError( + this.mock.$safeDecreaseAllowance(this.token.address, spender, 0), + 'SafeERC20FailedOperation', + [this.token.address], + ); + }); + + it('reverts on forceApprove', async function () { + await expectRevertCustomError( + this.mock.$forceApprove(this.token.address, spender, 0), + 'SafeERC20FailedOperation', + [this.token.address], + ); + }); }); describe('with token that returns true on all calls', function () { @@ -118,7 +185,7 @@ contract('SafeERC20', function (accounts) { ); expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); // invalid call revert when called through the SafeERC20 library - await expectRevert( + await expectRevertCustomError( this.mock.$safePermit( this.token.address, this.data.message.owner, @@ -129,7 +196,8 @@ contract('SafeERC20', function (accounts) { this.signature.r, this.signature.s, ), - 'SafeERC20: permit did not succeed', + 'SafeERC20FailedOperation', + [this.token.address], ); expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); }); @@ -154,7 +222,7 @@ contract('SafeERC20', function (accounts) { ); // invalid call revert when called through the SafeERC20 library - await expectRevert( + await expectRevertCustomError( this.mock.$safePermit( this.token.address, this.data.message.owner, @@ -165,7 +233,8 @@ contract('SafeERC20', function (accounts) { invalidSignature.r, invalidSignature.s, ), - 'SafeERC20: permit did not succeed', + 'SafeERC20FailedOperation', + [this.token.address], ); }); }); @@ -200,30 +269,6 @@ contract('SafeERC20', function (accounts) { }); }); -function shouldRevertOnAllCalls([receiver, spender], reason) { - it('reverts on transfer', async function () { - await expectRevert(this.mock.$safeTransfer(this.token.address, receiver, 0), reason); - }); - - it('reverts on transferFrom', async function () { - await expectRevert(this.mock.$safeTransferFrom(this.token.address, this.mock.address, receiver, 0), reason); - }); - - it('reverts on increaseAllowance', async function () { - // [TODO] make sure it's reverting for the right reason - await expectRevert.unspecified(this.mock.$safeIncreaseAllowance(this.token.address, spender, 0)); - }); - - it('reverts on decreaseAllowance', async function () { - // [TODO] make sure it's reverting for the right reason - await expectRevert.unspecified(this.mock.$safeDecreaseAllowance(this.token.address, spender, 0)); - }); - - it('reverts on forceApprove', async function () { - await expectRevert(this.mock.$forceApprove(this.token.address, spender, 0), reason); - }); -} - function shouldOnlyRevertOnErrors([owner, receiver, spender]) { describe('transfers', function () { beforeEach(async function () { @@ -273,9 +318,10 @@ function shouldOnlyRevertOnErrors([owner, receiver, spender]) { }); it('reverts when decreasing the allowance', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.$safeDecreaseAllowance(this.token.address, spender, 10), - 'SafeERC20: decreased allowance below zero', + 'SafeERC20FailedDecreaseAllowance', + [spender, 0, 10], ); }); }); @@ -306,9 +352,10 @@ function shouldOnlyRevertOnErrors([owner, receiver, spender]) { }); it('reverts when decreasing the allowance to a negative value', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.$safeDecreaseAllowance(this.token.address, spender, 200), - 'SafeERC20: decreased allowance below zero', + 'SafeERC20FailedDecreaseAllowance', + [spender, 100, 200], ); }); }); diff --git a/test/token/ERC721/ERC721.behavior.js b/test/token/ERC721/ERC721.behavior.js index 6867db31f84..75700f6ab56 100644 --- a/test/token/ERC721/ERC721.behavior.js +++ b/test/token/ERC721/ERC721.behavior.js @@ -3,14 +3,13 @@ const { expect } = require('chai'); const { ZERO_ADDRESS } = constants; 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 Error = ['None', 'RevertWithMessage', 'RevertWithoutMessage', 'Panic'].reduce( - (acc, entry, idx) => Object.assign({ [entry]: idx }, acc), - {}, -); +const RevertType = Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'); const firstTokenId = new BN('5042'); const secondTokenId = new BN('79217'); @@ -20,7 +19,7 @@ const baseURI = 'https://api.example.com/v1/'; const RECEIVER_MAGIC_VALUE = '0x150b7a02'; -function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherApproved, operator, other) { +function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, operator, other) { shouldSupportInterfaces(['ERC165', 'ERC721']); context('with minted tokens', function () { @@ -45,7 +44,7 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when querying the zero address', function () { it('throws', async function () { - await expectRevert(this.token.balanceOf(ZERO_ADDRESS), 'ERC721: address zero is not a valid owner'); + await expectRevertCustomError(this.token.balanceOf(ZERO_ADDRESS), 'ERC721InvalidOwner', [ZERO_ADDRESS]); }); }); }); @@ -63,7 +62,7 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA const tokenId = nonExistentTokenId; it('reverts', async function () { - await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); }); }); }); @@ -172,36 +171,40 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when the address of the previous owner is incorrect', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( transferFunction.call(this, other, other, tokenId, { from: owner }), - 'ERC721: transfer from incorrect owner', + 'ERC721IncorrectOwner', + [other, tokenId, owner], ); }); }); context('when the sender is not authorized for the token id', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( transferFunction.call(this, owner, other, tokenId, { from: other }), - 'ERC721: caller is not token owner or approved', + 'ERC721InsufficientApproval', + [other, tokenId], ); }); }); context('when the given token ID does not exist', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( transferFunction.call(this, owner, other, nonExistentTokenId, { from: owner }), - 'ERC721: invalid token ID', + 'ERC721NonexistentToken', + [nonExistentTokenId], ); }); }); context('when the address to transfer the token to is the zero address', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( transferFunction.call(this, owner, ZERO_ADDRESS, tokenId, { from: owner }), - 'ERC721: transfer to the zero address', + 'ERC721InvalidReceiver', + [ZERO_ADDRESS], ); }); }); @@ -229,7 +232,7 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('to a valid receiver contract', function () { beforeEach(async function () { - this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.None); + this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.None); this.toWhom = this.receiver.address; }); @@ -259,9 +262,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('with an invalid token id', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( transferFun.call(this, owner, this.receiver.address, nonExistentTokenId, { from: owner }), - 'ERC721: invalid token ID', + 'ERC721NonexistentToken', + [nonExistentTokenId], ); }); }); @@ -278,17 +282,18 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('to a receiver contract returning unexpected value', function () { it('reverts', async function () { - const invalidReceiver = await ERC721ReceiverMock.new('0x42', Error.None); - await expectRevert( + const invalidReceiver = await ERC721ReceiverMock.new('0x42', RevertType.None); + await expectRevertCustomError( this.token.safeTransferFrom(owner, invalidReceiver.address, tokenId, { from: owner }), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [invalidReceiver.address], ); }); }); describe('to a receiver contract that reverts with message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithMessage); + const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithMessage); await expectRevert( this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }), 'ERC721ReceiverMock: reverting', @@ -298,17 +303,35 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('to a receiver contract that reverts without message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithoutMessage); - await expectRevert( + const revertingReceiver = await ERC721ReceiverMock.new( + RECEIVER_MAGIC_VALUE, + RevertType.RevertWithoutMessage, + ); + await expectRevertCustomError( this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [revertingReceiver.address], + ); + }); + }); + + describe('to a receiver contract that reverts with custom error', function () { + it('reverts', async function () { + const revertingReceiver = await ERC721ReceiverMock.new( + RECEIVER_MAGIC_VALUE, + RevertType.RevertWithCustomError, + ); + await expectRevertCustomError( + this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }), + 'CustomError', + [RECEIVER_MAGIC_VALUE], ); }); }); describe('to a receiver contract that panics', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.Panic); + const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.Panic); await expectRevert.unspecified( this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }), ); @@ -318,9 +341,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('to a contract that does not implement the required function', function () { it('reverts', async function () { const nonReceiver = await NonERC721ReceiverMock.new(); - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(owner, nonReceiver.address, tokenId, { from: owner }), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [nonReceiver.address], ); }); }); @@ -334,7 +358,7 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA 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, Error.None); + this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.None); const receipt = await this.token.$_safeMint(this.receiver.address, tokenId, data); await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', { @@ -345,7 +369,7 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA }); it('calls onERC721Received — without data', async function () { - this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.None); + this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.None); const receipt = await this.token.$_safeMint(this.receiver.address, tokenId); await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', { @@ -356,17 +380,18 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('to a receiver contract returning unexpected value', function () { it('reverts', async function () { - const invalidReceiver = await ERC721ReceiverMock.new('0x42', Error.None); - await expectRevert( + const invalidReceiver = await ERC721ReceiverMock.new('0x42', RevertType.None); + await expectRevertCustomError( this.token.$_safeMint(invalidReceiver.address, tokenId), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [invalidReceiver.address], ); }); }); context('to a receiver contract that reverts with message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithMessage); + const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithMessage); await expectRevert( this.token.$_safeMint(revertingReceiver.address, tokenId), 'ERC721ReceiverMock: reverting', @@ -376,17 +401,33 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('to a receiver contract that reverts without message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithoutMessage); - await expectRevert( + const revertingReceiver = await ERC721ReceiverMock.new( + RECEIVER_MAGIC_VALUE, + RevertType.RevertWithoutMessage, + ); + await expectRevertCustomError( this.token.$_safeMint(revertingReceiver.address, tokenId), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [revertingReceiver.address], + ); + }); + }); + + context('to a receiver contract that reverts with custom error', function () { + it('reverts', async function () { + const revertingReceiver = await ERC721ReceiverMock.new( + RECEIVER_MAGIC_VALUE, + RevertType.RevertWithCustomError, ); + await expectRevertCustomError(this.token.$_safeMint(revertingReceiver.address, tokenId), 'CustomError', [ + RECEIVER_MAGIC_VALUE, + ]); }); }); context('to a receiver contract that panics', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.Panic); + const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.Panic); await expectRevert.unspecified(this.token.$_safeMint(revertingReceiver.address, tokenId)); }); }); @@ -394,9 +435,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('to a contract that does not implement the required function', function () { it('reverts', async function () { const nonReceiver = await NonERC721ReceiverMock.new(); - await expectRevert( + await expectRevertCustomError( this.token.$_safeMint(nonReceiver.address, tokenId), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [nonReceiver.address], ); }); }); @@ -484,15 +526,18 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when the address that receives the approval is the owner', function () { it('reverts', async function () { - await expectRevert(this.token.approve(owner, tokenId, { from: owner }), 'ERC721: approval to current owner'); + await expectRevertCustomError(this.token.approve(owner, tokenId, { from: owner }), 'ERC721InvalidOperator', [ + owner, + ]); }); }); context('when the sender does not own the given token ID', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.approve(approved, tokenId, { from: other }), - 'ERC721: approve caller is not token owner or approved', + 'ERC721InvalidApprover', + [other], ); }); }); @@ -500,9 +545,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when the sender is approved for the given token ID', function () { it('reverts', async function () { await this.token.approve(approved, tokenId, { from: owner }); - await expectRevert( + await expectRevertCustomError( this.token.approve(anotherApproved, tokenId, { from: approved }), - 'ERC721: approve caller is not token owner or approved for all', + 'ERC721InvalidApprover', + [approved], ); }); }); @@ -519,9 +565,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when the given token ID does not exist', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.approve(approved, nonExistentTokenId, { from: operator }), - 'ERC721: invalid token ID', + 'ERC721NonexistentToken', + [nonExistentTokenId], ); }); }); @@ -600,7 +647,11 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when the operator is the owner', function () { it('reverts', async function () { - await expectRevert(this.token.setApprovalForAll(owner, true, { from: owner }), 'ERC721: approve to caller'); + await expectRevertCustomError( + this.token.setApprovalForAll(owner, true, { from: owner }), + 'ERC721InvalidOperator', + [owner], + ); }); }); }); @@ -608,7 +659,9 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('getApproved', async function () { context('when token is not minted', async function () { it('reverts', async function () { - await expectRevert(this.token.getApproved(nonExistentTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.getApproved(nonExistentTokenId), 'ERC721NonexistentToken', [ + nonExistentTokenId, + ]); }); }); @@ -632,7 +685,9 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('_mint(address, uint256)', function () { it('reverts with a null destination address', async function () { - await expectRevert(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721: mint to the zero address'); + await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721InvalidReceiver', [ + ZERO_ADDRESS, + ]); }); context('with minted token', async function () { @@ -650,14 +705,16 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA }); it('reverts when adding a token id that already exists', async function () { - await expectRevert(this.token.$_mint(owner, firstTokenId), 'ERC721: token already minted'); + await expectRevertCustomError(this.token.$_mint(owner, firstTokenId), 'ERC721InvalidSender', [ZERO_ADDRESS]); }); }); }); describe('_burn', function () { it('reverts when burning a non-existent token id', async function () { - await expectRevert(this.token.$_burn(nonExistentTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.$_burn(nonExistentTokenId), 'ERC721NonexistentToken', [ + nonExistentTokenId, + ]); }); context('with minted tokens', function () { @@ -677,18 +734,18 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA it('deletes the token', async function () { expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1'); - await expectRevert(this.token.ownerOf(firstTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.ownerOf(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); }); it('reverts when burning a token id that has been deleted', async function () { - await expectRevert(this.token.$_burn(firstTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.$_burn(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); }); }); }); }); } -function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved, anotherApproved, operator, other) { +function shouldBehaveLikeERC721Enumerable(owner, newOwner, approved, anotherApproved, operator, other) { shouldSupportInterfaces(['ERC721Enumerable']); context('with minted tokens', function () { @@ -713,13 +770,13 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved describe('when the index is greater than or equal to the total tokens owned by the given address', function () { it('reverts', async function () { - await expectRevert(this.token.tokenOfOwnerByIndex(owner, 2), 'ERC721Enumerable: owner index out of bounds'); + await expectRevertCustomError(this.token.tokenOfOwnerByIndex(owner, 2), 'ERC721OutOfBoundsIndex', [owner, 2]); }); }); describe('when the given address does not own any token', function () { it('reverts', async function () { - await expectRevert(this.token.tokenOfOwnerByIndex(other, 0), 'ERC721Enumerable: owner index out of bounds'); + await expectRevertCustomError(this.token.tokenOfOwnerByIndex(other, 0), 'ERC721OutOfBoundsIndex', [other, 0]); }); }); @@ -740,7 +797,7 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved it('returns empty collection for original owner', async function () { expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('0'); - await expectRevert(this.token.tokenOfOwnerByIndex(owner, 0), 'ERC721Enumerable: owner index out of bounds'); + await expectRevertCustomError(this.token.tokenOfOwnerByIndex(owner, 0), 'ERC721OutOfBoundsIndex', [owner, 0]); }); }); }); @@ -755,7 +812,7 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved }); it('reverts if index is greater than supply', async function () { - await expectRevert(this.token.tokenByIndex(2), 'ERC721Enumerable: global index out of bounds'); + await expectRevertCustomError(this.token.tokenByIndex(2), 'ERC721OutOfBoundsIndex', [ZERO_ADDRESS, 2]); }); [firstTokenId, secondTokenId].forEach(function (tokenId) { @@ -781,7 +838,9 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved describe('_mint(address, uint256)', function () { it('reverts with a null destination address', async function () { - await expectRevert(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721: mint to the zero address'); + await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721InvalidReceiver', [ + ZERO_ADDRESS, + ]); }); context('with minted token', async function () { @@ -801,7 +860,7 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved describe('_burn', function () { it('reverts when burning a non-existent token id', async function () { - await expectRevert(this.token.$_burn(firstTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.$_burn(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); }); context('with minted tokens', function () { @@ -826,14 +885,14 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved it('burns all tokens', async function () { await this.token.$_burn(secondTokenId, { from: owner }); expect(await this.token.totalSupply()).to.be.bignumber.equal('0'); - await expectRevert(this.token.tokenByIndex(0), 'ERC721Enumerable: global index out of bounds'); + await expectRevertCustomError(this.token.tokenByIndex(0), 'ERC721OutOfBoundsIndex', [ZERO_ADDRESS, 0]); }); }); }); }); } -function shouldBehaveLikeERC721Metadata(errorPrefix, name, symbol, owner) { +function shouldBehaveLikeERC721Metadata(name, symbol, owner) { shouldSupportInterfaces(['ERC721Metadata']); describe('metadata', function () { @@ -855,7 +914,9 @@ function shouldBehaveLikeERC721Metadata(errorPrefix, name, symbol, owner) { }); it('reverts when queried for non existent token id', async function () { - await expectRevert(this.token.tokenURI(nonExistentTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.tokenURI(nonExistentTokenId), 'ERC721NonexistentToken', [ + nonExistentTokenId, + ]); }); describe('base URI', function () { diff --git a/test/token/ERC721/ERC721.test.js b/test/token/ERC721/ERC721.test.js index 312430cb973..372dd5069d0 100644 --- a/test/token/ERC721/ERC721.test.js +++ b/test/token/ERC721/ERC721.test.js @@ -10,6 +10,6 @@ contract('ERC721', function (accounts) { this.token = await ERC721.new(name, symbol); }); - shouldBehaveLikeERC721('ERC721', ...accounts); - shouldBehaveLikeERC721Metadata('ERC721', name, symbol, ...accounts); + shouldBehaveLikeERC721(...accounts); + shouldBehaveLikeERC721Metadata(name, symbol, ...accounts); }); diff --git a/test/token/ERC721/ERC721Enumerable.test.js b/test/token/ERC721/ERC721Enumerable.test.js index b32f22dd6d0..31c28d177b5 100644 --- a/test/token/ERC721/ERC721Enumerable.test.js +++ b/test/token/ERC721/ERC721Enumerable.test.js @@ -14,7 +14,7 @@ contract('ERC721Enumerable', function (accounts) { this.token = await ERC721Enumerable.new(name, symbol); }); - shouldBehaveLikeERC721('ERC721', ...accounts); - shouldBehaveLikeERC721Metadata('ERC721', name, symbol, ...accounts); - shouldBehaveLikeERC721Enumerable('ERC721', ...accounts); + shouldBehaveLikeERC721(...accounts); + shouldBehaveLikeERC721Metadata(name, symbol, ...accounts); + shouldBehaveLikeERC721Enumerable(...accounts); }); diff --git a/test/token/ERC721/extensions/ERC721Burnable.test.js b/test/token/ERC721/extensions/ERC721Burnable.test.js index 6a4bc6dbc4f..df059e09078 100644 --- a/test/token/ERC721/extensions/ERC721Burnable.test.js +++ b/test/token/ERC721/extensions/ERC721Burnable.test.js @@ -1,11 +1,12 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC721Burnable = artifacts.require('$ERC721Burnable'); contract('ERC721Burnable', function (accounts) { - const [owner, approved] = accounts; + const [owner, approved, another] = accounts; const firstTokenId = new BN(1); const secondTokenId = new BN(2); @@ -34,7 +35,7 @@ contract('ERC721Burnable', function (accounts) { }); it('burns the given token ID and adjusts the balance of the owner', async function () { - await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1'); }); @@ -55,14 +56,25 @@ contract('ERC721Burnable', function (accounts) { context('getApproved', function () { it('reverts', async function () { - await expectRevert(this.token.getApproved(tokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.getApproved(tokenId), 'ERC721NonexistentToken', [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, + ]); + }); + }); + describe('when the given token ID was not tracked by this contract', function () { it('reverts', async function () { - await expectRevert(this.token.burn(unknownTokenId, { from: owner }), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.burn(unknownTokenId, { from: owner }), 'ERC721NonexistentToken', [ + unknownTokenId, + ]); }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Consecutive.test.js b/test/token/ERC721/extensions/ERC721Consecutive.test.js index d4f0b4f8a50..55c26dffe08 100644 --- a/test/token/ERC721/extensions/ERC721Consecutive.test.js +++ b/test/token/ERC721/extensions/ERC721Consecutive.test.js @@ -1,6 +1,8 @@ -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); 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'); @@ -83,14 +85,19 @@ contract('ERC721Consecutive', function (accounts) { expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(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], + ); + }); }); describe('minting after construction', function () { it('consecutive minting is not possible after construction', async function () { - await expectRevert( - this.token.$_mintConsecutive(user1, 10), - 'ERC721Consecutive: batch minting restricted to constructor', - ); + await expectRevertCustomError(this.token.$_mintConsecutive(user1, 10), 'ERC721ForbiddenBatchMint', []); }); it('simple minting is possible after construction', async function () { @@ -110,7 +117,7 @@ contract('ERC721Consecutive', function (accounts) { expect(await this.token.$_exists(tokenId)).to.be.equal(true); - await expectRevert(this.token.$_mint(user1, tokenId), 'ERC721: token already minted'); + await expectRevertCustomError(this.token.$_mint(user1, tokenId), 'ERC721InvalidSender', [ZERO_ADDRESS]); }); }); @@ -130,7 +137,7 @@ contract('ERC721Consecutive', function (accounts) { tokenId, }); - await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); expectEvent(await this.token.$_mint(user2, tokenId), 'Transfer', { from: constants.ZERO_ADDRESS, @@ -145,7 +152,7 @@ contract('ERC721Consecutive', function (accounts) { const tokenId = web3.utils.toBN(sum(...batches.map(({ amount }) => amount)) + offset); expect(await this.token.$_exists(tokenId)).to.be.equal(false); - await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); // mint await this.token.$_mint(user1, tokenId); @@ -161,7 +168,7 @@ contract('ERC721Consecutive', function (accounts) { }); expect(await this.token.$_exists(tokenId)).to.be.equal(false); - await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); // re-mint expectEvent(await this.token.$_mint(user2, tokenId), 'Transfer', { @@ -173,41 +180,56 @@ contract('ERC721Consecutive', function (accounts) { expect(await this.token.$_exists(tokenId)).to.be.equal(true); expect(await this.token.ownerOf(tokenId), user2); }); + + it('reverts burning batches of size != 1', async function () { + const tokenId = batches[0].amount + offset; + const receiver = batches[0].receiver; + + await expectRevertCustomError( + this.token.$_afterTokenTransfer(receiver, ZERO_ADDRESS, tokenId, 2), + 'ERC721ForbiddenBatchBurn', + [], + ); + }); }); }); } describe('invalid use', function () { it('cannot mint a batch larger than 5000', async function () { - await expectRevert( + await expectRevertCustomError( ERC721ConsecutiveMock.new(name, symbol, 0, [], [user1], ['5001']), - 'ERC721Consecutive: batch too large', + 'ERC721ExceededMaxBatchMint', + [5000, 5001], ); }); it('cannot use single minting during construction', async function () { - await expectRevert( + await expectRevertCustomError( ERC721ConsecutiveNoConstructorMintMock.new(name, symbol), - "ERC721Consecutive: can't mint during construction", + 'ERC721ForbiddenMint', + [], ); }); it('cannot use single minting during construction', async function () { - await expectRevert( + await expectRevertCustomError( ERC721ConsecutiveNoConstructorMintMock.new(name, symbol), - "ERC721Consecutive: can't mint during construction", + 'ERC721ForbiddenMint', + [], ); }); it('consecutive mint not compatible with enumerability', async function () { - await expectRevert( + await expectRevertCustomError( ERC721ConsecutiveEnumerableMock.new( name, symbol, batches.map(({ receiver }) => receiver), batches.map(({ amount }) => amount), ), - 'ERC721Enumerable: consecutive transfers not supported', + 'ERC721EnumerableForbiddenBatchMint', + [], ); }); }); diff --git a/test/token/ERC721/extensions/ERC721Pausable.test.js b/test/token/ERC721/extensions/ERC721Pausable.test.js index c7fc8233f13..ec99dea96b9 100644 --- a/test/token/ERC721/extensions/ERC721Pausable.test.js +++ b/test/token/ERC721/extensions/ERC721Pausable.test.js @@ -1,6 +1,7 @@ -const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC721Pausable = artifacts.require('$ERC721Pausable'); @@ -26,34 +27,37 @@ contract('ERC721Pausable', function (accounts) { }); it('reverts when trying to transferFrom', async function () { - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(owner, receiver, firstTokenId, { from: owner }), - 'ERC721Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to safeTransferFrom', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(owner, receiver, firstTokenId, { from: owner }), - 'ERC721Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to safeTransferFrom with data', async function () { - await expectRevert( + await expectRevertCustomError( this.token.methods['safeTransferFrom(address,address,uint256,bytes)'](owner, receiver, firstTokenId, mockData, { from: owner, }), - 'ERC721Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to mint', async function () { - await expectRevert(this.token.$_mint(receiver, secondTokenId), 'ERC721Pausable: token transfer while paused'); + await expectRevertCustomError(this.token.$_mint(receiver, secondTokenId), 'EnforcedPause', []); }); it('reverts when trying to burn', async function () { - await expectRevert(this.token.$_burn(firstTokenId), 'ERC721Pausable: token transfer while paused'); + await expectRevertCustomError(this.token.$_burn(firstTokenId), 'EnforcedPause', []); }); describe('getApproved', function () { diff --git a/test/token/ERC721/extensions/ERC721URIStorage.test.js b/test/token/ERC721/extensions/ERC721URIStorage.test.js index 60c80066c08..34738cae12a 100644 --- a/test/token/ERC721/extensions/ERC721URIStorage.test.js +++ b/test/token/ERC721/extensions/ERC721URIStorage.test.js @@ -1,7 +1,8 @@ -const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC721URIStorageMock = artifacts.require('$ERC721URIStorageMock'); @@ -33,7 +34,9 @@ contract('ERC721URIStorage', function (accounts) { }); it('reverts when queried for non existent token id', async function () { - await expectRevert(this.token.tokenURI(nonExistentTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.tokenURI(nonExistentTokenId), 'ERC721NonexistentToken', [ + nonExistentTokenId, + ]); }); it('can be set for a token id', async function () { @@ -48,10 +51,9 @@ contract('ERC721URIStorage', function (accounts) { }); it('reverts when setting for non existent token id', async function () { - await expectRevert( - this.token.$_setTokenURI(nonExistentTokenId, sampleUri), - 'ERC721URIStorage: URI set of nonexistent token', - ); + await expectRevertCustomError(this.token.$_setTokenURI(nonExistentTokenId, sampleUri), 'ERC721NonexistentToken', [ + nonExistentTokenId, + ]); }); it('base URI can be set', async function () { @@ -85,7 +87,7 @@ contract('ERC721URIStorage', function (accounts) { await this.token.$_burn(firstTokenId, { from: owner }); expect(await this.token.$_exists(firstTokenId)).to.equal(false); - await expectRevert(this.token.tokenURI(firstTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.tokenURI(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); }); it('tokens with URI can be burnt ', async function () { @@ -94,7 +96,7 @@ contract('ERC721URIStorage', function (accounts) { await this.token.$_burn(firstTokenId, { from: owner }); expect(await this.token.$_exists(firstTokenId)).to.equal(false); - await expectRevert(this.token.tokenURI(firstTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.tokenURI(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Wrapper.test.js b/test/token/ERC721/extensions/ERC721Wrapper.test.js index 6e46d2e5ab3..6839977449d 100644 --- a/test/token/ERC721/extensions/ERC721Wrapper.test.js +++ b/test/token/ERC721/extensions/ERC721Wrapper.test.js @@ -1,7 +1,8 @@ -const { BN, expectEvent, constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, expectEvent, constants } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { shouldBehaveLikeERC721 } = require('../ERC721.behavior'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC721 = artifacts.require('$ERC721'); const ERC721Wrapper = artifacts.require('$ERC721Wrapper'); @@ -115,9 +116,10 @@ contract('ERC721Wrapper', function (accounts) { }); it('reverts with missing approval', async function () { - await expectRevert( + await expectRevertCustomError( this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }), - 'ERC721: caller is not token owner or approved', + 'ERC721InsufficientApproval', + [this.token.address, firstTokenId], ); }); }); @@ -178,9 +180,10 @@ contract('ERC721Wrapper', function (accounts) { }); it("doesn't work for a non-owner nor approved", async function () { - await expectRevert( + await expectRevertCustomError( this.token.withdrawTo(initialHolder, [firstTokenId], { from: anotherAccount }), - 'ERC721Wrapper: caller is not token owner or approved', + 'ERC721InsufficientApproval', + [anotherAccount, firstTokenId], ); }); @@ -230,7 +233,7 @@ contract('ERC721Wrapper', function (accounts) { describe('onERC721Received', function () { it('only allows calls from underlying', async function () { - await expectRevert( + await expectRevertCustomError( this.token.onERC721Received( initialHolder, this.token.address, @@ -238,7 +241,8 @@ contract('ERC721Wrapper', function (accounts) { anotherAccount, // Correct data { from: anotherAccount }, ), - 'ERC721Wrapper: caller is not underlying', + 'ERC721UnsupportedToken', + [anotherAccount], ); }); @@ -270,14 +274,16 @@ contract('ERC721Wrapper', function (accounts) { }); it('reverts if there is nothing to recover', async function () { - await expectRevert( - this.token.$_recover(initialHolder, firstTokenId), - 'ERC721Wrapper: wrapper is not token owner', - ); + const owner = await this.underlying.ownerOf(firstTokenId); + await expectRevertCustomError(this.token.$_recover(initialHolder, firstTokenId), 'ERC721IncorrectOwner', [ + this.token.address, + firstTokenId, + owner, + ]); }); }); describe('ERC712 behavior', function () { - shouldBehaveLikeERC721('ERC721', ...accounts); + shouldBehaveLikeERC721(...accounts); }); }); diff --git a/test/token/ERC721/utils/ERC721Holder.test.js b/test/token/ERC721/utils/ERC721Holder.test.js index 0fd82228035..4aa2b79484b 100644 --- a/test/token/ERC721/utils/ERC721Holder.test.js +++ b/test/token/ERC721/utils/ERC721Holder.test.js @@ -1,6 +1,6 @@ const { expect } = require('chai'); -const ERC721Holder = artifacts.require('ERC721Holder'); +const ERC721Holder = artifacts.require('$ERC721Holder'); const ERC721 = artifacts.require('$ERC721'); contract('ERC721Holder', function (accounts) { diff --git a/test/token/common/ERC2981.behavior.js b/test/token/common/ERC2981.behavior.js index 5d0f677152c..15efa239f70 100644 --- a/test/token/common/ERC2981.behavior.js +++ b/test/token/common/ERC2981.behavior.js @@ -1,8 +1,9 @@ -const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants } = require('@openzeppelin/test-helpers'); 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'); @@ -60,11 +61,18 @@ function shouldBehaveLikeERC2981() { }); it('reverts if invalid parameters', async function () { - await expectRevert(this.token.$_setDefaultRoyalty(ZERO_ADDRESS, royaltyFraction), 'ERC2981: invalid receiver'); + const royaltyDenominator = await this.token.$_feeDenominator(); + await expectRevertCustomError( + this.token.$_setDefaultRoyalty(ZERO_ADDRESS, royaltyFraction), + 'ERC2981InvalidDefaultRoyaltyReceiver', + [ZERO_ADDRESS], + ); - await expectRevert( - this.token.$_setDefaultRoyalty(this.account1, new BN('11000')), - 'ERC2981: royalty fee will exceed salePrice', + const anotherRoyaltyFraction = new BN('11000'); + await expectRevertCustomError( + this.token.$_setDefaultRoyalty(this.account1, anotherRoyaltyFraction), + 'ERC2981InvalidDefaultRoyalty', + [anotherRoyaltyFraction, royaltyDenominator], ); }); }); @@ -104,14 +112,18 @@ function shouldBehaveLikeERC2981() { }); it('reverts if invalid parameters', async function () { - await expectRevert( + const royaltyDenominator = await this.token.$_feeDenominator(); + await expectRevertCustomError( this.token.$_setTokenRoyalty(this.tokenId1, ZERO_ADDRESS, royaltyFraction), - 'ERC2981: Invalid parameters', + 'ERC2981InvalidTokenRoyaltyReceiver', + [this.tokenId1.toString(), ZERO_ADDRESS], ); - await expectRevert( - this.token.$_setTokenRoyalty(this.tokenId1, this.account1, new BN('11000')), - 'ERC2981: royalty fee will exceed salePrice', + const anotherRoyaltyFraction = new BN('11000'); + await expectRevertCustomError( + this.token.$_setTokenRoyalty(this.tokenId1, this.account1, anotherRoyaltyFraction), + 'ERC2981InvalidTokenRoyalty', + [this.tokenId1.toString(), anotherRoyaltyFraction, royaltyDenominator], ); }); diff --git a/test/utils/Address.test.js b/test/utils/Address.test.js index ea72ab610b6..beded18e1d4 100644 --- a/test/utils/Address.test.js +++ b/test/utils/Address.test.js @@ -1,7 +1,9 @@ const { balance, constants, ether, expectRevert, send, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); const Address = artifacts.require('$Address'); +const AddressFnPointerMock = artifacts.require('$AddressFnPointerMock'); const EtherReceiver = artifacts.require('EtherReceiverMock'); const CallReceiverMock = artifacts.require('CallReceiverMock'); @@ -10,6 +12,7 @@ contract('Address', function (accounts) { beforeEach(async function () { this.mock = await Address.new(); + this.mockFnPointer = await AddressFnPointerMock.new(); }); describe('sendValue', function () { @@ -25,7 +28,9 @@ contract('Address', function (accounts) { }); it('reverts when sending non-zero amounts', async function () { - await expectRevert(this.mock.$sendValue(other, 1), 'Address: insufficient balance'); + await expectRevertCustomError(this.mock.$sendValue(other, 1), 'AddressInsufficientBalance', [ + this.mock.address, + ]); }); }); @@ -52,7 +57,9 @@ contract('Address', function (accounts) { }); it('reverts when sending more than the balance', async function () { - await expectRevert(this.mock.$sendValue(recipient, funds.addn(1)), 'Address: insufficient balance'); + await expectRevertCustomError(this.mock.$sendValue(recipient, funds.addn(1)), 'AddressInsufficientBalance', [ + this.mock.address, + ]); }); context('with contract recipient', function () { @@ -71,10 +78,7 @@ contract('Address', function (accounts) { it('reverts on recipient revert', async function () { await this.target.setAcceptEther(false); - await expectRevert( - this.mock.$sendValue(this.target.address, funds), - 'Address: unable to send value, recipient may have reverted', - ); + await expectRevertCustomError(this.mock.$sendValue(this.target.address, funds), 'FailedInnerCall', []); }); }); }); @@ -91,7 +95,7 @@ contract('Address', function (accounts) { const receipt = await this.mock.$functionCall(this.target.address, abiEncodedCall); - expectEvent(receipt, 'return$functionCall_address_bytes', { + expectEvent(receipt, 'return$functionCall', { ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']), }); await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); @@ -108,9 +112,10 @@ contract('Address', function (accounts) { it('reverts when the called function reverts with no reason', async function () { const abiEncodedCall = this.target.contract.methods.mockFunctionRevertsNoReason().encodeABI(); - await expectRevert( + await expectRevertCustomError( this.mock.$functionCall(this.target.address, abiEncodedCall), - 'Address: low-level call failed', + 'FailedInnerCall', + [], ); }); @@ -123,9 +128,10 @@ contract('Address', function (accounts) { it('reverts when the called function runs out of gas', async function () { const abiEncodedCall = this.target.contract.methods.mockFunctionOutOfGas().encodeABI(); - await expectRevert( + await expectRevertCustomError( this.mock.$functionCall(this.target.address, abiEncodedCall, { gas: '120000' }), - 'Address: low-level call failed', + 'FailedInnerCall', + [], ); }); @@ -135,9 +141,12 @@ contract('Address', function (accounts) { await expectRevert.unspecified(this.mock.$functionCall(this.target.address, abiEncodedCall)); }); - it('bubbles up error message if specified', async function () { - const errorMsg = 'Address: expected error'; - await expectRevert(this.mock.$functionCall(this.target.address, '0x12345678', errorMsg), errorMsg); + it('bubbles up error if specified', async function () { + await expectRevertCustomError( + this.mockFnPointer.functionCall(this.target.address, '0x12345678'), + 'CustomRevert', + [], + ); }); it('reverts when function does not exist', async function () { @@ -150,9 +159,10 @@ contract('Address', function (accounts) { [], ); - await expectRevert( + await expectRevertCustomError( this.mock.$functionCall(this.target.address, abiEncodedCall), - 'Address: low-level call failed', + 'FailedInnerCall', + [], ); }); }); @@ -162,7 +172,9 @@ contract('Address', function (accounts) { const [recipient] = accounts; const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); - await expectRevert(this.mock.$functionCall(recipient, abiEncodedCall), 'Address: call to non-contract'); + await expectRevertCustomError(this.mock.$functionCall(recipient, abiEncodedCall), 'AddressEmptyCode', [ + recipient, + ]); }); }); }); @@ -177,7 +189,7 @@ contract('Address', function (accounts) { const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); const receipt = await this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, 0); - expectEvent(receipt, 'return$functionCallWithValue_address_bytes_uint256', { + expectEvent(receipt, 'return$functionCallWithValue', { ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']), }); await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); @@ -190,9 +202,10 @@ contract('Address', function (accounts) { it('reverts if insufficient sender balance', async function () { const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); - await expectRevert( + await expectRevertCustomError( this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount), - 'Address: insufficient balance for call', + 'AddressInsufficientBalance', + [this.mock.address], ); }); @@ -204,7 +217,7 @@ contract('Address', function (accounts) { await send.ether(other, this.mock.address, amount); const receipt = await this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount); - expectEvent(receipt, 'return$functionCallWithValue_address_bytes_uint256', { + expectEvent(receipt, 'return$functionCallWithValue', { ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']), }); await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); @@ -223,7 +236,7 @@ contract('Address', function (accounts) { from: other, value: amount, }); - expectEvent(receipt, 'return$functionCallWithValue_address_bytes_uint256', { + expectEvent(receipt, 'return$functionCallWithValue', { ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']), }); await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); @@ -235,15 +248,19 @@ contract('Address', function (accounts) { const abiEncodedCall = this.target.contract.methods.mockFunctionNonPayable().encodeABI(); await send.ether(other, this.mock.address, amount); - await expectRevert( + await expectRevertCustomError( this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount), - 'Address: low-level call with value failed', + 'FailedInnerCall', + [], ); }); - it('bubbles up error message if specified', async function () { - const errorMsg = 'Address: expected error'; - await expectRevert(this.mock.$functionCallWithValue(this.target.address, '0x12345678', 0, errorMsg), errorMsg); + it('bubbles up error if specified', async function () { + await expectRevertCustomError( + this.mockFnPointer.functionCallWithValue(this.target.address, '0x12345678', 0), + 'CustomRevert', + [], + ); }); }); }); @@ -264,9 +281,10 @@ contract('Address', function (accounts) { it('reverts on a non-static function', async function () { const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); - await expectRevert( + await expectRevertCustomError( this.mock.$functionStaticCall(this.target.address, abiEncodedCall), - 'Address: low-level static call failed', + 'FailedInnerCall', + [], ); }); @@ -283,12 +301,17 @@ contract('Address', function (accounts) { const [recipient] = accounts; const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); - await expectRevert(this.mock.$functionStaticCall(recipient, abiEncodedCall), 'Address: call to non-contract'); + await expectRevertCustomError(this.mock.$functionStaticCall(recipient, abiEncodedCall), 'AddressEmptyCode', [ + recipient, + ]); }); - it('bubbles up error message if specified', async function () { - const errorMsg = 'Address: expected error'; - await expectRevert(this.mock.$functionCallWithValue(this.target.address, '0x12345678', 0, errorMsg), errorMsg); + it('bubbles up error if specified', async function () { + await expectRevertCustomError( + this.mockFnPointer.functionCallWithValue(this.target.address, '0x12345678', 0), + 'CustomRevert', + [], + ); }); }); @@ -308,7 +331,7 @@ contract('Address', function (accounts) { expectEvent( await this.mock.$functionDelegateCall(this.target.address, abiEncodedCall), - 'return$functionDelegateCall_address_bytes', + 'return$functionDelegateCall', { ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']) }, ); @@ -328,24 +351,32 @@ contract('Address', function (accounts) { const [recipient] = accounts; const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); - await expectRevert(this.mock.$functionDelegateCall(recipient, abiEncodedCall), 'Address: call to non-contract'); + await expectRevertCustomError(this.mock.$functionDelegateCall(recipient, abiEncodedCall), 'AddressEmptyCode', [ + recipient, + ]); }); - it('bubbles up error message if specified', async function () { - const errorMsg = 'Address: expected error'; - await expectRevert(this.mock.$functionCallWithValue(this.target.address, '0x12345678', 0, errorMsg), errorMsg); + it('bubbles up error if specified', async function () { + await expectRevertCustomError( + this.mockFnPointer.functionCallWithValue(this.target.address, '0x12345678', 0), + 'CustomRevert', + [], + ); }); }); describe('verifyCallResult', function () { it('returns returndata on success', async function () { const returndata = '0x123abc'; - expect(await this.mock.$verifyCallResult(true, returndata, '')).to.equal(returndata); + expect(await this.mockFnPointer.verifyCallResult(true, returndata)).to.equal(returndata); + }); + + it('reverts with return data and error', async function () { + await expectRevertCustomError(this.mockFnPointer.verifyCallResult(false, '0x'), 'CustomRevert', []); }); - it('reverts with return data and error m', async function () { - const errorMsg = 'Address: expected error'; - await expectRevert(this.mock.$verifyCallResult(false, '0x', errorMsg), errorMsg); + it('reverts expecting error if provided onRevert is a non-reverting function', async function () { + await expectRevertCustomError(this.mockFnPointer.verifyCallResultVoid(false, '0x'), 'FailedInnerCall', []); }); }); }); diff --git a/test/utils/Create2.test.js b/test/utils/Create2.test.js index 526602600fd..f88d5504c36 100644 --- a/test/utils/Create2.test.js +++ b/test/utils/Create2.test.js @@ -1,6 +1,7 @@ const { balance, ether, expectEvent, expectRevert, send } = require('@openzeppelin/test-helpers'); const { computeCreate2Address } = require('../helpers/create2'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); const Create2 = artifacts.require('$Create2'); const VestingWallet = artifacts.require('VestingWallet'); @@ -78,15 +79,22 @@ contract('Create2', function (accounts) { it('fails deploying a contract in an existent address', async function () { expectEvent(await this.factory.$deploy(0, saltHex, constructorByteCode), 'return$deploy'); - await expectRevert(this.factory.$deploy(0, saltHex, constructorByteCode), 'Create2: Failed on 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)); }); it('fails deploying a contract if the bytecode length is zero', async function () { - await expectRevert(this.factory.$deploy(0, saltHex, '0x'), 'Create2: bytecode length is zero'); + await expectRevertCustomError(this.factory.$deploy(0, saltHex, '0x'), 'Create2EmptyBytecode', []); }); it('fails deploying a contract if factory contract does not have sufficient balance', async function () { - await expectRevert(this.factory.$deploy(1, saltHex, constructorByteCode), 'Create2: insufficient balance'); + await expectRevertCustomError( + this.factory.$deploy(1, saltHex, constructorByteCode), + 'Create2InsufficientBalance', + [0, 1], + ); }); }); }); diff --git a/test/utils/Multicall.test.js b/test/utils/Multicall.test.js index cfb80076956..65443cd0a85 100644 --- a/test/utils/Multicall.test.js +++ b/test/utils/Multicall.test.js @@ -1,4 +1,5 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); +const { expectRevertCustomError } = require('../helpers/customError'); const ERC20MulticallMock = artifacts.require('$ERC20MulticallMock'); @@ -50,7 +51,7 @@ contract('Multicall', function (accounts) { { from: deployer }, ); - await expectRevert(call, 'ERC20: transfer amount exceeds balance'); + await expectRevertCustomError(call, 'ERC20InsufficientBalance', [deployer, 0, amount]); expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0')); }); @@ -63,6 +64,6 @@ contract('Multicall', function (accounts) { { from: deployer }, ); - await expectRevert(call, 'ERC20: transfer amount exceeds balance'); + await expectRevertCustomError(call, 'ERC20InsufficientBalance', [deployer, 0, amount]); }); }); diff --git a/test/utils/Nonces.test.js b/test/utils/Nonces.test.js index 4a01bb1bc8d..361eeeeec81 100644 --- a/test/utils/Nonces.test.js +++ b/test/utils/Nonces.test.js @@ -1,4 +1,5 @@ const expectEvent = require('@openzeppelin/test-helpers/src/expectEvent'); +const { expectRevertCustomError } = require('../helpers/customError'); require('@openzeppelin/test-helpers'); @@ -15,22 +16,57 @@ contract('Nonces', function (accounts) { expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); }); - it('increment a nonce', async function () { - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); + describe('_useNonce', function () { + it('increments a nonce', async function () { + expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); + + const { receipt } = await this.nonces.$_useNonce(sender); + expectEvent(receipt, 'return$_useNonce', ['0']); + + expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); + }); - const { receipt } = await this.nonces.$_useNonce(sender); - expectEvent(receipt, 'return$_useNonce', ['0']); + 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.nonces.nonces(sender)).to.be.bignumber.equal('1'); + await this.nonces.$_useNonce(sender); + + expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); + expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + }); }); - it('nonce is specific to address argument', async function () { - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); - expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + describe('_useCheckedNonce', function () { + it('increments a nonce', async function () { + const currentNonce = await this.nonces.nonces(sender); + expect(currentNonce).to.be.bignumber.equal('0'); + + const { receipt } = await this.nonces.$_useCheckedNonce(sender, currentNonce); + expectEvent(receipt, 'return$_useCheckedNonce', [currentNonce]); + + expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); + }); + + it("increments only sender's nonce", async function () { + const currentNonce = await this.nonces.nonces(sender); + + expect(currentNonce).to.be.bignumber.equal('0'); + expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + + await this.nonces.$_useCheckedNonce(sender, currentNonce); - await this.nonces.$_useNonce(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.nonces.nonces(sender)).to.be.bignumber.equal('1'); - expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + 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], + ); + }); }); }); diff --git a/test/utils/ShortStrings.test.js b/test/utils/ShortStrings.test.js index f5cd82fbdbd..189281d38c9 100644 --- a/test/utils/ShortStrings.test.js +++ b/test/utils/ShortStrings.test.js @@ -29,7 +29,7 @@ contract('ShortStrings', function () { const decoded = await this.mock.$toString(encoded); expect(decoded).to.be.equal(str); } else { - await expectRevertCustomError(this.mock.$toShortString(str), `StringTooLong("${str}")`); + await expectRevertCustomError(this.mock.$toShortString(str), 'StringTooLong', [str]); } }); @@ -41,7 +41,7 @@ contract('ShortStrings', function () { if (str.length < 32) { expect(await promise).to.be.equal(str); } else { - await expectRevertCustomError(promise, 'InvalidShortString()'); + await expectRevertCustomError(promise, 'InvalidShortString', []); } const length = await this.mock.$byteLengthWithFallback(ret0, 0); diff --git a/test/utils/Strings.test.js b/test/utils/Strings.test.js index 6658871a00c..2435fc71c0d 100644 --- a/test/utils/Strings.test.js +++ b/test/utils/Strings.test.js @@ -1,4 +1,5 @@ -const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants } = require('@openzeppelin/test-helpers'); +const { expectRevertCustomError } = require('../helpers/customError'); const { expect } = require('chai'); @@ -47,22 +48,22 @@ contract('Strings', function () { describe('int256', function () { it('converts MAX_INT256', async function () { const value = constants.MAX_INT256; - expect(await this.strings.methods['$toString(int256)'](value)).to.equal(value.toString(10)); + expect(await this.strings.methods['$toStringSigned(int256)'](value)).to.equal(value.toString(10)); }); it('converts MIN_INT256', async function () { const value = constants.MIN_INT256; - expect(await this.strings.methods['$toString(int256)'](value)).to.equal(value.toString(10)); + expect(await this.strings.methods['$toStringSigned(int256)'](value)).to.equal(value.toString(10)); }); for (const value of values) { it(`convert ${value}`, async function () { - expect(await this.strings.methods['$toString(int256)'](value)).to.equal(value); + expect(await this.strings.methods['$toStringSigned(int256)'](value)).to.equal(value); }); it(`convert negative ${value}`, async function () { const negated = new BN(value).neg(); - expect(await this.strings.methods['$toString(int256)'](negated)).to.equal(negated.toString(10)); + expect(await this.strings.methods['$toStringSigned(int256)'](negated)).to.equal(negated.toString(10)); }); } }); @@ -92,9 +93,11 @@ contract('Strings', function () { }); it('converts a positive number (short)', async function () { - await expectRevert( - this.strings.methods['$toHexString(uint256,uint256)'](0x4132, 1), - 'Strings: hex length insufficient', + const length = 1; + await expectRevertCustomError( + this.strings.methods['$toHexString(uint256,uint256)'](0x4132, length), + `StringsInsufficientHexLength`, + [0x4132, length], ); }); diff --git a/test/utils/cryptography/ECDSA.test.js b/test/utils/cryptography/ECDSA.test.js index ae737086b12..3fd112a1844 100644 --- a/test/utils/cryptography/ECDSA.test.js +++ b/test/utils/cryptography/ECDSA.test.js @@ -1,4 +1,5 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); +require('@openzeppelin/test-helpers'); +const { expectRevertCustomError } = require('../../helpers/customError'); const { toEthSignedMessageHash, toDataWithIntendedValidatorHash } = require('../../helpers/sign'); const { expect } = require('chai'); @@ -51,17 +52,18 @@ contract('ECDSA', function (accounts) { context('recover with invalid signature', function () { it('with short signature', async function () { - await expectRevert(this.ecdsa.$recover(TEST_MESSAGE, '0x1234'), 'ECDSA: invalid signature length'); + await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, '0x1234'), 'ECDSAInvalidSignatureLength', [2]); }); it('with long signature', async function () { - await expectRevert( + await expectRevertCustomError( // eslint-disable-next-line max-len this.ecdsa.$recover( TEST_MESSAGE, '0x01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789', ), - 'ECDSA: invalid signature length', + 'ECDSAInvalidSignatureLength', + [85], ); }); }); @@ -93,7 +95,7 @@ contract('ECDSA', function (accounts) { // eslint-disable-next-line max-len const signature = '0x332ce75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e01c'; - await expectRevert(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSA: invalid signature'); + await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSAInvalidSignature', []); }); }); @@ -141,11 +143,12 @@ contract('ECDSA', function (accounts) { it('reverts wrong v values', async function () { for (const v of ['00', '01']) { const signature = signatureWithoutV + v; - await expectRevert(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSA: invalid signature'); + await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSAInvalidSignature', []); - await expectRevert( + await expectRevertCustomError( this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)), - 'ECDSA: invalid signature', + 'ECDSAInvalidSignature', + [], ); } }); @@ -153,9 +156,10 @@ contract('ECDSA', function (accounts) { it('rejects short EIP2098 format', async function () { const v = '1b'; // 27 = 1b. const signature = signatureWithoutV + v; - await expectRevert( + await expectRevertCustomError( this.ecdsa.$recover(TEST_MESSAGE, to2098Format(signature)), - 'ECDSA: invalid signature length', + 'ECDSAInvalidSignatureLength', + [64], ); }); }); @@ -203,11 +207,12 @@ contract('ECDSA', function (accounts) { it('reverts invalid v values', async function () { for (const v of ['00', '01']) { const signature = signatureWithoutV + v; - await expectRevert(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSA: invalid signature'); + await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSAInvalidSignature', []); - await expectRevert( + await expectRevertCustomError( this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)), - 'ECDSA: invalid signature', + 'ECDSAInvalidSignature', + [], ); } }); @@ -215,9 +220,10 @@ contract('ECDSA', function (accounts) { it('rejects short EIP2098 format', async function () { const v = '1c'; // 27 = 1b. const signature = signatureWithoutV + v; - await expectRevert( + await expectRevertCustomError( this.ecdsa.$recover(TEST_MESSAGE, to2098Format(signature)), - 'ECDSA: invalid signature length', + 'ECDSAInvalidSignatureLength', + [64], ); }); }); @@ -227,10 +233,12 @@ contract('ECDSA', function (accounts) { // eslint-disable-next-line max-len const highSSignature = '0xe742ff452d41413616a5bf43fe15dd88294e983d3d36206c2712f39083d638bde0a0fc89be718fbc1033e1d30d78be1c68081562ed2e97af876f286f3453231d1b'; - await expectRevert(this.ecdsa.$recover(message, highSSignature), "ECDSA: invalid signature 's' value"); - await expectRevert( - this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(highSSignature)), - "ECDSA: invalid signature 's' value", + 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"); }); diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js index 54a4e772372..7ea535b7f7d 100644 --- a/test/utils/cryptography/EIP712.test.js +++ b/test/utils/cryptography/EIP712.test.js @@ -98,6 +98,14 @@ contract('EIP712', function (accounts) { await this.eip712.verify(signature, wallet.getAddressString(), message.to, message.contents); }); + + it('name', async function () { + expect(await this.eip712.$_EIP712Name()).to.be.equal(name); + }); + + it('version', async function () { + expect(await this.eip712.$_EIP712Version()).to.be.equal(version); + }); }); } }); diff --git a/test/utils/cryptography/MerkleProof.test.js b/test/utils/cryptography/MerkleProof.test.js index 62157b56a84..5b87bc5252e 100644 --- a/test/utils/cryptography/MerkleProof.test.js +++ b/test/utils/cryptography/MerkleProof.test.js @@ -1,10 +1,10 @@ -require('@openzeppelin/test-helpers'); - const { expectRevert } = require('@openzeppelin/test-helpers'); + const { MerkleTree } = require('merkletreejs'); const keccak256 = require('keccak256'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../helpers/customError'); const MerkleProof = artifacts.require('$MerkleProof'); @@ -106,23 +106,25 @@ contract('MerkleProof', function () { const root = merkleTree.getRoot(); - await expectRevert( + await expectRevertCustomError( this.merkleProof.$multiProofVerify( [leaves[1], fill, merkleTree.layers[1][1]], [false, false, false], root, [leaves[0], badLeaf], // A, E ), - 'MerkleProof: invalid multiproof', + 'MerkleProofInvalidMultiproof', + [], ); - await expectRevert( + await expectRevertCustomError( this.merkleProof.$multiProofVerifyCalldata( [leaves[1], fill, merkleTree.layers[1][1]], [false, false, false], root, [leaves[0], badLeaf], // A, E ), - 'MerkleProof: invalid multiproof', + 'MerkleProofInvalidMultiproof', + [], ); }); @@ -176,5 +178,30 @@ contract('MerkleProof', function () { expect(await this.merkleProof.$multiProofVerify([root], [], root, [])).to.equal(true); expect(await this.merkleProof.$multiProofVerifyCalldata([root], [], root, [])).to.equal(true); }); + + it('reverts processing manipulated proofs with a zero-value node at depth 1', async function () { + // Create a merkle tree that contains a zero leaf at depth 1 + const leaves = [keccak256('real leaf'), Buffer.alloc(32, 0)]; + const merkleTree = new MerkleTree(leaves, keccak256, { sortPairs: true }); + + const root = merkleTree.getRoot(); + + // Now we can pass any **malicious** fake leaves as valid! + const maliciousLeaves = ['malicious', 'leaves'].map(keccak256).sort(Buffer.compare); + const maliciousProof = [leaves[0], leaves[0]]; + const maliciousProofFlags = [true, true, false]; + + await expectRevertCustomError( + this.merkleProof.$multiProofVerify(maliciousProof, maliciousProofFlags, root, maliciousLeaves), + 'MerkleProofInvalidMultiproof', + [], + ); + + await expectRevertCustomError( + this.merkleProof.$multiProofVerifyCalldata(maliciousProof, maliciousProofFlags, root, maliciousLeaves), + 'MerkleProofInvalidMultiproof', + [], + ); + }); }); }); diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index df459c5f82c..afd822b1788 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -2,6 +2,7 @@ const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); 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'); @@ -204,6 +205,19 @@ contract('Math', function () { }); describe('ceilDiv', function () { + it('reverts on zero division', async function () { + const a = new BN('2'); + const b = new BN('0'); + // It's unspecified because it's a low level 0 division error + await expectRevert.unspecified(this.math.$ceilDiv(a, b)); + }); + + 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'); + }); + it('does not round up on exact division', async function () { const a = new BN('10'); const b = new BN('5'); @@ -233,6 +247,10 @@ contract('Math', function () { await expectRevert.unspecified(this.math.$mulDiv(1, 1, 0, Rounding.Down)); }); + it('reverts with result higher than 2 ^ 256', async function () { + await expectRevertCustomError(this.math.$mulDiv(5, MAX_UINT256, 2, Rounding.Down), 'MathOverflowedMulDiv', []); + }); + describe('does round down', async function () { it('small values', async function () { expect(await this.math.$mulDiv('3', '4', '5', Rounding.Down)).to.be.bignumber.equal('2'); diff --git a/test/utils/math/SafeCast.test.js b/test/utils/math/SafeCast.test.js index 63223f5d1ba..4b8ec5a7203 100644 --- a/test/utils/math/SafeCast.test.js +++ b/test/utils/math/SafeCast.test.js @@ -1,6 +1,7 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { range } = require('../../../scripts/helpers'); +const { expectRevertCustomError } = require('../../helpers/customError'); const SafeCast = artifacts.require('$SafeCast'); @@ -26,16 +27,18 @@ contract('SafeCast', async function () { }); it(`reverts when downcasting 2^${bits} (${maxValue.addn(1)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toUint${bits}`](maxValue.addn(1)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedUintDowncast`, + [bits, maxValue.addn(1)], ); }); it(`reverts when downcasting 2^${bits} + 1 (${maxValue.addn(2)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toUint${bits}`](maxValue.addn(2)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedUintDowncast`, + [bits, maxValue.addn(2)], ); }); }); @@ -60,11 +63,11 @@ contract('SafeCast', async function () { }); it('reverts when casting -1', async function () { - await expectRevert(this.safeCast.$toUint256(-1), 'SafeCast: value must be positive'); + await expectRevertCustomError(this.safeCast.$toUint256(-1), `SafeCastOverflowedIntToUint`, [-1]); }); it(`reverts when casting INT256_MIN (${minInt256})`, async function () { - await expectRevert(this.safeCast.$toUint256(minInt256), 'SafeCast: value must be positive'); + await expectRevertCustomError(this.safeCast.$toUint256(minInt256), `SafeCastOverflowedIntToUint`, [minInt256]); }); }); @@ -94,30 +97,34 @@ contract('SafeCast', async function () { }); it(`reverts when downcasting -2^${bits - 1} - 1 (${minValue.subn(1)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toInt${bits}`](minValue.subn(1)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedIntDowncast`, + [bits, minValue.subn(1)], ); }); it(`reverts when downcasting -2^${bits - 1} - 2 (${minValue.subn(2)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toInt${bits}`](minValue.subn(2)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedIntDowncast`, + [bits, minValue.subn(2)], ); }); it(`reverts when downcasting 2^${bits - 1} (${maxValue.addn(1)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toInt${bits}`](maxValue.addn(1)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedIntDowncast`, + [bits, maxValue.addn(1)], ); }); it(`reverts when downcasting 2^${bits - 1} + 1 (${maxValue.addn(2)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toInt${bits}`](maxValue.addn(2)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedIntDowncast`, + [bits, maxValue.addn(2)], ); }); }); @@ -142,11 +149,13 @@ contract('SafeCast', async function () { }); it(`reverts when casting INT256_MAX + 1 (${maxInt256.addn(1)})`, async function () { - await expectRevert(this.safeCast.$toInt256(maxInt256.addn(1)), "SafeCast: value doesn't fit in an int256"); + await expectRevertCustomError(this.safeCast.$toInt256(maxInt256.addn(1)), 'SafeCastOverflowedUintToInt', [ + maxInt256.addn(1), + ]); }); it(`reverts when casting UINT256_MAX (${maxUint256})`, async function () { - await expectRevert(this.safeCast.$toInt256(maxUint256), "SafeCast: value doesn't fit in an int256"); + await expectRevertCustomError(this.safeCast.$toInt256(maxUint256), 'SafeCastOverflowedUintToInt', [maxUint256]); }); }); }); diff --git a/test/utils/structs/Checkpoints.test.js b/test/utils/structs/Checkpoints.test.js index ad95373a48a..936ac565af6 100644 --- a/test/utils/structs/Checkpoints.test.js +++ b/test/utils/structs/Checkpoints.test.js @@ -1,7 +1,10 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); +require('@openzeppelin/test-helpers'); + const { expect } = require('chai'); 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'); @@ -20,6 +23,7 @@ contract('Checkpoints', function () { 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), @@ -33,6 +37,11 @@ contract('Checkpoints', function () { }); 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)); + }); + it('returns zero as latest value', async function () { expect(await this.methods.latest()).to.be.bignumber.equal('0'); @@ -63,6 +72,14 @@ 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); + } + }); + it('length', async function () { expect(await this.methods.length()).to.be.bignumber.equal(this.checkpoints.length.toString()); }); @@ -77,7 +94,11 @@ contract('Checkpoints', function () { }); it('cannot push values in the past', async function () { - await expectRevert(this.methods.push(last(this.checkpoints).key - 1, '0'), 'Checkpoint: decreasing keys'); + await expectRevertCustomError( + this.methods.push(last(this.checkpoints).key - 1, '0'), + 'CheckpointUnorderedInsertion', + [], + ); }); it('can update last value', async function () { diff --git a/test/utils/structs/DoubleEndedQueue.test.js b/test/utils/structs/DoubleEndedQueue.test.js index 2fbb8dc2580..cbf37d76b79 100644 --- a/test/utils/structs/DoubleEndedQueue.test.js +++ b/test/utils/structs/DoubleEndedQueue.test.js @@ -30,10 +30,10 @@ contract('DoubleEndedQueue', function () { }); it('reverts on accesses', async function () { - await expectRevertCustomError(this.deque.$popBack(0), 'Empty()'); - await expectRevertCustomError(this.deque.$popFront(0), 'Empty()'); - await expectRevertCustomError(this.deque.$back(0), 'Empty()'); - await expectRevertCustomError(this.deque.$front(0), 'Empty()'); + 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', []); }); }); @@ -54,7 +54,7 @@ contract('DoubleEndedQueue', function () { }); it('out of bounds access', async function () { - await expectRevertCustomError(this.deque.$at(0, this.content.length), 'OutOfBounds()'); + await expectRevertCustomError(this.deque.$at(0, this.content.length), 'QueueOutOfBounds', []); }); describe('push', function () { diff --git a/test/utils/structs/EnumerableMap.behavior.js b/test/utils/structs/EnumerableMap.behavior.js index 3db45df6aa9..67b19e39a2c 100644 --- a/test/utils/structs/EnumerableMap.behavior.js +++ b/test/utils/structs/EnumerableMap.behavior.js @@ -1,7 +1,8 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const zip = require('lodash.zip'); +const { expectRevertCustomError } = require('../../helpers/customError'); function shouldBehaveLikeMap(keys, values, zeroValue, methods, events) { const [keyA, keyB, keyC] = keys; @@ -150,7 +151,10 @@ function shouldBehaveLikeMap(keys, values, zeroValue, methods, events) { expect(await methods.get(this.map, keyA).then(r => r.toString())).to.be.equal(valueA.toString()); }); it('missing value', async function () { - await expectRevert(methods.get(this.map, keyB), 'EnumerableMap: nonexistent key'); + const key = web3.utils.toHex(keyB); + await expectRevertCustomError(methods.get(this.map, keyB), 'EnumerableMapNonexistentKey', [ + key.length == 66 ? key : web3.utils.padLeft(key, 64, '0'), + ]); }); });