Преглед на файлове

Replace revert strings with custom errors (#4261)

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Co-authored-by: Francisco <fg@frang.io>
Ernesto García преди 2 години
родител
ревизия
b425a72240
променени са 100 файла, в които са добавени 2423 реда и са изтрити 807 реда
  1. 5 0
      .changeset/lovely-geckos-hide.md
  2. 14 0
      GUIDELINES.md
  3. 7 14
      contracts/access/AccessControl.sol
  4. 25 11
      contracts/access/AccessControlDefaultAdminRules.sol
  5. 14 2
      contracts/access/IAccessControl.sol
  6. 22 0
      contracts/access/IAccessControlDefaultAdminRules.sol
  7. 16 2
      contracts/access/Ownable.sol
  8. 3 1
      contracts/access/Ownable2Step.sol
  9. 8 1
      contracts/finance/VestingWallet.sol
  10. 69 27
      contracts/governance/Governor.sol
  11. 57 0
      contracts/governance/IGovernor.sol
  12. 63 13
      contracts/governance/TimelockController.sol
  13. 12 7
      contracts/governance/compatibility/GovernorCompatibilityBravo.sol
  14. 5 0
      contracts/governance/compatibility/IGovernorCompatibilityBravo.sol
  15. 4 2
      contracts/governance/extensions/GovernorCountingSimple.sol
  16. 3 1
      contracts/governance/extensions/GovernorSettings.sol
  17. 14 6
      contracts/governance/extensions/GovernorTimelockCompound.sol
  18. 8 1
      contracts/governance/extensions/GovernorTimelockControl.sol
  19. 9 4
      contracts/governance/extensions/GovernorVotesQuorumFraction.sol
  20. 10 0
      contracts/governance/extensions/IGovernorTimelock.sol
  21. 5 0
      contracts/governance/utils/IVotes.sol
  22. 25 5
      contracts/governance/utils/Votes.sol
  23. 162 0
      contracts/interfaces/draft-IERC6093.sol
  24. 37 5
      contracts/metatx/MinimalForwarder.sol
  25. 50 0
      contracts/mocks/AddressFnPointersMock.sol
  26. 0 1
      contracts/mocks/token/ERC721ConsecutiveMock.sol
  27. 11 2
      contracts/proxy/Clones.sol
  28. 40 9
      contracts/proxy/ERC1967/ERC1967Upgrade.sol
  29. 8 1
      contracts/proxy/beacon/UpgradeableBeacon.sol
  30. 14 2
      contracts/proxy/transparent/TransparentUpgradeableProxy.sol
  31. 22 7
      contracts/proxy/utils/Initializable.sol
  32. 17 3
      contracts/proxy/utils/UUPSUpgradeable.sol
  33. 17 3
      contracts/security/Pausable.sol
  34. 8 1
      contracts/security/ReentrancyGuard.sol
  35. 52 25
      contracts/token/ERC1155/ERC1155.sol
  36. 6 8
      contracts/token/ERC1155/extensions/ERC1155Burnable.sol
  37. 1 2
      contracts/token/ERC1155/extensions/ERC1155Pausable.sol
  38. 1 4
      contracts/token/ERC1155/extensions/ERC1155Supply.sol
  39. 37 14
      contracts/token/ERC20/ERC20.sol
  40. 20 4
      contracts/token/ERC20/extensions/ERC20Capped.sol
  41. 25 6
      contracts/token/ERC20/extensions/ERC20FlashMint.sol
  42. 1 2
      contracts/token/ERC20/extensions/ERC20Pausable.sol
  43. 16 2
      contracts/token/ERC20/extensions/ERC20Permit.sol
  44. 10 1
      contracts/token/ERC20/extensions/ERC20Votes.sol
  45. 11 2
      contracts/token/ERC20/extensions/ERC20Wrapper.sol
  46. 36 4
      contracts/token/ERC20/extensions/ERC4626.sol
  47. 24 8
      contracts/token/ERC20/utils/SafeERC20.sol
  48. 53 25
      contracts/token/ERC721/ERC721.sol
  49. 3 2
      contracts/token/ERC721/extensions/ERC721Burnable.sol
  50. 39 5
      contracts/token/ERC721/extensions/ERC721Consecutive.sol
  51. 19 3
      contracts/token/ERC721/extensions/ERC721Enumerable.sol
  52. 1 1
      contracts/token/ERC721/extensions/ERC721Pausable.sol
  53. 3 1
      contracts/token/ERC721/extensions/ERC721URIStorage.sol
  54. 15 3
      contracts/token/ERC721/extensions/ERC721Wrapper.sol
  55. 36 4
      contracts/token/common/ERC2981.sol
  56. 85 32
      contracts/utils/Address.sol
  57. 24 3
      contracts/utils/Create2.sol
  58. 16 0
      contracts/utils/Nonces.sol
  59. 1 1
      contracts/utils/StorageSlot.sol
  60. 11 3
      contracts/utils/Strings.sol
  61. 37 17
      contracts/utils/cryptography/ECDSA.sol
  62. 11 2
      contracts/utils/cryptography/MerkleProof.sol
  63. 1 1
      contracts/utils/cryptography/SignatureChecker.sol
  64. 8 1
      contracts/utils/math/Math.sol
  65. 212 64
      contracts/utils/math/SafeCast.sol
  66. 11 2
      contracts/utils/structs/Checkpoints.sol
  67. 12 12
      contracts/utils/structs/DoubleEndedQueue.sol
  68. 8 1
      contracts/utils/structs/EnumerableMap.sol
  69. 11 1
      scripts/generate/templates/Checkpoints.js
  70. 8 1
      scripts/generate/templates/EnumerableMap.js
  71. 35 4
      scripts/generate/templates/SafeCast.js
  72. 1 1
      scripts/generate/templates/StorageSlot.js
  73. 69 57
      test/access/AccessControl.behavior.js
  74. 1 1
      test/access/AccessControl.test.js
  75. 4 3
      test/access/AccessControlDefaultAdminRules.test.js
  76. 2 2
      test/access/AccessControlEnumerable.test.js
  77. 14 5
      test/access/Ownable.test.js
  78. 12 9
      test/access/Ownable2Step.test.js
  79. 5 3
      test/finance/VestingWallet.test.js
  80. 118 42
      test/governance/Governor.test.js
  81. 94 63
      test/governance/TimelockController.test.js
  82. 42 19
      test/governance/compatibility/GovernorCompatibilityBravo.test.js
  83. 7 2
      test/governance/extensions/GovernorPreventLateQuorum.test.js
  84. 42 17
      test/governance/extensions/GovernorTimelockCompound.test.js
  85. 58 31
      test/governance/extensions/GovernorTimelockControl.test.js
  86. 21 9
      test/governance/extensions/GovernorVotesQuorumFraction.test.js
  87. 31 8
      test/governance/utils/Votes.behavior.js
  88. 7 2
      test/governance/utils/Votes.test.js
  89. 32 11
      test/helpers/customError.js
  90. 1 0
      test/helpers/enums.js
  91. 39 0
      test/helpers/governance.js
  92. 67 65
      test/metatx/MinimalForwarder.test.js
  93. 4 2
      test/proxy/Clones.test.js
  94. 7 2
      test/proxy/beacon/BeaconProxy.test.js
  95. 10 10
      test/proxy/beacon/UpgradeableBeacon.test.js
  96. 12 7
      test/proxy/transparent/ProxyAdmin.test.js
  97. 10 8
      test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js
  98. 13 12
      test/proxy/utils/Initializable.test.js
  99. 8 7
      test/proxy/utils/UUPSUpgradeable.test.js
  100. 8 7
      test/security/Pausable.test.js

+ 5 - 0
.changeset/lovely-geckos-hide.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': major
+---
+
+Replace revert strings and require statements with custom errors.

+ 14 - 0
GUIDELINES.md

@@ -115,3 +115,17 @@ In addition to the official Solidity Style Guide we have a number of other conve
   ```
 
 * 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<number>` 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.

+ 7 - 14
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);
     }
 
     /**

+ 25 - 11
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;

+ 14 - 2
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;
 }

+ 22 - 0
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`

+ 16 - 2
contracts/access/Ownable.sol

@@ -20,6 +20,16 @@ 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);
 
     /**
@@ -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);
     }
 

+ 3 - 1
contracts/access/Ownable2Step.sol

@@ -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);
     }
 }

+ 8 - 1
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;

+ 69 - 27
contracts/governance/Governor.sol

@@ -47,6 +47,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
     }
     // 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 +70,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
      * governance protocol (since v4.6).
      */
     modifier onlyGovernance() {
-        require(_msgSender() == _executor(), "Governor: onlyGovernance");
+        if (_msgSender() != _executor()) {
+            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 +92,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();
+        }
     }
 
     /**
@@ -174,7 +179,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();
@@ -275,17 +280,24 @@ 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();
@@ -327,10 +339,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 +367,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 +387,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 +445,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);
@@ -570,7 +593,10 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
         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);
         _countVote(proposalId, account, support, weight, params);
@@ -592,7 +618,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);
     }
 
     /**
@@ -631,6 +657,22 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
     }
 
     /**
+     * @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

+ 57 - 0
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.
      */

+ 63 - 13
contracts/governance/TimelockController.sol

@@ -6,6 +6,7 @@ pragma solidity ^0.8.19;
 import "../access/AccessControl.sol";
 import "../token/ERC721/IERC721Receiver.sol";
 import "../token/ERC1155/IERC1155Receiver.sol";
+import "../utils/Address.sol";
 
 /**
  * @dev Contract module which acts as a timelocked controller. When set as the
@@ -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`.
      */
@@ -243,8 +276,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 +294,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 +312,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 +366,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 +387,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,7 +424,9 @@ 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;
     }

+ 12 - 7
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);
     }
@@ -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();
         }
     }
 }

+ 5 - 0
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.

+ 4 - 2
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();
         }
     }
 }

+ 3 - 1
contracts/governance/extensions/GovernorSettings.sol

@@ -93,7 +93,9 @@ abstract contract GovernorSettings is Governor {
      */
     function _setVotingPeriod(uint256 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;
     }

+ 14 - 6
contracts/governance/extensions/GovernorTimelockCompound.sol

@@ -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);
 
         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);

+ 8 - 1
contracts/governance/extensions/GovernorTimelockControl.sol

@@ -95,7 +95,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);

+ 9 - 4
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));

+ 10 - 0
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);

+ 5 - 0
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.
      */

+ 25 - 5
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);
     }
 

+ 162 - 0
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 ERC1155InsufficientApprovalForAll(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);
+}

+ 37 - 5
contracts/metatx/MinimalForwarder.sol

@@ -31,6 +31,16 @@ contract MinimalForwarder is EIP712 {
 
     mapping(address => uint256) private _nonces;
 
+    /**
+     * @dev The request `from` doesn't match with the recovered `signer`.
+     */
+    error MinimalForwarderInvalidSigner(address signer, address from);
+
+    /**
+     * @dev The request nonce doesn't match with the `current` nonce for the request signer.
+     */
+    error MinimalForwarderInvalidNonce(address signer, uint256 current);
+
     constructor() EIP712("MinimalForwarder", "0.0.1") {}
 
     function getNonce(address from) public view returns (uint256) {
@@ -38,17 +48,25 @@ contract MinimalForwarder is EIP712 {
     }
 
     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;
+        address signer = _recover(req, signature);
+        (bool correctFrom, bool correctNonce) = _validateReq(req, signer);
+        return correctFrom && correctNonce;
     }
 
     function execute(
         ForwardRequest calldata req,
         bytes calldata signature
     ) public payable returns (bool, bytes memory) {
-        require(verify(req, signature), "MinimalForwarder: signature does not match request");
+        address signer = _recover(req, signature);
+        (bool correctFrom, bool correctNonce) = _validateReq(req, signer);
+
+        if (!correctFrom) {
+            revert MinimalForwarderInvalidSigner(signer, req.from);
+        }
+        if (!correctNonce) {
+            revert MinimalForwarderInvalidNonce(signer, _nonces[req.from]);
+        }
+
         _nonces[req.from] = req.nonce + 1;
 
         (bool success, bytes memory returndata) = req.to.call{gas: req.gas, value: req.value}(
@@ -69,4 +87,18 @@ contract MinimalForwarder is EIP712 {
 
         return (success, returndata);
     }
+
+    function _recover(ForwardRequest calldata req, bytes calldata signature) internal view returns (address) {
+        return
+            _hashTypedDataV4(
+                keccak256(abi.encode(_TYPEHASH, req.from, req.to, req.value, req.gas, req.nonce, keccak256(req.data)))
+            ).recover(signature);
+    }
+
+    function _validateReq(
+        ForwardRequest calldata req,
+        address signer
+    ) internal view returns (bool correctFrom, bool correctNonce) {
+        return (signer == req.from, _nonces[req.from] == req.nonce);
+    }
 }

+ 50 - 0
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 {}
+}

+ 0 - 1
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";
 

+ 11 - 2
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();
+        }
     }
 
     /**

+ 40 - 9
contracts/proxy/ERC1967/ERC1967Upgrade.sol

@@ -26,6 +26,26 @@ abstract contract ERC1967Upgrade is IERC1967 {
      */
     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 The storage `slot` is unsupported as a UUID.
+     */
+    error ERC1967UnsupportedProxiableUUID(bytes32 slot);
+
     /**
      * @dev Returns the current implementation address.
      */
@@ -37,7 +57,9 @@ abstract contract ERC1967Upgrade is IERC1967 {
      * @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");
+        if (newImplementation.code.length == 0) {
+            revert ERC1967InvalidImplementation(newImplementation);
+        }
         StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
     }
 
@@ -76,9 +98,12 @@ abstract contract ERC1967Upgrade is IERC1967 {
             _setImplementation(newImplementation);
         } else {
             try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) {
-                require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID");
+                if (slot != _IMPLEMENTATION_SLOT) {
+                    revert ERC1967UnsupportedProxiableUUID(slot);
+                }
             } catch {
-                revert("ERC1967Upgrade: new implementation is not UUPS");
+                // The implementation is not UUPS
+                revert ERC1967InvalidImplementation(newImplementation);
             }
             _upgradeToAndCall(newImplementation, data, forceCall);
         }
@@ -106,7 +131,9 @@ abstract contract ERC1967Upgrade is IERC1967 {
      * @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");
+        if (newAdmin == address(0)) {
+            revert ERC1967InvalidAdmin(address(0));
+        }
         StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin;
     }
 
@@ -137,11 +164,15 @@ abstract contract ERC1967Upgrade is IERC1967 {
      * @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"
-        );
+        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;
     }
 

+ 8 - 1
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;
     }
 }

+ 14 - 2
contracts/proxy/transparent/TransparentUpgradeableProxy.sol

@@ -52,6 +52,16 @@ interface ITransparentUpgradeableProxy is IERC1967 {
  * render the admin operations inaccessible, which could prevent upgradeability. Transparency may also be compromised.
  */
 contract TransparentUpgradeableProxy is ERC1967Proxy {
+    /**
+     * @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}.
@@ -74,7 +84,7 @@ contract TransparentUpgradeableProxy is ERC1967Proxy {
             } 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))
@@ -127,6 +137,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();
+        }
     }
 }

+ 22 - 7
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);

+ 17 - 3
contracts/proxy/utils/UUPSUpgradeable.sol

@@ -22,6 +22,11 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade {
     /// @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 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 +35,14 @@ 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
+            revert UUPSUnauthorizedCallContext();
+        }
+        if (_getImplementation() != __self) {
+            // Must be called through an active proxy
+            revert UUPSUnauthorizedCallContext();
+        }
         _;
     }
 
@@ -40,7 +51,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();
+        }
         _;
     }
 

+ 17 - 3
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();
+        }
     }
 
     /**

+ 8 - 1
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;

+ 52 - 25
contracts/token/ERC1155/ERC1155.sol

@@ -8,6 +8,7 @@ import "./IERC1155Receiver.sol";
 import "./extensions/IERC1155MetadataURI.sol";
 import "../../utils/Context.sol";
 import "../../utils/introspection/ERC165.sol";
+import "../../interfaces/draft-IERC6093.sol";
 
 /**
  * @dev Implementation of the basic standard multi-token.
@@ -16,7 +17,7 @@ import "../../utils/introspection/ERC165.sol";
  *
  * _Available since v3.1._
  */
-contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI {
+contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IERC1155Errors {
     // Mapping from token ID to account balances
     mapping(uint256 => mapping(address => uint256)) private _balances;
 
@@ -79,7 +80,9 @@ 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);
 
@@ -108,10 +111,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 ERC1155InsufficientApprovalForAll(_msgSender(), from);
+        }
         _safeTransferFrom(from, to, id, amount, data);
     }
 
@@ -125,10 +127,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 ERC1155InsufficientApprovalForAll(_msgSender(), from);
+        }
         _safeBatchTransferFrom(from, to, ids, amounts, data);
     }
 
@@ -149,7 +150,9 @@ 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();
 
@@ -159,7 +162,9 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI {
 
             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;
                 }
@@ -198,8 +203,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 +230,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 +274,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 +293,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 +310,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 +327,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 +339,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 +357,14 @@ 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 Error(string memory reason) {
                 revert(reason);
             } catch {
-                revert("ERC1155: transfer to non-ERC1155Receiver implementer");
+                // non-ERC1155Receiver implementer
+                revert ERC1155InvalidReceiver(to);
             }
         }
     }
@@ -357,12 +382,14 @@ 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 Error(string memory reason) {
                 revert(reason);
             } catch {
-                revert("ERC1155: transfer to non-ERC1155Receiver implementer");
+                // non-ERC1155Receiver implementer
+                revert ERC1155InvalidReceiver(to);
             }
         }
     }

+ 6 - 8
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 ERC1155InsufficientApprovalForAll(_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 ERC1155InsufficientApprovalForAll(_msgSender(), account);
+        }
 
         _burnBatch(account, ids, values);
     }

+ 1 - 2
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);
     }
 }

+ 1 - 4
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;
                 }

+ 37 - 14
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 {
+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,9 +311,12 @@ 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");
-
+        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);
     }
@@ -311,7 +332,9 @@ 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);
             }

+ 20 - 4
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);
     }
 }

+ 25 - 6
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)) {

+ 1 - 2
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);
     }
 }

+ 16 - 2
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);
     }

+ 10 - 1
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);
     }

+ 11 - 2
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;

+ 36 - 4
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);

+ 24 - 8
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));
+        }
     }
 
     /**

+ 53 - 25
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 {
+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;
     }
 
@@ -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 {

+ 3 - 2
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);
     }
 }

+ 39 - 5
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);

+ 19 - 3
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;

+ 1 - 1
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();
     }
 }

+ 3 - 1
contracts/token/ERC721/extensions/ERC721URIStorage.sol

@@ -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);

+ 15 - 3
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;
     }

+ 36 - 4
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);
     }

+ 85 - 32
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();
         }
     }
 }

+ 24 - 3
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();
+        }
     }
 
     /**

+ 16 - 0
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;
+    }
 }

+ 1 - 1
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;
  *     }
  * }

+ 11 - 3
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.
      */
@@ -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);
     }
 

+ 37 - 17
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;
     }
 

+ 11 - 2
contracts/utils/cryptography/MerkleProof.sol

@@ -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
@@ -124,7 +129,9 @@ library MerkleProof {
         uint256 totalHashes = proofFlags.length;
 
         // Check proof validity.
-        require(leavesLen + proof.length - 1 == totalHashes, "MerkleProof: invalid multiproof");
+        if (leavesLen + proof.length - 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".
@@ -176,7 +183,9 @@ library MerkleProof {
         uint256 totalHashes = proofFlags.length;
 
         // Check proof validity.
-        require(leavesLen + proof.length - 1 == totalHashes, "MerkleProof: invalid multiproof");
+        if (leavesLen + proof.length - 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".

+ 1 - 1
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);

+ 8 - 1
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
@@ -140,7 +145,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.

+ 212 - 64
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);
     }
 }

+ 11 - 2
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) {

+ 12 - 12
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];
     }
 

+ 8 - 1
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;
     }
 

+ 11 - 1
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)),
   '}',
 );

+ 8 - 1
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;
 }
 

+ 35 - 4
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)],
   '}',
 );

+ 1 - 1
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;
  *     }
  * }

+ 69 - 57
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');
         });
       }

+ 1 - 1
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);
 });

+ 4 - 3
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);
 });

+ 2 - 2
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);
 });

+ 14 - 5
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 () {

+ 12 - 9
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);
     });
   });
 });

+ 5 - 3
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],
     );
   });
 

+ 118 - 42
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([], '<proposal description>');
-          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) {
             },
             '<proposal description>',
           );
-          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) {
             },
             '<proposal description>',
           );
-          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) {
             },
             '<proposal description>',
           );
-          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(),
               },
             ],
             '<proposal description>',
@@ -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 () {

+ 94 - 63
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));

+ 42 - 19
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,
           },
           '<proposal description>',
         );
 
-        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,
+          ]);
         });
       });
     });

+ 7 - 2
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 () {

+ 42 - 17
test/governance/extensions/GovernorTimelockCompound.test.js

@@ -3,7 +3,8 @@ 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');
 
@@ -129,7 +130,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 +142,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], '<proposal description>');
+            const { id } = this.helper.setProposal([action, action], '<proposal description>');
 
             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 +162,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 +190,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 +205,11 @@ 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]),
+            ]);
           });
         });
       });
@@ -214,7 +224,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 +241,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 +256,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 +306,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 () {

+ 58 - 31
test/governance/extensions/GovernorTimelockControl.test.js

@@ -2,7 +2,8 @@ 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');
 
@@ -163,7 +164,11 @@ contract('GovernorTimelockControl', 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]),
+                ]);
               });
             });
 
@@ -176,7 +181,10 @@ contract('GovernorTimelockControl', function (accounts) {
 
                 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');
+                await expectRevertCustomError(this.helper.execute(), 'TimelockUnexpectedOperationState', [
+                  this.proposal.timelockid,
+                  Enums.OperationState.Ready,
+                ]);
               });
 
               it('if too early', async function () {
@@ -188,7 +196,10 @@ contract('GovernorTimelockControl', function (accounts) {
 
                 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');
+                await expectRevertCustomError(this.helper.execute(), 'TimelockUnexpectedOperationState', [
+                  this.proposal.timelockid,
+                  Enums.OperationState.Ready,
+                ]);
               });
 
               it('if already executed', async function () {
@@ -199,7 +210,11 @@ contract('GovernorTimelockControl', 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]),
+                ]);
               });
 
               it('if already executed by another proposer', async function () {
@@ -216,7 +231,11 @@ contract('GovernorTimelockControl', function (accounts) {
                   this.proposal.shortProposal[3],
                 );
 
-                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]),
+                ]);
               });
             });
           });
@@ -231,7 +250,11 @@ contract('GovernorTimelockControl', 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 () {
@@ -244,7 +267,11 @@ contract('GovernorTimelockControl', 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]),
+              ]);
             });
 
             it('cancel on timelock is reflected on governor', async function () {
@@ -271,9 +298,12 @@ contract('GovernorTimelockControl', 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],
                 );
               });
 
@@ -346,28 +376,21 @@ contract('GovernorTimelockControl', function (accounts) {
               });
 
               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 },
-                );
+                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 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',
+                await expectRevertCustomError(
+                  this.timelock.execute(target, value, data, predecessor, salt, { from: owner }),
+                  'QueueEmpty', // Bubbled up from Governor
+                  [],
                 );
               });
             });
@@ -383,7 +406,11 @@ contract('GovernorTimelockControl', 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 () {

+ 21 - 9
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(),
               },
             ],
             '<proposal description>',
@@ -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,
+          ]);
         });
       });
     });

+ 31 - 8
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 () {

+ 7 - 2
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 () {

+ 32 - 11
test/helpers/customError.js

@@ -1,21 +1,42 @@
-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) {
+async function expectRevertCustomError(promise, expectedErrorName, args) {
   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);
-      }
+    if (!Array.isArray(args)) {
+      expect.fail('Expected 3rd array parameter for error arguments');
+    }
+    // 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] = revert.message.match(/'(.*)'/);
+
+    // Attempt to parse as an error
+    const match = error.match(/(?<name>\w+)\((?<args>.*)\)/);
+    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`);
   }
 }
 

+ 1 - 0
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'),
 };

+ 39 - 0
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,
 };

+ 67 - 65
test/metatx/MinimalForwarder.test.js

@@ -1,8 +1,9 @@
 const ethSigUtil = require('eth-sig-util');
 const Wallet = require('ethereumjs-wallet').default;
 const { getDomain, domainType } = require('../helpers/eip712');
+const { expectRevertCustomError } = require('../helpers/customError');
 
-const { expectRevert, constants } = require('@openzeppelin/test-helpers');
+const { constants, expectRevert } = require('@openzeppelin/test-helpers');
 const { expect } = require('chai');
 
 const MinimalForwarder = artifacts.require('MinimalForwarder');
@@ -27,6 +28,14 @@ contract('MinimalForwarder', function (accounts) {
   });
 
   context('with message', function () {
+    const tamperedValues = {
+      from: accounts[0],
+      to: accounts[0],
+      value: web3.utils.toWei('1'),
+      nonce: 1234,
+      data: '0x1742',
+    };
+
     beforeEach(async function () {
       this.wallet = Wallet.generate();
       this.sender = web3.utils.toChecksumAddress(this.wallet.getAddressString());
@@ -38,14 +47,15 @@ contract('MinimalForwarder', function (accounts) {
         nonce: Number(await this.forwarder.getNonce(this.sender)),
         data: '0x',
       };
-      this.sign = () =>
+      this.forgeData = req => ({
+        types: this.types,
+        domain: this.domain,
+        primaryType: 'ForwardRequest',
+        message: { ...this.req, ...req },
+      });
+      this.sign = req =>
         ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), {
-          data: {
-            types: this.types,
-            domain: this.domain,
-            primaryType: 'ForwardRequest',
-            message: this.req,
-          },
+          data: this.forgeData(req),
         });
     });
 
@@ -64,31 +74,29 @@ contract('MinimalForwarder', function (accounts) {
         });
       });
 
-      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 () {
+      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, this.sign())).to.be.equal(
+              false,
+            );
+          });
+        }
+
+        it('returns false with 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);
         });
+
+        it('returns false with valid signature for non-current nonce', async function () {
+          const req = {
+            ...this.req,
+            nonce: this.req.nonce + 1,
+          };
+          const sig = this.sign(req);
+          expect(await this.forwarder.verify(req, sig)).to.be.equal(false);
+        });
       });
     });
 
@@ -109,44 +117,38 @@ contract('MinimalForwarder', function (accounts) {
         });
       });
 
-      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',
+      context('with tampered values', function () {
+        for (const [key, value] of Object.entries(tamperedValues)) {
+          it(`reverts with tampered ${key}`, async function () {
+            const sig = this.sign();
+            const data = this.forgeData({ [key]: value });
+            await expectRevertCustomError(this.forwarder.execute(data.message, sig), 'MinimalForwarderInvalidSigner', [
+              ethSigUtil.recoverTypedSignature({ data, sig }),
+              data.message.from,
+            ]);
+          });
+        }
+
+        it('reverts with tampered signature', async function () {
+          const tamperedSig = web3.utils.hexToBytes(this.sign());
+          tamperedSig[42] ^= 0xff;
+          await expectRevertCustomError(
+            this.forwarder.execute(this.req, web3.utils.bytesToHex(tamperedSig)),
+            'MinimalForwarderInvalidSigner',
+            [ethSigUtil.recoverTypedSignature({ data: this.forgeData(), sig: tamperedSig }), this.req.from],
           );
         });
-        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('reverts with valid signature for non-current nonce', async function () {
+          const req = {
+            ...this.req,
+            nonce: this.req.nonce + 1,
+          };
+          const sig = this.sign(req);
+          await expectRevertCustomError(this.forwarder.execute(req, sig), 'MinimalForwarderInvalidNonce', [
+            this.req.from,
+            this.req.nonce,
+          ]);
         });
       });
 

+ 4 - 2
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 () {

+ 7 - 2
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,
+      ]);
     });
   });
 

+ 10 - 10
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,
+      ]);
     });
   });
 });

+ 12 - 7
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,6 +6,9 @@ const ProxyAdmin = artifacts.require('ProxyAdmin');
 const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy');
 const ITransparentUpgradeableProxy = artifacts.require('ITransparentUpgradeableProxy');
 
+const { getAddressInSlot, ImplementationSlot, AdminSlot } = require('../../helpers/erc1967');
+const { expectRevertCustomError } = require('../../helpers/customError');
+
 contract('ProxyAdmin', function (accounts) {
   const [proxyAdminOwner, newAdmin, anotherAccount] = accounts;
 
@@ -32,9 +34,10 @@ contract('ProxyAdmin', function (accounts) {
 
   describe('#changeProxyAdmin', function () {
     it('fails to change proxy admin if its not the proxy owner', async function () {
-      await expectRevert(
+      await expectRevertCustomError(
         this.proxyAdmin.changeProxyAdmin(this.proxy.address, newAdmin, { from: anotherAccount }),
-        'caller is not the owner',
+        'OwnableUnauthorizedAccount',
+        [anotherAccount],
       );
     });
 
@@ -49,9 +52,10 @@ contract('ProxyAdmin', function (accounts) {
   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 +74,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],
         );
       });
     });

+ 10 - 8
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');
@@ -67,10 +68,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,
+          ]);
         });
       });
     });
@@ -289,9 +289,10 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx
 
     describe('when the new proposed admin is the zero address', function () {
       it('reverts', async function () {
-        await expectRevert(
+        await expectRevertCustomError(
           this.proxy.changeAdmin(ZERO_ADDRESS, { from: proxyAdminAddress }),
-          'ERC1967: new admin is the zero address',
+          'ERC1967InvalidAdmin',
+          [ZERO_ADDRESS],
         );
       });
     });
@@ -306,9 +307,10 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx
     });
 
     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',
+        [],
       );
     });
 

+ 13 - 12
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 () {

+ 8 - 7
test/proxy/utils/UUPSUpgradeable.test.js

@@ -1,6 +1,7 @@
-const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
+const { expectEvent } = require('@openzeppelin/test-helpers');
 const { web3 } = require('@openzeppelin/test-helpers/src/setup');
 const { getSlot, ImplementationSlot } = require('../../helpers/erc1967');
+const { expectRevertCustomError } = require('../../helpers/customError');
 
 const ERC1967Proxy = artifacts.require('ERC1967Proxy');
 const UUPSUpgradeableMock = artifacts.require('UUPSUpgradeableMock');
@@ -47,9 +48,10 @@ contract('UUPSUpgradeable', function () {
 
   // delegate to a non existing upgradeTo function causes a low level revert
   it('reject upgrade to non uups implementation', async function () {
-    await expectRevert(
+    await expectRevertCustomError(
       this.instance.upgradeTo(this.implUpgradeNonUUPS.address),
-      'ERC1967Upgrade: new implementation is not UUPS',
+      'ERC1967InvalidImplementation',
+      [this.implUpgradeNonUUPS.address],
     );
   });
 
@@ -57,10 +59,9 @@ contract('UUPSUpgradeable', function () {
     const { address } = await ERC1967Proxy.new(this.implInitial.address, '0x');
     const otherInstance = await UUPSUpgradeableMock.at(address);
 
-    await expectRevert(
-      this.instance.upgradeTo(otherInstance.address),
-      'ERC1967Upgrade: new implementation is not UUPS',
-    );
+    await expectRevertCustomError(this.instance.upgradeTo(otherInstance.address), 'ERC1967InvalidImplementation', [
+      otherInstance.address,
+    ]);
   });
 
   it('can upgrade from legacy implementations', async function () {

+ 8 - 7
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', []);
           });
         });
       });

Някои файлове не бяха показани, защото твърде много файлове са промени