From 2a62fb4a2d9b9248aa15bba9c89d75a7ddc8cfc6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 19 Jan 2023 22:34:15 +0100 Subject: [PATCH 001/197] Improve TransparentUpgradeableProxy's transparency (#3977) Co-authored-by: Francisco --- .changeset/many-panthers-hide.md | 5 +++++ .../mocks/proxy/ClashingImplementation.sol | 7 +++---- .../TransparentUpgradeableProxy.sol | 20 +++++++++++++++---- .../TransparentUpgradeableProxy.behaviour.js | 15 +++++++++++--- 4 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 .changeset/many-panthers-hide.md diff --git a/.changeset/many-panthers-hide.md b/.changeset/many-panthers-hide.md new file mode 100644 index 000000000..5f04c99df --- /dev/null +++ b/.changeset/many-panthers-hide.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`TransparentUpgradeableProxy`: support value passthrough for all ifAdmin function. diff --git a/contracts/mocks/proxy/ClashingImplementation.sol b/contracts/mocks/proxy/ClashingImplementation.sol index 80aca0c29..5c272a89d 100644 --- a/contracts/mocks/proxy/ClashingImplementation.sol +++ b/contracts/mocks/proxy/ClashingImplementation.sol @@ -3,12 +3,11 @@ pragma solidity ^0.8.0; /** - * @dev Implementation contract with an admin() function made to clash with - * @dev TransparentUpgradeableProxy's to test correct functioning of the - * @dev Transparent Proxy feature. + * @dev Implementation contract with a payable admin() function made to clash with TransparentUpgradeableProxy's to + * test correct functioning of the Transparent Proxy feature. */ contract ClashingImplementation { - function admin() external pure returns (address) { + function admin() external payable returns (address) { return 0x0000000000000000000000000000000011111142; } diff --git a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol index 3685360e7..155a22e01 100644 --- a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol @@ -55,7 +55,8 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` */ - function admin() external ifAdmin returns (address admin_) { + function admin() external payable ifAdmin returns (address admin_) { + _requireZeroValue(); admin_ = _getAdmin(); } @@ -68,7 +69,8 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` */ - function implementation() external ifAdmin returns (address implementation_) { + function implementation() external payable ifAdmin returns (address implementation_) { + _requireZeroValue(); implementation_ = _implementation(); } @@ -79,7 +81,8 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { * * NOTE: Only the admin can call this function. See {ProxyAdmin-changeProxyAdmin}. */ - function changeAdmin(address newAdmin) external virtual ifAdmin { + function changeAdmin(address newAdmin) external payable virtual ifAdmin { + _requireZeroValue(); _changeAdmin(newAdmin); } @@ -88,7 +91,8 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { * * NOTE: Only the admin can call this function. See {ProxyAdmin-upgrade}. */ - function upgradeTo(address newImplementation) external ifAdmin { + function upgradeTo(address newImplementation) external payable ifAdmin { + _requireZeroValue(); _upgradeToAndCall(newImplementation, bytes(""), false); } @@ -117,4 +121,12 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target"); super._beforeFallback(); } + + /** + * @dev To keep this contract fully transparent, all `ifAdmin` functions must be payable. This helper is here to + * emulate some proxy functions being non-payable while still allowing value to pass through. + */ + function _requireZeroValue() private { + require(msg.value == 0); + } } diff --git a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js index da8f21bee..3a10357a9 100644 --- a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js +++ b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js @@ -330,14 +330,23 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx ); }); - context('when function names clash', function () { + describe('when function names clash', function () { it('when sender is proxy admin should run the proxy function', async function () { - const value = await this.proxy.admin.call({ from: proxyAdminAddress }); + const value = await this.proxy.admin.call({ from: proxyAdminAddress, value: 0 }); expect(value).to.be.equal(proxyAdminAddress); }); it('when sender is other should delegate to implementation', async function () { - const value = await this.proxy.admin.call({ from: anotherAccount }); + const value = await this.proxy.admin.call({ from: anotherAccount, value: 0 }); + expect(value).to.be.equal('0x0000000000000000000000000000000011111142'); + }); + + it('when sender is proxy admin value should not be accepted', async function () { + await expectRevert.unspecified(this.proxy.admin.call({ from: proxyAdminAddress, value: 1 })); + }); + + it('when sender is other value should be accepted', async function () { + const value = await this.proxy.admin.call({ from: anotherAccount, value: 1 }); expect(value).to.be.equal('0x0000000000000000000000000000000011111142'); }); }); From c404862cba658deae081a9e437942a241eee78c0 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 19 Jan 2023 23:00:53 +0100 Subject: [PATCH 002/197] Use oz-upgrades-unsafe-allow-reachable in ERC1967Upgrade (#3971) Co-authored-by: Francisco --- .changeset/strong-bulldogs-buy.md | 5 +++++ .changeset/yellow-swans-cover.md | 5 +++++ contracts/proxy/ERC1967/ERC1967Upgrade.sol | 2 -- contracts/proxy/utils/UUPSUpgradeable.sol | 4 ++++ 4 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 .changeset/strong-bulldogs-buy.md create mode 100644 .changeset/yellow-swans-cover.md diff --git a/.changeset/strong-bulldogs-buy.md b/.changeset/strong-bulldogs-buy.md new file mode 100644 index 000000000..001b0f88f --- /dev/null +++ b/.changeset/strong-bulldogs-buy.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC1967Upgrade`: removed contract-wide `oz-upgrades-unsafe-allow delegatecall` annotation, replaced by granular annotation in `UUPSUpgradeable`. diff --git a/.changeset/yellow-swans-cover.md b/.changeset/yellow-swans-cover.md new file mode 100644 index 000000000..ee1680178 --- /dev/null +++ b/.changeset/yellow-swans-cover.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`UUPSUpgradeable`: added granular `oz-upgrades-unsafe-allow-reachable` annotation to improve upgrade safety checks on latest version of the Upgrades Plugins (starting with `@openzeppelin/upgrades-core@1.21.0`). diff --git a/contracts/proxy/ERC1967/ERC1967Upgrade.sol b/contracts/proxy/ERC1967/ERC1967Upgrade.sol index 0f32d4d23..0680f3549 100644 --- a/contracts/proxy/ERC1967/ERC1967Upgrade.sol +++ b/contracts/proxy/ERC1967/ERC1967Upgrade.sol @@ -13,8 +13,6 @@ import "../../utils/StorageSlot.sol"; * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots. * * _Available since v4.1._ - * - * @custom:oz-upgrades-unsafe-allow delegatecall */ abstract contract ERC1967Upgrade { // This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1 diff --git a/contracts/proxy/utils/UUPSUpgradeable.sol b/contracts/proxy/utils/UUPSUpgradeable.sol index 8b7366328..bb696f6fa 100644 --- a/contracts/proxy/utils/UUPSUpgradeable.sol +++ b/contracts/proxy/utils/UUPSUpgradeable.sol @@ -62,6 +62,8 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * Calls {_authorizeUpgrade}. * * Emits an {Upgraded} event. + * + * @custom:oz-upgrades-unsafe-allow-reachable delegatecall */ function upgradeTo(address newImplementation) external virtual onlyProxy { _authorizeUpgrade(newImplementation); @@ -75,6 +77,8 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * Calls {_authorizeUpgrade}. * * Emits an {Upgraded} event. + * + * @custom:oz-upgrades-unsafe-allow-reachable delegatecall */ function upgradeToAndCall(address newImplementation, bytes memory data) external payable virtual onlyProxy { _authorizeUpgrade(newImplementation); From b1c2c43d6af6adf0b0a74cc77683b1d13d66e8bc Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Mon, 23 Jan 2023 11:30:11 +0100 Subject: [PATCH 003/197] Lint should not override files (#3988) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 45db40433..b4035b820 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "lint:fix": "npm run lint:js:fix && npm run lint:sol:fix", "lint:js": "prettier --loglevel warn --ignore-path .gitignore '**/*.{js,ts}' --check && eslint --ignore-path .gitignore .", "lint:js:fix": "prettier --loglevel warn --ignore-path .gitignore '**/*.{js,ts}' --write && eslint --ignore-path .gitignore . --fix", - "lint:sol": "prettier --loglevel warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --write && solhint '{contracts,test}/**/*.sol'", + "lint:sol": "prettier --loglevel warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --check && solhint '{contracts,test}/**/*.sol'", "lint:sol:fix": "prettier --loglevel warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --write", "clean": "hardhat clean && rimraf build contracts/build", "prepare": "scripts/prepare.sh", From a34dd8bb1b8e941f6e7f65c58df0b3b994afbc16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 24 Jan 2023 15:00:03 -0600 Subject: [PATCH 004/197] Add `solidity` language to missing code snippets (#3992) --- GUIDELINES.md | 6 +++--- contracts/access/AccessControl.sol | 4 ++-- contracts/proxy/utils/Initializable.sol | 3 ++- contracts/utils/StorageSlot.sol | 2 +- contracts/utils/structs/DoubleEndedQueue.sol | 2 +- contracts/utils/structs/EnumerableMap.sol | 2 +- contracts/utils/structs/EnumerableSet.sol | 2 +- scripts/generate/templates/EnumerableMap.js | 2 +- scripts/generate/templates/EnumerableSet.js | 2 +- 9 files changed, 13 insertions(+), 12 deletions(-) diff --git a/GUIDELINES.md b/GUIDELINES.md index cdeddb90c..bac94be6f 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -72,7 +72,7 @@ In addition to the official Solidity Style Guide we have a number of other conve * Internal or private state variables or functions should have an underscore prefix. - ``` + ```solidity contract TestContract { uint256 private _privateVar; uint256 internal _internalVar; @@ -84,7 +84,7 @@ In addition to the official Solidity Style Guide we have a number of other conve * Events should be emitted immediately after the state change that they represent, and should be named in the past tense. - ``` + ```solidity function _burn(address who, uint256 value) internal { super._burn(who, value); emit TokensBurned(who, value); @@ -96,7 +96,7 @@ In addition to the official Solidity Style Guide we have a number of other conve * Interface names should have a capital I prefix. - ``` + ```solidity interface IERC777 { ``` diff --git a/contracts/access/AccessControl.sol b/contracts/access/AccessControl.sol index 386b85c03..5f2829e74 100644 --- a/contracts/access/AccessControl.sol +++ b/contracts/access/AccessControl.sol @@ -19,14 +19,14 @@ import "../utils/introspection/ERC165.sol"; * in the external API and be unique. The best way to achieve this is by * using `public constant` hash digests: * - * ``` + * ```solidity * bytes32 public constant MY_ROLE = keccak256("MY_ROLE"); * ``` * * Roles can be used to represent a set of permissions. To restrict access to a * function call, use {hasRole}: * - * ``` + * ```solidity * function foo() public { * require(hasRole(MY_ROLE, msg.sender)); * ... diff --git a/contracts/proxy/utils/Initializable.sol b/contracts/proxy/utils/Initializable.sol index b454b5d95..638dbe353 100644 --- a/contracts/proxy/utils/Initializable.sol +++ b/contracts/proxy/utils/Initializable.sol @@ -18,12 +18,13 @@ import "../../utils/Address.sol"; * For example: * * [.hljs-theme-light.nopadding] - * ``` + * ```solidity * contract MyToken is ERC20Upgradeable { * function initialize() initializer public { * __ERC20_init("MyToken", "MTK"); * } * } + * * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable { * function initializeV2() reinitializer(2) public { * __ERC20Permit_init("MyToken"); diff --git a/contracts/utils/StorageSlot.sol b/contracts/utils/StorageSlot.sol index 6ab8f5dc6..d23363bd6 100644 --- a/contracts/utils/StorageSlot.sol +++ b/contracts/utils/StorageSlot.sol @@ -12,7 +12,7 @@ pragma solidity ^0.8.0; * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. * * Example usage to set ERC1967 implementation slot: - * ``` + * ```solidity * contract ERC1967 { * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; * diff --git a/contracts/utils/structs/DoubleEndedQueue.sol b/contracts/utils/structs/DoubleEndedQueue.sol index eae79ada7..6b3ea70e3 100644 --- a/contracts/utils/structs/DoubleEndedQueue.sol +++ b/contracts/utils/structs/DoubleEndedQueue.sol @@ -12,7 +12,7 @@ import "../math/SafeCast.sol"; * * The struct is called `Bytes32Deque`. Other types can be cast to and from `bytes32`. This data structure can only be * used in storage, and not in memory. - * ``` + * ```solidity * DoubleEndedQueue.Bytes32Deque queue; * ``` * diff --git a/contracts/utils/structs/EnumerableMap.sol b/contracts/utils/structs/EnumerableMap.sol index 8b188c734..fb21f02cf 100644 --- a/contracts/utils/structs/EnumerableMap.sol +++ b/contracts/utils/structs/EnumerableMap.sol @@ -17,7 +17,7 @@ import "./EnumerableSet.sol"; * (O(1)). * - Entries are enumerated in O(n). No guarantees are made on the ordering. * - * ``` + * ```solidity * contract Example { * // Add the library methods * using EnumerableMap for EnumerableMap.UintToAddressMap; diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index 4c701c26c..a01f82d41 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -15,7 +15,7 @@ pragma solidity ^0.8.0; * (O(1)). * - Elements are enumerated in O(n). No guarantees are made on the ordering. * - * ``` + * ```solidity * contract Example { * // Add the library methods * using EnumerableSet for EnumerableSet.AddressSet; diff --git a/scripts/generate/templates/EnumerableMap.js b/scripts/generate/templates/EnumerableMap.js index dbd502a0b..2bbb69e3e 100644 --- a/scripts/generate/templates/EnumerableMap.js +++ b/scripts/generate/templates/EnumerableMap.js @@ -25,7 +25,7 @@ import "./EnumerableSet.sol"; * (O(1)). * - Entries are enumerated in O(n). No guarantees are made on the ordering. * - * \`\`\` + * \`\`\`solidity * contract Example { * // Add the library methods * using EnumerableMap for EnumerableMap.UintToAddressMap; diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index a3af2c304..2b6a9c64d 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -22,7 +22,7 @@ pragma solidity ^0.8.0; * (O(1)). * - Elements are enumerated in O(n). No guarantees are made on the ordering. * - * \`\`\` + * \`\`\`solidity * contract Example { * // Add the library methods * using EnumerableSet for EnumerableSet.AddressSet; From a284569a7c611423d1d12d826d67db6d9967923c Mon Sep 17 00:00:00 2001 From: Pascal Marco Caversaccio Date: Wed, 25 Jan 2023 14:54:51 +0100 Subject: [PATCH 005/197] Rename `ERC20ReturnFalseMock copy.sol` to `ERC20ReturnFalseMock.sol` (#4000) --- .../{ERC20ReturnFalseMock copy.sol => ERC20ReturnFalseMock.sol} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contracts/mocks/token/{ERC20ReturnFalseMock copy.sol => ERC20ReturnFalseMock.sol} (100%) diff --git a/contracts/mocks/token/ERC20ReturnFalseMock copy.sol b/contracts/mocks/token/ERC20ReturnFalseMock.sol similarity index 100% rename from contracts/mocks/token/ERC20ReturnFalseMock copy.sol rename to contracts/mocks/token/ERC20ReturnFalseMock.sol From 69c8d1010e1ca53ce3fa7f6fddff96759aec87e1 Mon Sep 17 00:00:00 2001 From: blockeater Date: Thu, 26 Jan 2023 00:46:34 +0500 Subject: [PATCH 006/197] Make upgradeTo and upgradeToAndCall public (#3959) Co-authored-by: Francisco --- .changeset/five-ducks-develop.md | 5 +++++ contracts/mocks/proxy/UUPSLegacy.sol | 4 ++-- contracts/mocks/proxy/UUPSUpgradeableMock.sol | 4 ++-- contracts/proxy/utils/UUPSUpgradeable.sol | 4 ++-- 4 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/five-ducks-develop.md diff --git a/.changeset/five-ducks-develop.md b/.changeset/five-ducks-develop.md new file mode 100644 index 000000000..fe25db071 --- /dev/null +++ b/.changeset/five-ducks-develop.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`UUPSUpgradeable.sol`: Change visibility to the functions `upgradeTo ` and `upgradeToAndCall ` from `external` to `public`. diff --git a/contracts/mocks/proxy/UUPSLegacy.sol b/contracts/mocks/proxy/UUPSLegacy.sol index 7a3002889..6956f5675 100644 --- a/contracts/mocks/proxy/UUPSLegacy.sol +++ b/contracts/mocks/proxy/UUPSLegacy.sol @@ -44,11 +44,11 @@ contract UUPSUpgradeableLegacyMock is UUPSUpgradeableMock { } // hooking into the old mechanism - function upgradeTo(address newImplementation) external override { + function upgradeTo(address newImplementation) public override { _upgradeToAndCallSecureLegacyV1(newImplementation, bytes(""), false); } - function upgradeToAndCall(address newImplementation, bytes memory data) external payable override { + function upgradeToAndCall(address newImplementation, bytes memory data) public payable override { _upgradeToAndCallSecureLegacyV1(newImplementation, data, false); } } diff --git a/contracts/mocks/proxy/UUPSUpgradeableMock.sol b/contracts/mocks/proxy/UUPSUpgradeableMock.sol index deb20a000..f02271c49 100644 --- a/contracts/mocks/proxy/UUPSUpgradeableMock.sol +++ b/contracts/mocks/proxy/UUPSUpgradeableMock.sol @@ -23,11 +23,11 @@ contract UUPSUpgradeableMock is NonUpgradeableMock, UUPSUpgradeable { } contract UUPSUpgradeableUnsafeMock is UUPSUpgradeableMock { - function upgradeTo(address newImplementation) external override { + function upgradeTo(address newImplementation) public override { ERC1967Upgrade._upgradeToAndCall(newImplementation, bytes(""), false); } - function upgradeToAndCall(address newImplementation, bytes memory data) external payable override { + function upgradeToAndCall(address newImplementation, bytes memory data) public payable override { ERC1967Upgrade._upgradeToAndCall(newImplementation, data, false); } } diff --git a/contracts/proxy/utils/UUPSUpgradeable.sol b/contracts/proxy/utils/UUPSUpgradeable.sol index bb696f6fa..4ff026638 100644 --- a/contracts/proxy/utils/UUPSUpgradeable.sol +++ b/contracts/proxy/utils/UUPSUpgradeable.sol @@ -65,7 +65,7 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * * @custom:oz-upgrades-unsafe-allow-reachable delegatecall */ - function upgradeTo(address newImplementation) external virtual onlyProxy { + function upgradeTo(address newImplementation) public virtual onlyProxy { _authorizeUpgrade(newImplementation); _upgradeToAndCallUUPS(newImplementation, new bytes(0), false); } @@ -80,7 +80,7 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * * @custom:oz-upgrades-unsafe-allow-reachable delegatecall */ - function upgradeToAndCall(address newImplementation, bytes memory data) external payable virtual onlyProxy { + function upgradeToAndCall(address newImplementation, bytes memory data) public payable virtual onlyProxy { _authorizeUpgrade(newImplementation); _upgradeToAndCallUUPS(newImplementation, data, true); } From d6fb3d645ff46f059745935b62fcc32c63cf4034 Mon Sep 17 00:00:00 2001 From: Francisco Date: Thu, 26 Jan 2023 14:17:18 -0300 Subject: [PATCH 007/197] Add guidelines for pull requests (#4002) --- GUIDELINES.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/GUIDELINES.md b/GUIDELINES.md index bac94be6f..0f4c9829e 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -62,6 +62,20 @@ Some other examples of automation are: - Looking for common security vulnerabilities or errors in our code (eg. reentrancy analysis). - Keeping dependencies up to date and monitoring for vulnerable dependencies. +## Pull requests + +Pull requests are squash-merged to keep the `master` branch history clean. The title of the pull request becomes the commit message, so it should be written in a consistent format: + +1) Begin with a capital letter. +2) Do not end with a period. +3) Write in the imperative: "Add feature X" and not "Adds feature X" or "Added feature X". + +This repository does not follow conventional commits, so do not prefix the title with "fix:" or "feat:". + +Work in progress pull requests should be submitted as Drafts and should not be prefixed with "WIP:". + +Branch names don't matter, and commit messages within a pull request mostly don't matter either, although they can help the review process. + # Solidity Conventions In addition to the official Solidity Style Guide we have a number of other conventions that must be followed. From 0320a718e8e07b1d932f5acb8ad9cec9d9eed99b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 26 Jan 2023 11:38:34 -0600 Subject: [PATCH 008/197] Fix Governance docs reference to blocktime (#3994) --- docs/modules/ROOT/pages/governance.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index aa27c0ba8..d59e3ce9f 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -137,7 +137,7 @@ votingDelay: How long after a proposal is created should voting power be fixed. votingPeriod: How long does a proposal remain open to votes. -These parameters are specified in number of blocks. Assuming block time of around 13.14 seconds, we will set votingDelay = 1 day = 6570 blocks, and votingPeriod = 1 week = 45992 blocks. +These parameters are specified in number of blocks. Assuming block time of around 12 seconds, we will set votingDelay = 1 day = 7200 blocks, and votingPeriod = 1 week = 50400 blocks. We can optionally set a proposal threshold as well. This restricts proposal creation to accounts who have enough voting power. From 5e28952cbdc0eb7d19ee62580ab31b30c2376e48 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 26 Jan 2023 20:46:11 +0100 Subject: [PATCH 009/197] Add a public Governor.cancel function (#3983) --- .changeset/flat-deers-end.md | 5 + contracts/governance/Governor.sol | 30 +++- contracts/governance/IGovernor.sol | 8 ++ .../GovernorCompatibilityBravo.sol | 9 +- .../IGovernorCompatibilityBravo.sol | 5 - .../GovernorCompatibilityBravoMock.sol | 4 + contracts/mocks/wizard/MyGovernor3.sol | 4 + test/governance/Governor.test.js | 128 +++++++++++++----- .../GovernorCompatibilityBravo.test.js | 6 +- .../GovernorTimelockCompound.test.js | 4 +- .../GovernorTimelockControl.test.js | 4 +- test/helpers/governance.js | 13 +- 12 files changed, 157 insertions(+), 63 deletions(-) create mode 100644 .changeset/flat-deers-end.md diff --git a/.changeset/flat-deers-end.md b/.changeset/flat-deers-end.md new file mode 100644 index 000000000..61895f2cf --- /dev/null +++ b/.changeset/flat-deers-end.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Governor`: add a public `cancel(uint256)` function. diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index 8235fb5ec..f82f9fea8 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -40,6 +40,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive Timers.BlockNumber voteEnd; bool executed; bool canceled; + address proposer; } string private _name; @@ -95,9 +96,11 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive return interfaceId == (type(IGovernor).interfaceId ^ + this.cancel.selector ^ this.castVoteWithReasonAndParams.selector ^ this.castVoteWithReasonAndParamsBySig.selector ^ this.getVotesWithParams.selector) || + interfaceId == (type(IGovernor).interfaceId ^ this.cancel.selector) || interfaceId == type(IGovernor).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); @@ -248,8 +251,10 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive bytes[] memory calldatas, string memory description ) public virtual override returns (uint256) { + address proposer = _msgSender(); + require( - getVotes(_msgSender(), block.number - 1) >= proposalThreshold(), + getVotes(proposer, block.number - 1) >= proposalThreshold(), "Governor: proposer votes below proposal threshold" ); @@ -267,10 +272,11 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive proposal.voteStart.setDeadline(snapshot); proposal.voteEnd.setDeadline(deadline); + proposal.proposer = proposer; emit ProposalCreated( proposalId, - _msgSender(), + proposer, targets, values, new string[](targets.length), @@ -310,6 +316,15 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive return proposalId; } + /** + * @dev See {IGovernor-cancel}. + */ + function cancel(uint256 proposalId) public virtual override { + require(state(proposalId) == ProposalState.Pending, "Governor: too late to cancel"); + require(_msgSender() == _proposals[proposalId].proposer, "Governor: only proposer can cancel"); + _cancel(proposalId); + } + /** * @dev Internal execution mechanism. Can be overridden to implement different execution mechanism */ @@ -375,7 +390,16 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive bytes[] memory calldatas, bytes32 descriptionHash ) internal virtual returns (uint256) { - uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); + return _cancel(hashProposal(targets, values, calldatas, descriptionHash)); + } + + /** + * @dev Internal cancel mechanism: locks up the proposal timer, preventing it from being re-submitted. Marks it as + * canceled to allow distinguishing it from executed proposals. + * + * Emits a {IGovernor-ProposalCanceled} event. + */ + function _cancel(uint256 proposalId) internal virtual returns (uint256) { ProposalState status = state(proposalId); require( diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index eb3d1fc05..18cf57fb1 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -216,6 +216,14 @@ abstract contract IGovernor is IERC165 { bytes32 descriptionHash ) public payable virtual returns (uint256 proposalId); + /** + * @dev Cancel a proposal. This is restricted to Pending proposal (before the vote starts) and is restricted to + * the proposal's proposer. + * + * Emits a {ProposalCanceled} event. + */ + function cancel(uint256 proposalId) public virtual; + /** * @dev Cast a vote * diff --git a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol index 8d74742c5..912d02f8a 100644 --- a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol @@ -99,7 +99,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp ); } - function cancel(uint256 proposalId) public virtual override { + function cancel(uint256 proposalId) public virtual override(IGovernor, Governor) { ProposalDetails storage details = _proposalDetails[proposalId]; require( @@ -107,12 +107,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp "GovernorBravo: proposer above threshold" ); - _cancel( - details.targets, - details.values, - _encodeCalldata(details.signatures, details.calldatas), - details.descriptionHash - ); + _cancel(proposalId); } /** diff --git a/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol b/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol index d1ec0337d..159c55100 100644 --- a/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol @@ -90,11 +90,6 @@ abstract contract IGovernorCompatibilityBravo is IGovernor { */ function execute(uint256 proposalId) public payable virtual; - /** - * @dev Cancels a proposal only if sender is the proposer, or proposer delegates dropped below proposal threshold. - */ - function cancel(uint256 proposalId) public virtual; - /** * @dev Part of the Governor Bravo's interface: _"Gets actions of a proposal"_. */ diff --git a/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol b/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol index 4356bce7f..2727794f6 100644 --- a/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol +++ b/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol @@ -66,6 +66,10 @@ abstract contract GovernorCompatibilityBravoMock is return super.execute(targets, values, calldatas, salt); } + function cancel(uint256 proposalId) public override(Governor, GovernorCompatibilityBravo, IGovernor) { + super.cancel(proposalId); + } + function _execute( uint256 proposalId, address[] memory targets, diff --git a/contracts/mocks/wizard/MyGovernor3.sol b/contracts/mocks/wizard/MyGovernor3.sol index 320342290..4192cae94 100644 --- a/contracts/mocks/wizard/MyGovernor3.sol +++ b/contracts/mocks/wizard/MyGovernor3.sol @@ -54,6 +54,10 @@ contract MyGovernor is return super.propose(targets, values, calldatas, description); } + function cancel(uint256 proposalId) public override(Governor, GovernorCompatibilityBravo, IGovernor) { + super.cancel(proposalId); + } + function _execute( uint256 proposalId, address[] memory targets, diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js index cc3cc17ef..4c083ed92 100644 --- a/test/governance/Governor.test.js +++ b/test/governance/Governor.test.js @@ -382,55 +382,109 @@ contract('Governor', function (accounts) { }); describe('cancel', function () { - it('before proposal', async function () { - await expectRevert(this.helper.cancel(), 'Governor: unknown proposal id'); + describe('internal', function () { + it('before proposal', async function () { + await expectRevert(this.helper.cancel('internal'), 'Governor: unknown proposal id'); + }); + + it('after proposal', async function () { + await this.helper.propose(); + + await this.helper.cancel('internal'); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + + await this.helper.waitForSnapshot(); + await expectRevert( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'Governor: vote not currently active', + ); + }); + + it('after vote', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + + await this.helper.cancel('internal'); + 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'); + }); + + it('after deadline', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + 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'); + }); + + it('after execution', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + await expectRevert(this.helper.cancel('internal'), 'Governor: proposal not active'); + }); }); - it('after proposal', async function () { - await this.helper.propose(); + describe('public', function () { + it('before proposal', async function () { + await expectRevert(this.helper.cancel('external'), 'Governor: unknown proposal id'); + }); - await this.helper.cancel(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + it('after proposal', async function () { + await this.helper.propose(); - await this.helper.waitForSnapshot(); - await expectRevert( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: vote not currently active', - ); - }); + await this.helper.cancel('external'); + }); - it('after vote', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + it('after proposal - restricted to proposer', async function () { + await this.helper.propose(); - await this.helper.cancel(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + await expectRevert(this.helper.cancel('external', { from: owner }), 'Governor: only proposer can cancel'); + }); - await this.helper.waitForDeadline(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); + it('after vote started', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(1); // snapshot + 1 block - it('after deadline', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); + await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel'); + }); - await this.helper.cancel(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + it('after vote', 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 expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel'); + }); - it('after execution', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); + it('after deadline', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); - await expectRevert(this.helper.cancel(), 'Governor: proposal not active'); + await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel'); + }); + + it('after execution', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel'); + }); }); }); diff --git a/test/governance/compatibility/GovernorCompatibilityBravo.test.js b/test/governance/compatibility/GovernorCompatibilityBravo.test.js index 231d57b27..3666c4548 100644 --- a/test/governance/compatibility/GovernorCompatibilityBravo.test.js +++ b/test/governance/compatibility/GovernorCompatibilityBravo.test.js @@ -226,18 +226,18 @@ contract('GovernorCompatibilityBravo', function (accounts) { describe('cancel', function () { it('proposer can cancel', async function () { await this.helper.propose({ from: proposer }); - await this.helper.cancel({ from: proposer }); + await this.helper.cancel('external', { from: proposer }); }); it('anyone can cancel if proposer drop below threshold', async function () { await this.helper.propose({ from: proposer }); await this.token.transfer(voter1, web3.utils.toWei('1'), { from: proposer }); - await this.helper.cancel(); + await this.helper.cancel('external'); }); it('cannot cancel is proposer is still above threshold', async function () { await this.helper.propose({ from: proposer }); - await expectRevert(this.helper.cancel(), 'GovernorBravo: proposer above threshold'); + await expectRevert(this.helper.cancel('external'), 'GovernorBravo: proposer above threshold'); }); }); }); diff --git a/test/governance/extensions/GovernorTimelockCompound.test.js b/test/governance/extensions/GovernorTimelockCompound.test.js index 334659e07..fb6803383 100644 --- a/test/governance/extensions/GovernorTimelockCompound.test.js +++ b/test/governance/extensions/GovernorTimelockCompound.test.js @@ -193,7 +193,7 @@ contract('GovernorTimelockCompound', function (accounts) { await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); await this.helper.waitForDeadline(); - expectEvent(await this.helper.cancel(), 'ProposalCanceled', { proposalId: this.proposal.id }); + 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'); @@ -206,7 +206,7 @@ contract('GovernorTimelockCompound', function (accounts) { await this.helper.waitForDeadline(); await this.helper.queue(); - expectEvent(await this.helper.cancel(), 'ProposalCanceled', { proposalId: this.proposal.id }); + 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'); diff --git a/test/governance/extensions/GovernorTimelockControl.test.js b/test/governance/extensions/GovernorTimelockControl.test.js index 2e8c75888..4f9b9df73 100644 --- a/test/governance/extensions/GovernorTimelockControl.test.js +++ b/test/governance/extensions/GovernorTimelockControl.test.js @@ -185,7 +185,7 @@ contract('GovernorTimelockControl', function (accounts) { await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); await this.helper.waitForDeadline(); - expectEvent(await this.helper.cancel(), 'ProposalCanceled', { proposalId: this.proposal.id }); + 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'); @@ -198,7 +198,7 @@ contract('GovernorTimelockControl', function (accounts) { await this.helper.waitForDeadline(); await this.helper.queue(); - expectEvent(await this.helper.cancel(), 'ProposalCanceled', { proposalId: this.proposal.id }); + 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'); diff --git a/test/helpers/governance.js b/test/helpers/governance.js index ff341aa12..f933f389d 100644 --- a/test/helpers/governance.js +++ b/test/helpers/governance.js @@ -62,14 +62,19 @@ class GovernorHelper { ); } - cancel(opts = null) { + cancel(visibility = 'external', opts = null) { const proposal = this.currentProposal; - return proposal.useCompatibilityInterface - ? this.governor.methods['cancel(uint256)'](...concatOpts([proposal.id], opts)) - : this.governor.methods['$_cancel(address[],uint256[],bytes[],bytes32)']( + switch (visibility) { + case 'external': + return this.governor.methods['cancel(uint256)'](...concatOpts([proposal.id], opts)); + case 'internal': + return this.governor.methods['$_cancel(address[],uint256[],bytes[],bytes32)']( ...concatOpts(proposal.shortProposal, opts), ); + default: + throw new Error(`unsuported visibility "${visibility}"`); + } } vote(vote = {}, opts = null) { From bc6de21fe229520b33d13a4d60bbeb1bc5f91bcd Mon Sep 17 00:00:00 2001 From: JulissaDantes Date: Fri, 27 Jan 2023 13:47:43 -0500 Subject: [PATCH 010/197] Create ProposalSalt event for TimelockController (#4001) Co-authored-by: Francisco --- .changeset/five-poets-mix.md | 5 ++++ contracts/governance/TimelockController.sol | 15 ++++++++++-- test/governance/TimelockController.test.js | 23 +++++++++++++++++++ .../GovernorTimelockControl.test.js | 3 +++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 .changeset/five-poets-mix.md diff --git a/.changeset/five-poets-mix.md b/.changeset/five-poets-mix.md new file mode 100644 index 000000000..f5050b246 --- /dev/null +++ b/.changeset/five-poets-mix.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`TimelockController`: Add the `CallSalt` event to emit on operation schedule. diff --git a/contracts/governance/TimelockController.sol b/contracts/governance/TimelockController.sol index dac55d858..615b4c0e3 100644 --- a/contracts/governance/TimelockController.sol +++ b/contracts/governance/TimelockController.sol @@ -51,6 +51,11 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver */ event CallExecuted(bytes32 indexed id, uint256 indexed index, address target, uint256 value, bytes data); + /** + * @dev Emitted when new proposal is scheduled with non-zero salt. + */ + event CallSalt(bytes32 indexed id, bytes32 salt); + /** * @dev Emitted when operation `id` is cancelled. */ @@ -206,7 +211,7 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver /** * @dev Schedule an operation containing a single transaction. * - * Emits a {CallScheduled} event. + * Emits events {CallScheduled} and {CallSalt}. * * Requirements: * @@ -223,12 +228,15 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver bytes32 id = hashOperation(target, value, data, predecessor, salt); _schedule(id, delay); emit CallScheduled(id, 0, target, value, data, predecessor, delay); + if (salt != bytes32(0)) { + emit CallSalt(id, salt); + } } /** * @dev Schedule an operation containing a batch of transactions. * - * Emits one {CallScheduled} event per transaction in the batch. + * Emits a {CallSalt} event and one {CallScheduled} event per transaction in the batch. * * Requirements: * @@ -250,6 +258,9 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver for (uint256 i = 0; i < targets.length; ++i) { emit CallScheduled(id, i, targets[i], values[i], payloads[i], predecessor, delay); } + if (salt != bytes32(0)) { + emit CallSalt(id, salt); + } } /** diff --git a/test/governance/TimelockController.test.js b/test/governance/TimelockController.test.js index 83352ef85..dde923564 100644 --- a/test/governance/TimelockController.test.js +++ b/test/governance/TimelockController.test.js @@ -158,6 +158,11 @@ contract('TimelockController', function (accounts) { delay: MINDELAY, }); + expectEvent(receipt, 'CallSalt', { + id: this.operation.id, + salt: this.operation.salt, + }); + const block = await web3.eth.getBlock(receipt.receipt.blockHash); expect(await this.mock.getTimestamp(this.operation.id)).to.be.bignumber.equal( @@ -219,6 +224,19 @@ contract('TimelockController', function (accounts) { 'TimelockController: insufficient delay', ); }); + + it('schedule operation with salt zero', async function () { + const { receipt } = await this.mock.schedule( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + ZERO_BYTES32, + MINDELAY, + { from: proposer }, + ); + expectEvent.notEmitted(receipt, 'CallSalt'); + }); }); describe('execute', function () { @@ -364,6 +382,11 @@ contract('TimelockController', function (accounts) { predecessor: this.operation.predecessor, delay: MINDELAY, }); + + expectEvent(receipt, 'CallSalt', { + id: this.operation.id, + salt: this.operation.salt, + }); } const block = await web3.eth.getBlock(receipt.receipt.blockHash); diff --git a/test/governance/extensions/GovernorTimelockControl.test.js b/test/governance/extensions/GovernorTimelockControl.test.js index 4f9b9df73..34fdfe92f 100644 --- a/test/governance/extensions/GovernorTimelockControl.test.js +++ b/test/governance/extensions/GovernorTimelockControl.test.js @@ -106,6 +106,9 @@ contract('GovernorTimelockControl', function (accounts) { expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { id: this.proposal.timelockid }); + await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallSalt', { + id: this.proposal.timelockid, + }); expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); await expectEvent.inTransaction(txExecute.tx, this.timelock, 'CallExecuted', { id: this.proposal.timelockid }); From 1684c579223f02c9e78db3fbcf7a2106a6f7efe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Sat, 28 Jan 2023 17:21:07 -0600 Subject: [PATCH 011/197] Add `ERC*Pausable` warning for public pausing mechanism (#4007) Co-authored-by: Francisco --- .changeset/new-ways-own.md | 5 +++++ contracts/token/ERC1155/extensions/ERC1155Pausable.sol | 6 ++++++ contracts/token/ERC20/extensions/ERC20Pausable.sol | 6 ++++++ contracts/token/ERC721/extensions/ERC721Pausable.sol | 6 ++++++ 4 files changed, 23 insertions(+) create mode 100644 .changeset/new-ways-own.md diff --git a/.changeset/new-ways-own.md b/.changeset/new-ways-own.md new file mode 100644 index 000000000..f940bfeb7 --- /dev/null +++ b/.changeset/new-ways-own.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ERC20Pausable`, `ERC721Pausable`, `ERC1155Pausable`: Add note regarding missing public pausing functionality diff --git a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol index 64790e2aa..66ad55e7d 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol @@ -13,6 +13,12 @@ import "../../../security/Pausable.sol"; * period, or having an emergency switch for freezing all token transfers in the * event of a large bug. * + * IMPORTANT: This contract does not include public pause and unpause functions. In + * addition to inheriting this contract, you must define both functions, invoking the + * {Pausable-_pause} and {Pausable-_unpause} internal functions, with appropriate + * access control, e.g. using {AccessControl} or {Ownable}. Not doing so will + * make the contract unpausable. + * * _Available since v3.1._ */ abstract contract ERC1155Pausable is ERC1155, Pausable { diff --git a/contracts/token/ERC20/extensions/ERC20Pausable.sol b/contracts/token/ERC20/extensions/ERC20Pausable.sol index d0b224f4b..e011f08c0 100644 --- a/contracts/token/ERC20/extensions/ERC20Pausable.sol +++ b/contracts/token/ERC20/extensions/ERC20Pausable.sol @@ -12,6 +12,12 @@ import "../../../security/Pausable.sol"; * Useful for scenarios such as preventing trades until the end of an evaluation * period, or having an emergency switch for freezing all token transfers in the * event of a large bug. + * + * IMPORTANT: This contract does not include public pause and unpause functions. In + * addition to inheriting this contract, you must define both functions, invoking the + * {Pausable-_pause} and {Pausable-_unpause} internal functions, with appropriate + * access control, e.g. using {AccessControl} or {Ownable}. Not doing so will + * make the contract unpausable. */ abstract contract ERC20Pausable is ERC20, Pausable { /** diff --git a/contracts/token/ERC721/extensions/ERC721Pausable.sol b/contracts/token/ERC721/extensions/ERC721Pausable.sol index 4726540f4..cdd729b26 100644 --- a/contracts/token/ERC721/extensions/ERC721Pausable.sol +++ b/contracts/token/ERC721/extensions/ERC721Pausable.sol @@ -12,6 +12,12 @@ import "../../../security/Pausable.sol"; * Useful for scenarios such as preventing trades until the end of an evaluation * period, or having an emergency switch for freezing all token transfers in the * event of a large bug. + * + * IMPORTANT: This contract does not include public pause and unpause functions. In + * addition to inheriting this contract, you must define both functions, invoking the + * {Pausable-_pause} and {Pausable-_unpause} internal functions, with appropriate + * access control, e.g. using {AccessControl} or {Ownable}. Not doing so will + * make the contract unpausable. */ abstract contract ERC721Pausable is ERC721, Pausable { /** From 591c12d22de283a297e2b551175ccc914c938391 Mon Sep 17 00:00:00 2001 From: Francisco Date: Mon, 30 Jan 2023 21:00:56 -0300 Subject: [PATCH 012/197] Remove outdated note about virtual view functions (#4014) --- docs/modules/ROOT/pages/extending-contracts.adoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/modules/ROOT/pages/extending-contracts.adoc b/docs/modules/ROOT/pages/extending-contracts.adoc index 7330fc997..a440f4067 100644 --- a/docs/modules/ROOT/pages/extending-contracts.adoc +++ b/docs/modules/ROOT/pages/extending-contracts.adoc @@ -66,8 +66,6 @@ contract ModifiedAccessControl is AccessControl { The `super.revokeRole` statement at the end will invoke ``AccessControl``'s original version of `revokeRole`, the same code that would've run if there were no overrides in place. -NOTE: As of v3.0.0, `view` functions are not `virtual` in OpenZeppelin, and therefore cannot be overridden. We're considering https://github.com/OpenZeppelin/openzeppelin-contracts/issues/2154[lifting this restriction] in an upcoming release. Let us know if this is something you care about! - [[using-hooks]] == Using Hooks From 501a78e13426ec10249e44386239c74783478dd0 Mon Sep 17 00:00:00 2001 From: Mio Date: Tue, 31 Jan 2023 21:11:37 +0800 Subject: [PATCH 013/197] Fix typos (#4015) --- contracts/governance/IGovernor.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 18cf57fb1..60a4b5470 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -58,7 +58,7 @@ abstract contract IGovernor is IERC165 { * @dev Emitted when a vote is cast with params. * * Note: `support` values should be seen as buckets. Their interpretation depends on the voting module used. - * `params` are additional encoded parameters. Their intepepretation also depends on the voting module used. + * `params` are additional encoded parameters. Their interpepretation also depends on the voting module used. */ event VoteCastWithParams( address indexed voter, @@ -140,7 +140,7 @@ abstract contract IGovernor is IERC165 { /** * @notice module:user-config - * @dev Delay, in number of block, between the proposal is created and the vote starts. This can be increassed to + * @dev Delay, in number of block, between the proposal is created and the vote starts. This can be increased to * leave time for users to buy voting power, or delegate it, before the voting of a proposal starts. */ function votingDelay() public view virtual returns (uint256); From 2d05db171a99e93132abef98bda15534129f7805 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Tue, 31 Jan 2023 17:27:05 -0300 Subject: [PATCH 014/197] Add note about supply limit in ERC1155Supply --- contracts/token/ERC1155/extensions/ERC1155Supply.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/token/ERC1155/extensions/ERC1155Supply.sol b/contracts/token/ERC1155/extensions/ERC1155Supply.sol index 6596c4aed..fadc8f9ff 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Supply.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Supply.sol @@ -12,6 +12,9 @@ import "../ERC1155.sol"; * clearly identified. Note: While a totalSupply of 1 might mean the * corresponding is an NFT, there is no guarantees that no other token with the * same id are not going to be minted. + * + * NOTE: This contract implies a global limit of 2**256 - 1 to the number of tokens + * that can be minted. */ abstract contract ERC1155Supply is ERC1155 { mapping(uint256 => uint256) private _totalSupply; From a70ee4e3bbee565e49217c9ec59de5354f8af9d8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 31 Jan 2023 21:45:24 +0100 Subject: [PATCH 015/197] Check storage layout consistency in PRs (#3967) Co-authored-by: Francisco --- .github/actions/storage-layout/action.yml | 55 +++++++++ .github/workflows/checks.yml | 4 + package-lock.json | 144 ++++++++++++++++++++++ package.json | 1 + scripts/checks/compare-layout.js | 20 +++ scripts/checks/extract-layout.js | 40 ++++++ 6 files changed, 264 insertions(+) create mode 100644 .github/actions/storage-layout/action.yml create mode 100644 scripts/checks/compare-layout.js create mode 100644 scripts/checks/extract-layout.js diff --git a/.github/actions/storage-layout/action.yml b/.github/actions/storage-layout/action.yml new file mode 100644 index 000000000..e3e104a01 --- /dev/null +++ b/.github/actions/storage-layout/action.yml @@ -0,0 +1,55 @@ +name: Compare storage layouts +inputs: + token: + description: github token + required: true + buildinfo: + description: compilation artifacts + required: false + default: artifacts/build-info/*.json + layout: + description: extracted storage layout + required: false + default: HEAD.layout.json + out_layout: + description: storage layout to upload + required: false + default: ${{ github.ref_name }}.layout.json + ref_layout: + description: storage layout for the reference branch + required: false + default: ${{ github.base_ref }}.layout.json + +runs: + using: composite + steps: + - name: Extract layout + run: | + node scripts/checks/extract-layout.js ${{ inputs.buildinfo }} > ${{ inputs.layout }} + shell: bash + - name: Download reference + if: github.event_name == 'pull_request' + run: | + RUN_ID=`gh run list --repo ${{ github.repository }} --branch ${{ github.base_ref }} --workflow ${{ github.workflow }} --limit 100 --json 'conclusion,databaseId,event' --jq 'map(select(.conclusion=="success" and .event!="pull_request"))[0].databaseId'` + gh run download ${RUN_ID} --repo ${{ github.repository }} -n layout + env: + GITHUB_TOKEN: ${{ inputs.token }} + shell: bash + continue-on-error: true + id: reference + - name: Compare layouts + if: steps.reference.outcome == 'success' && github.event_name == 'pull_request' + run: | + node scripts/checks/compare-layout.js --head ${{ inputs.layout }} --ref ${{ inputs.ref_layout }} >> $GITHUB_STEP_SUMMARY + shell: bash + - name: Rename artifacts for upload + if: github.event_name != 'pull_request' + run: | + mv ${{ inputs.layout }} ${{ inputs.out_layout }} + shell: bash + - name: Save artifacts + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v3 + with: + name: layout + path: ${{ inputs.out_layout }} diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 17d9e3578..02664e85c 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -42,6 +42,10 @@ jobs: uses: ./.github/actions/gas-compare with: token: ${{ github.token }} + - name: Check storage layout + uses: ./.github/actions/storage-layout + with: + token: ${{ github.token }} foundry-tests: if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' diff --git a/package-lock.json b/package-lock.json index 51a71854c..51d350ac7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@nomiclabs/hardhat-web3": "^2.0.0", "@openzeppelin/docs-utils": "^0.1.3", "@openzeppelin/test-helpers": "^0.5.13", + "@openzeppelin/upgrades-core": "^1.20.6", "chai": "^4.2.0", "eslint": "^8.30.0", "eslint-config-prettier": "^8.5.0", @@ -2405,6 +2406,58 @@ "semver": "bin/semver" } }, + "node_modules/@openzeppelin/upgrades-core": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.20.6.tgz", + "integrity": "sha512-KWdtlahm+iunlAlzLsdpBueanwEx0LLPfAkDL1p0C4SPjMiUqHHFlyGtmmWwdiqDpJ//605vfwkd5RqfnFrHSg==", + "dev": true, + "dependencies": { + "cbor": "^8.0.0", + "chalk": "^4.1.0", + "compare-versions": "^5.0.0", + "debug": "^4.1.1", + "ethereumjs-util": "^7.0.3", + "proper-lockfile": "^4.1.1", + "solidity-ast": "^0.4.15" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/cbor": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", + "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", + "dev": true, + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=12.19" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "dev": true, + "engines": { + "node": ">=12.19" + } + }, "node_modules/@scure/base": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", @@ -4571,6 +4624,12 @@ "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==", "dev": true }, + "node_modules/compare-versions": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.3.tgz", + "integrity": "sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -11430,6 +11489,17 @@ "asap": "~2.0.6" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -12019,6 +12089,15 @@ "node": ">=4" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -17993,6 +18072,48 @@ } } }, + "@openzeppelin/upgrades-core": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.20.6.tgz", + "integrity": "sha512-KWdtlahm+iunlAlzLsdpBueanwEx0LLPfAkDL1p0C4SPjMiUqHHFlyGtmmWwdiqDpJ//605vfwkd5RqfnFrHSg==", + "dev": true, + "requires": { + "cbor": "^8.0.0", + "chalk": "^4.1.0", + "compare-versions": "^5.0.0", + "debug": "^4.1.1", + "ethereumjs-util": "^7.0.3", + "proper-lockfile": "^4.1.1", + "solidity-ast": "^0.4.15" + }, + "dependencies": { + "cbor": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", + "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", + "dev": true, + "requires": { + "nofilter": "^3.1.0" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "dev": true + } + } + }, "@scure/base": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", @@ -19767,6 +19888,12 @@ "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==", "dev": true }, + "compare-versions": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.3.tgz", + "integrity": "sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -25154,6 +25281,17 @@ "asap": "~2.0.6" } }, + "proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -25598,6 +25736,12 @@ "signal-exit": "^3.0.2" } }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", diff --git a/package.json b/package.json index b4035b820..de2951bd1 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@nomiclabs/hardhat-web3": "^2.0.0", "@openzeppelin/docs-utils": "^0.1.3", "@openzeppelin/test-helpers": "^0.5.13", + "@openzeppelin/upgrades-core": "^1.20.6", "chai": "^4.2.0", "eslint": "^8.30.0", "eslint-config-prettier": "^8.5.0", diff --git a/scripts/checks/compare-layout.js b/scripts/checks/compare-layout.js new file mode 100644 index 000000000..ff8cb712e --- /dev/null +++ b/scripts/checks/compare-layout.js @@ -0,0 +1,20 @@ +const fs = require('fs'); +const { getStorageUpgradeReport } = require('@openzeppelin/upgrades-core/dist/storage'); + +const { ref, head } = require('yargs').argv; + +const oldLayout = JSON.parse(fs.readFileSync(ref)); +const newLayout = JSON.parse(fs.readFileSync(head)); + +for (const name in oldLayout) { + if (name in newLayout) { + const report = getStorageUpgradeReport(oldLayout[name], newLayout[name], {}); + if (!report.ok) { + console.log(`ERROR: Storage incompatibility in ${name}`); + console.log(report.explain()); + process.exitCode = 1; + } + } else { + console.log(`WARNING: ${name} is missing from the current branch`); + } +} diff --git a/scripts/checks/extract-layout.js b/scripts/checks/extract-layout.js new file mode 100644 index 000000000..d0c9cf36b --- /dev/null +++ b/scripts/checks/extract-layout.js @@ -0,0 +1,40 @@ +const fs = require('fs'); +const { findAll } = require('solidity-ast/utils'); +const { astDereferencer } = require('@openzeppelin/upgrades-core/dist/ast-dereferencer'); +const { solcInputOutputDecoder } = require('@openzeppelin/upgrades-core/dist/src-decoder'); +const { extractStorageLayout } = require('@openzeppelin/upgrades-core/dist/storage/extract'); + +const { _ } = require('yargs').argv; + +const skipPath = ['contracts/mocks/', 'contracts-exposed/']; +const skipKind = ['interface', 'library']; + +function extractLayouts(path) { + const layout = {}; + const { input, output } = JSON.parse(fs.readFileSync(path)); + + const decoder = solcInputOutputDecoder(input, output); + const deref = astDereferencer(output); + + for (const src in output.contracts) { + if (skipPath.some(prefix => src.startsWith(prefix))) { + continue; + } + + for (const contractDef of findAll('ContractDefinition', output.sources[src].ast)) { + if (skipKind.includes(contractDef.contractKind)) { + continue; + } + + layout[contractDef.name] = extractStorageLayout( + contractDef, + decoder, + deref, + output.contracts[src][contractDef.name].storageLayout, + ); + } + } + return layout; +} + +console.log(JSON.stringify(Object.assign(..._.map(extractLayouts)))); From 91e8d0ba3c3beb8a1db31310d8599664e48639ef Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 1 Feb 2023 22:46:05 +0100 Subject: [PATCH 016/197] Add `string` and `bytes` support to the `StorageSlots` library (#4008) Co-authored-by: Francisco --- .changeset/modern-games-exist.md | 5 ++ contracts/mocks/StorageSlotMock.sol | 38 +++++++- contracts/utils/StorageSlot.sol | 52 ++++++++++- scripts/generate/run.js | 1 + scripts/generate/templates/StorageSlot.js | 87 +++++++++++++++++++ scripts/helpers.js | 5 ++ test/utils/StorageSlot.test.js | 100 ++++++++++++++++++++++ 7 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 .changeset/modern-games-exist.md create mode 100644 scripts/generate/templates/StorageSlot.js diff --git a/.changeset/modern-games-exist.md b/.changeset/modern-games-exist.md new file mode 100644 index 000000000..bd89b4f16 --- /dev/null +++ b/.changeset/modern-games-exist.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`StorageSlot`: Add support for `string` and `bytes`. diff --git a/contracts/mocks/StorageSlotMock.sol b/contracts/mocks/StorageSlotMock.sol index 5d099fca8..1da577c19 100644 --- a/contracts/mocks/StorageSlotMock.sol +++ b/contracts/mocks/StorageSlotMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "../utils/StorageSlot.sol"; contract StorageSlotMock { - using StorageSlot for bytes32; + using StorageSlot for *; function setBoolean(bytes32 slot, bool value) public { slot.getBooleanSlot().value = value; @@ -38,4 +38,40 @@ contract StorageSlotMock { function getUint256(bytes32 slot) public view returns (uint256) { return slot.getUint256Slot().value; } + + mapping(uint256 => string) public stringMap; + + function setString(bytes32 slot, string calldata value) public { + slot.getStringSlot().value = value; + } + + function setStringStorage(uint256 key, string calldata value) public { + stringMap[key].getStringSlot().value = value; + } + + function getString(bytes32 slot) public view returns (string memory) { + return slot.getStringSlot().value; + } + + function getStringStorage(uint256 key) public view returns (string memory) { + return stringMap[key].getStringSlot().value; + } + + mapping(uint256 => bytes) public bytesMap; + + function setBytes(bytes32 slot, bytes calldata value) public { + slot.getBytesSlot().value = value; + } + + function setBytesStorage(uint256 key, bytes calldata value) public { + bytesMap[key].getBytesSlot().value = value; + } + + function getBytes(bytes32 slot) public view returns (bytes memory) { + return slot.getBytesSlot().value; + } + + function getBytesStorage(uint256 key) public view returns (bytes memory) { + return bytesMap[key].getBytesSlot().value; + } } diff --git a/contracts/utils/StorageSlot.sol b/contracts/utils/StorageSlot.sol index d23363bd6..44285c900 100644 --- a/contracts/utils/StorageSlot.sol +++ b/contracts/utils/StorageSlot.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (utils/StorageSlot.sol) +// This file was procedurally generated from scripts/generate/templates/StorageSlot.js. pragma solidity ^0.8.0; @@ -27,7 +28,8 @@ pragma solidity ^0.8.0; * } * ``` * - * _Available since v4.1 for `address`, `bool`, `bytes32`, and `uint256`._ + * _Available since v4.1 for `address`, `bool`, `bytes32`, `uint256`._ + * _Available since v4.9 for `string`, `bytes`._ */ library StorageSlot { struct AddressSlot { @@ -46,6 +48,14 @@ library StorageSlot { uint256 value; } + struct StringSlot { + string value; + } + + struct BytesSlot { + bytes value; + } + /** * @dev Returns an `AddressSlot` with member `value` located at `slot`. */ @@ -85,4 +95,44 @@ library StorageSlot { r.slot := slot } } + + /** + * @dev Returns an `StringSlot` with member `value` located at `slot`. + */ + function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `StringSlot` representation of the string storage pointer `store`. + */ + function getStringSlot(string storage store) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } + + /** + * @dev Returns an `BytesSlot` with member `value` located at `slot`. + */ + function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`. + */ + function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } } diff --git a/scripts/generate/run.js b/scripts/generate/run.js index a34823223..e68681e9d 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -18,6 +18,7 @@ for (const [file, template] of Object.entries({ 'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js', 'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js', 'utils/Checkpoints.sol': './templates/Checkpoints.js', + 'utils/StorageSlot.sol': './templates/StorageSlot.js', })) { const script = path.relative(path.join(__dirname, '../..'), __filename); const input = path.join(path.dirname(script), template); diff --git a/scripts/generate/templates/StorageSlot.js b/scripts/generate/templates/StorageSlot.js new file mode 100644 index 000000000..69fa7ccc0 --- /dev/null +++ b/scripts/generate/templates/StorageSlot.js @@ -0,0 +1,87 @@ +const format = require('../format-lines'); +const { capitalize, unique } = require('../../helpers'); + +const TYPES = [ + { type: 'address', isValueType: true, version: '4.1' }, + { type: 'bool', isValueType: true, name: 'Boolean', version: '4.1' }, + { type: 'bytes32', isValueType: true, version: '4.1' }, + { type: 'uint256', isValueType: true, version: '4.1' }, + { type: 'string', isValueType: false, version: '4.9' }, + { type: 'bytes', isValueType: false, version: '4.9' }, +].map(type => Object.assign(type, { struct: (type.name ?? capitalize(type.type)) + 'Slot' })); + +const VERSIONS = unique(TYPES.map(t => t.version)).map( + version => + `_Available since v${version} for ${TYPES.filter(t => t.version == version) + .map(t => `\`${t.type}\``) + .join(', ')}._`, +); + +const header = `\ +pragma solidity ^0.8.0; + +/** + * @dev Library for reading and writing primitive types to specific storage slots. + * + * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * The functions in this library return Slot structs that contain a \`value\` member that can be used to read or write. + * + * Example usage to set ERC1967 implementation slot: + * \`\`\`solidity + * contract ERC1967 { + * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + * + * function _getImplementation() internal view returns (address) { + * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + * } + * + * function _setImplementation(address newImplementation) internal { + * require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); + * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + * } + * } + * \`\`\` + * +${VERSIONS.map(s => ` * ${s}`).join('\n')} + */ +`; + +const struct = type => `\ +struct ${type.struct} { + ${type.type} value; +} +`; + +const get = type => `\ +/** + * @dev Returns an \`${type.struct}\` with member \`value\` located at \`slot\`. + */ +function get${type.struct}(bytes32 slot) internal pure returns (${type.struct} storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } +} +`; + +const getStorage = type => `\ +/** + * @dev Returns an \`${type.struct}\` representation of the ${type.type} storage pointer \`store\`. + */ +function get${type.struct}(${type.type} storage store) internal pure returns (${type.struct} storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } +} +`; + +// GENERATE +module.exports = format( + header.trimEnd(), + 'library StorageSlot {', + [...TYPES.map(struct), ...TYPES.flatMap(type => [get(type), type.isValueType ? '' : getStorage(type)])], + '}', +); diff --git a/scripts/helpers.js b/scripts/helpers.js index 26d0a2baa..fb9aad4fc 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -24,9 +24,14 @@ function zip(...args) { .map((_, i) => args.map(arg => arg[i])); } +function capitalize(str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + module.exports = { chunk, range, unique, zip, + capitalize, }; diff --git a/test/utils/StorageSlot.test.js b/test/utils/StorageSlot.test.js index 9d428875f..846512ed2 100644 --- a/test/utils/StorageSlot.test.js +++ b/test/utils/StorageSlot.test.js @@ -107,4 +107,104 @@ contract('StorageSlot', function (accounts) { }); }); }); + + describe('string storage slot', function () { + beforeEach(async function () { + this.value = 'lorem ipsum'; + }); + + it('set', async function () { + await this.store.setString(slot, this.value); + }); + + describe('get', function () { + beforeEach(async function () { + await this.store.setString(slot, this.value); + }); + + it('from right slot', async function () { + expect(await this.store.getString(slot)).to.be.equal(this.value); + }); + + it('from other slot', async function () { + expect(await this.store.getString(otherSlot)).to.be.equal(''); + }); + }); + }); + + describe('string storage pointer', function () { + beforeEach(async function () { + this.value = 'lorem ipsum'; + }); + + it('set', async function () { + await this.store.setStringStorage(slot, this.value); + }); + + describe('get', function () { + beforeEach(async function () { + await this.store.setStringStorage(slot, this.value); + }); + + it('from right slot', async function () { + expect(await this.store.stringMap(slot)).to.be.equal(this.value); + expect(await this.store.getStringStorage(slot)).to.be.equal(this.value); + }); + + it('from other slot', async function () { + expect(await this.store.stringMap(otherSlot)).to.be.equal(''); + expect(await this.store.getStringStorage(otherSlot)).to.be.equal(''); + }); + }); + }); + + describe('bytes storage slot', function () { + beforeEach(async function () { + this.value = web3.utils.randomHex(128); + }); + + it('set', async function () { + await this.store.setBytes(slot, this.value); + }); + + describe('get', function () { + beforeEach(async function () { + await this.store.setBytes(slot, this.value); + }); + + it('from right slot', async function () { + expect(await this.store.getBytes(slot)).to.be.equal(this.value); + }); + + it('from other slot', async function () { + expect(await this.store.getBytes(otherSlot)).to.be.equal(null); + }); + }); + }); + + describe('bytes storage pointer', function () { + beforeEach(async function () { + this.value = web3.utils.randomHex(128); + }); + + it('set', async function () { + await this.store.setBytesStorage(slot, this.value); + }); + + describe('get', function () { + beforeEach(async function () { + await this.store.setBytesStorage(slot, this.value); + }); + + it('from right slot', async function () { + expect(await this.store.bytesMap(slot)).to.be.equal(this.value); + expect(await this.store.getBytesStorage(slot)).to.be.equal(this.value); + }); + + it('from other slot', async function () { + expect(await this.store.bytesMap(otherSlot)).to.be.equal(null); + expect(await this.store.getBytesStorage(otherSlot)).to.be.equal(null); + }); + }); + }); }); From 740ce2d440766e5013640f0e47640fae57f5d1d5 Mon Sep 17 00:00:00 2001 From: Francisco Date: Thu, 2 Feb 2023 21:08:41 -0300 Subject: [PATCH 017/197] Update renovate.json --- renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renovate.json b/renovate.json index 875bf6ec4..c0b97d8d7 100644 --- a/renovate.json +++ b/renovate.json @@ -1,4 +1,4 @@ { "extends": ["github>OpenZeppelin/configs"], - "labels": ["ignore-changelog"] + "labels": ["ignore-changeset"] } From 132e5aa97bd8f453f9556534a50b51b4b3bb9c2f Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 3 Feb 2023 19:57:47 +0100 Subject: [PATCH 018/197] Add IERC5313.sol (#4013) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- .changeset/happy-socks-travel.md | 5 +++++ contracts/interfaces/IERC5313.sol | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 .changeset/happy-socks-travel.md create mode 100644 contracts/interfaces/IERC5313.sol diff --git a/.changeset/happy-socks-travel.md b/.changeset/happy-socks-travel.md new file mode 100644 index 000000000..b29d6bacd --- /dev/null +++ b/.changeset/happy-socks-travel.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`IERC5313`: Add an interface for EIP-5313 that is now final. diff --git a/contracts/interfaces/IERC5313.sol b/contracts/interfaces/IERC5313.sol new file mode 100644 index 000000000..2c9a47da9 --- /dev/null +++ b/contracts/interfaces/IERC5313.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev Interface for the Light Contract Ownership Standard. + * + * A standardized minimal interface required to identify an account that controls a contract + * + * _Available since v4.9._ + */ +interface IERC5313 { + /** + * @dev Gets the address of the owner. + */ + function owner() external view returns (address); +} From d13ec90f3719b6a1182b96455aea1ba21699f197 Mon Sep 17 00:00:00 2001 From: Yamen Merhi Date: Fri, 3 Feb 2023 21:15:41 +0200 Subject: [PATCH 019/197] Add warning for `supportsERC165InterfaceUnchecked` edge case (#4017) --- contracts/utils/introspection/ERC165Checker.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/utils/introspection/ERC165Checker.sol b/contracts/utils/introspection/ERC165Checker.sol index 40ffd68f7..db6939d83 100644 --- a/contracts/utils/introspection/ERC165Checker.sol +++ b/contracts/utils/introspection/ERC165Checker.sol @@ -101,6 +101,10 @@ library ERC165Checker { * @dev Assumes that account contains a contract that supports ERC165, otherwise * the behavior of this method is undefined. This precondition can be checked * with {supportsERC165}. + * + * Some precompiled contracts will falsely indicate support for a given interface, so caution + * should be exercised when using this function. + * * Interface identification is specified in ERC-165. */ function supportsERC165InterfaceUnchecked(address account, bytes4 interfaceId) internal view returns (bool) { From 3b591a48acaab78008ed39d60fbcf429a83155ca Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Fri, 3 Feb 2023 21:15:59 -0300 Subject: [PATCH 020/197] Add storageLayout to outputSelection --- hardhat.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/hardhat.config.js b/hardhat.config.js index 74f40c4d1..32f721b65 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -72,6 +72,7 @@ module.exports = { runs: 200, }, viaIR: withOptimizations && argv.ir, + outputSelection: { '*': { '*': ['storageLayout'] } }, }, }, warnings: { From 260e082ed10e86e5870c4e5859750a8271eeb2b9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 6 Feb 2023 09:59:25 +0100 Subject: [PATCH 021/197] Add a library for handling short strings in a gas efficient way (#4023) Co-authored-by: Francisco --- .changeset/violet-frogs-hide.md | 5 ++ contracts/utils/README.adoc | 2 + contracts/utils/ShortStrings.sol | 94 ++++++++++++++++++++++++++++++++ package-lock.json | 14 ++--- package.json | 2 +- test/utils/ShortStrings.test.js | 44 +++++++++++++++ 6 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 .changeset/violet-frogs-hide.md create mode 100644 contracts/utils/ShortStrings.sol create mode 100644 test/utils/ShortStrings.test.js diff --git a/.changeset/violet-frogs-hide.md b/.changeset/violet-frogs-hide.md new file mode 100644 index 000000000..21d2bf984 --- /dev/null +++ b/.changeset/violet-frogs-hide.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ShortStrings`: Added a library for handling short strings in a gas efficient way, with fallback to storage for longer strings. diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 7fef825a6..5e8af93e3 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -106,6 +106,8 @@ Note that, in all cases, accounts simply _declare_ their interfaces, but they ar {{Strings}} +{{ShortStrings}} + {{StorageSlot}} {{Multicall}} diff --git a/contracts/utils/ShortStrings.sol b/contracts/utils/ShortStrings.sol new file mode 100644 index 000000000..9f253df82 --- /dev/null +++ b/contracts/utils/ShortStrings.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.8; + +import "./StorageSlot.sol"; + +type ShortString is bytes32; + +/** + * @dev This library provides functions to convert short memory strings + * into a `ShortString` type that can be used as an immutable variable. + * Strings of arbitrary length can be optimized if they are short enough by + * the addition of a storage variable used as fallback. + * + * Usage example: + * + * ```solidity + * contract Named { + * using ShortStrings for *; + * + * ShortString private immutable _name; + * string private _nameFallback; + * + * constructor(string memory contractName) { + * _name = contractName.toShortStringWithFallback(_nameFallback); + * } + * + * function name() external view returns (string memory) { + * return _name.toStringWithFallback(_nameFallback); + * } + * } + * ``` + */ +library ShortStrings { + error StringTooLong(string str); + + /** + * @dev Encode a string of at most 31 chars into a `ShortString`. + * + * This will trigger a `StringTooLong` error is the input string is too long. + */ + function toShortString(string memory str) internal pure returns (ShortString) { + bytes memory bstr = bytes(str); + if (bstr.length > 31) { + revert StringTooLong(str); + } + return ShortString.wrap(bytes32(uint256(bytes32(bstr)) | bstr.length)); + } + + /** + * @dev Decode a `ShortString` back to a "normal" string. + */ + function toString(ShortString sstr) internal pure returns (string memory) { + uint256 len = length(sstr); + // using `new string(len)` would work locally but is not memory safe. + string memory str = new string(32); + /// @solidity memory-safe-assembly + assembly { + mstore(str, len) + mstore(add(str, 0x20), sstr) + } + return str; + } + + /** + * @dev Return the length of a `ShortString`. + */ + function length(ShortString sstr) internal pure returns (uint256) { + return uint256(ShortString.unwrap(sstr)) & 0xFF; + } + + /** + * @dev Encode a string into a `ShortString`, or write it to storage if it is too long. + */ + function toShortStringWithFallback(string memory value, string storage store) internal returns (ShortString) { + if (bytes(value).length < 32) { + return toShortString(value); + } else { + StorageSlot.getStringSlot(store).value = value; + return ShortString.wrap(0); + } + } + + /** + * @dev Decode a string that was encoded to `ShortString` or written to storage using {setWithFallback}. + */ + function toStringWithFallback(ShortString value, string storage store) internal pure returns (string memory) { + if (length(value) > 0) { + return toString(value); + } else { + return store; + } + } +} diff --git a/package-lock.json b/package-lock.json index 51d350ac7..a663055fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "glob": "^8.0.3", "graphlib": "^2.1.8", "hardhat": "^2.9.1", - "hardhat-exposed": "^0.3.0", + "hardhat-exposed": "^0.3.1", "hardhat-gas-reporter": "^1.0.4", "hardhat-ignore-warnings": "^0.2.0", "keccak256": "^1.0.2", @@ -7923,9 +7923,9 @@ } }, "node_modules/hardhat-exposed": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.0.tgz", - "integrity": "sha512-1p2Aou7QW3VVI0iJhh3q9hgPyF66zggeW7v/PrcipniQqaXK+KxJnnJvzGsLvXYzB8lVp23GIK7MXoTjjyXkHQ==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.1.tgz", + "integrity": "sha512-qyHXdS3NmzrtXF+XL547BMsTAK+IEMW9OOYMH4d362DlPn4L2B2KXKG6OpuJczxYjgMFJ0LunLxNgu6jtRvjRg==", "dev": true, "dependencies": { "micromatch": "^4.0.4", @@ -22753,9 +22753,9 @@ } }, "hardhat-exposed": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.0.tgz", - "integrity": "sha512-1p2Aou7QW3VVI0iJhh3q9hgPyF66zggeW7v/PrcipniQqaXK+KxJnnJvzGsLvXYzB8lVp23GIK7MXoTjjyXkHQ==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.1.tgz", + "integrity": "sha512-qyHXdS3NmzrtXF+XL547BMsTAK+IEMW9OOYMH4d362DlPn4L2B2KXKG6OpuJczxYjgMFJ0LunLxNgu6jtRvjRg==", "dev": true, "requires": { "micromatch": "^4.0.4", diff --git a/package.json b/package.json index de2951bd1..66d44888a 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "glob": "^8.0.3", "graphlib": "^2.1.8", "hardhat": "^2.9.1", - "hardhat-exposed": "^0.3.0", + "hardhat-exposed": "^0.3.1", "hardhat-gas-reporter": "^1.0.4", "hardhat-ignore-warnings": "^0.2.0", "keccak256": "^1.0.2", diff --git a/test/utils/ShortStrings.test.js b/test/utils/ShortStrings.test.js new file mode 100644 index 000000000..f41084a7e --- /dev/null +++ b/test/utils/ShortStrings.test.js @@ -0,0 +1,44 @@ +const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); + +const ShortStrings = artifacts.require('$ShortStrings'); + +function decode(sstr) { + const length = parseInt(sstr.slice(64), 16); + return web3.utils.toUtf8(sstr).slice(0, length); +} + +contract('ShortStrings', function () { + before(async function () { + this.mock = await ShortStrings.new(); + }); + + for (const str of [0, 1, 16, 31, 32, 64, 1024].map(length => 'a'.repeat(length))) { + describe(`with string length ${str.length}`, function () { + it('encode / decode', async function () { + if (str.length < 32) { + const encoded = await this.mock.$toShortString(str); + expect(decode(encoded)).to.be.equal(str); + + const length = await this.mock.$length(encoded); + expect(length.toNumber()).to.be.equal(str.length); + + const decoded = await this.mock.$toString(encoded); + expect(decoded).to.be.equal(str); + } else { + await expectRevertCustomError(this.mock.$toShortString(str), `StringTooLong("${str}")`); + } + }); + + it('set / get with fallback', async function () { + const { logs } = await this.mock.$toShortStringWithFallback(str, 0); + const { ret0 } = logs.find(({ event }) => event == 'return$toShortStringWithFallback').args; + + expect(await this.mock.$toString(ret0)).to.be.equal(str.length < 32 ? str : ''); + + const recovered = await this.mock.$toStringWithFallback(ret0, 0); + expect(recovered).to.be.equal(str); + }); + }); + } +}); From 1575cc6908f0f38bfb36d459c4ce7295f0f89c49 Mon Sep 17 00:00:00 2001 From: Francisco Date: Mon, 6 Feb 2023 12:20:12 -0300 Subject: [PATCH 022/197] Update Tally screenshots (#4030) --- docs/modules/ROOT/images/tally-admin.png | Bin 22024 -> 0 bytes docs/modules/ROOT/images/tally-exec.png | Bin 0 -> 231859 bytes docs/modules/ROOT/images/tally-vote.png | Bin 33437 -> 40507 bytes docs/modules/ROOT/pages/governance.adoc | 4 ++-- 4 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 docs/modules/ROOT/images/tally-admin.png create mode 100644 docs/modules/ROOT/images/tally-exec.png diff --git a/docs/modules/ROOT/images/tally-admin.png b/docs/modules/ROOT/images/tally-admin.png deleted file mode 100644 index 8265259302418995cbc0ee77128e6dde20d1dfe0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22024 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
>t>*+2Sz_Kd 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∈YQ8Q z;&RDT2Rf>1rBImK6--UAEV*;Np@(uB)rEco56Y> z+Qa`3UvC)|*S53^Cj@utK;ub(pg|k=;1E0nclX9EK#)N2;O-6q0tB}}2X_xnq{d=f3W@uy2ZX7j$Uvuo=~sCB=u*NVhPQI%ta#Y1Wx zA60`mYF&>fmfQVQLE6!m$f!)o6_YzPWwZ9Cs&pEKR1m4&&0hc?#fV(BnIMV#8S~+I2V|8ap6f1$8na>BO~i)|{V_v-$|5jMMW3;yxAs9}+t{K0@XjSx&MtKQs7c0D^g3H-_VakIv)c7g@) zvZH?`Xt^cjS-(2o(>Uy;spZ~9k=m?m|M~^n{3T%+|0zkqZpA*&A;nMhZsX!<+Oq*W zS!bh|fsC`BI;MsEH`XCZL}38FXjEzNyI=$fOBqYKj_QY|);#CwwG;F5Sv$}1Gd0On zOBK;Cd9IfalkXtL>R)5^^SHNI4D0CdF;Dx>Eu^xXt^>VJ2h{53m_Il=+Bu2(O`P3H z?N4qG$7Df(2&g$but%v2ASpCTV9~9&P)e(Cp1XPaen*2cKt6p8f=+`TI7*^ZZ<61$ zqt{?87(y@X6hOjY)=15jLRlD*bkQs8GBrneY^kN4eA5kS%~WCY!r{TGX%F?e@Xd77;g~foS)WOGpTaws;sDez_+L_RAa5 zjm*NlrN~yK0RO+euj%I$4242((Xc(ziXQJD@0`wdK0n@P|Ja?ZEmHP~qz;{ER8Gqf zar!7dka6dKXF4Ae)ndvQn{4!DHYSjR`0CIB0gAV3kch5+UuL_ zB*xY2HX1$_cpuiklUY6h4S(Y&3!00~(k|c?1eO@YN_nzWstlvK3hpIDV&-Bgl>M#T zMO5=AINyh)O$1c~9nFf-5Wo8x#pg(AI?S|nr*Nh;!=mX0fS3a_0^TEoVsoufGX&uOlNE+C)IEmt%)l-lj3Zae^?7f3oUxYIbk7%#Pd3e z8nih0ICL&*yG*D-1j>aCEr)m(FmaN;B`>w*(MIG=O^xYk>7^FAAb#tT$jrTGfY`#= zoxFDbz)!8MFIw*=>POnIRMyU#Y;Vq*qBA}NM8eO_!%P{oioqe2-sioYLB(q7_WWLS z9ion6S=_^#Ec$}FruuCTg5J~&xa`9!VWI|yzy_|qM~|N~QyRV)l6LE!He%(lhxfVbx9KRv*D7G7?;WA-oYg3OAHx| z5e!{R>*abnnR(R-8kJkfW{oZsK^ZbsDBDcS+0fGw)ABkoN9XrdD<6EWb^!dO%_a4G z!(-pFugt}vXpO0+{ryo#ek3)v&-r{%Yil=Ig|G2$Tt2;0wuldWrVY*Kf|}QI!%{Hf zXsN9jNR7QxwR~s=wbdcUDkd27k?yXEmKMKWoUq>fW0L!aBNQemO_*7KzLAD6ff$9q$A<){kP!nIce z(=BlzTtQVKxKtv^f!LRyW3q)!;|UP9mbO6mgTn%C>)+7g?%d8LG~CgNqVf>I z@bj%;S;*E9^UO}ge#1dHz8p+=w+T=8klOW2ye(-+?79TN@}3%>7AxxEq+1oCT2{b0 z1f3^^fa2lXNNk?Z;OFTc>2@x4NpE#8@_fm_Oct-`Lev?#$+PQY&rtKbX_Cmws z&T?~*&pz(d4CTD8hih+EkmZQs7H$6_RwAo*YF;fs8>att4Km1Z6b2XKqsRv%o&o6I zF|thV{rX<0UPa%)DU#B}f(I2Og@P_3xq0!PVH$h%^7>vw!PfQWppM)`2$ z>`Snc32g-!0jp&&`nRUbfF+qOC>Xx%^4&}yyAE(1&t7zc5*R{E5)a0GeIG<_Ada_(S29Z5rg2O4hDcrq& zTW4Lrp`|%uNY)+xf1}T@FUf%iD%YoyS%0;Q%OVXx8;Wi|uG$w9t$ncx8chdqNK2*sNQGq_s z_tzlzA1=CS<=2vc63=d7YC;cx9`!!NLp6Eeo}is+I}Da@W3>d}V?CvhtgZp4Zl~g|~(;w|^zvVhN_va(H+f1?)mj z5k|-|yfh^)kO946w51DP1Ea)}yC5{>;sPD4Emnsk=@3D0(HEPsx5H4-V7!$=Qoc4~ zHFnZl9muBLO`J>%s4FS?S?eb2CeS88LoD_09|FB1wyf;a>yKh$k)%0khY2ByD%f%^ zeBC|hNn5|gz`8Z9ucd<21|&HW9x0=|Tw^}%opA*+AR|2+)MN;`y`hW+M=uv++dNbH z3Uuk1ehKr{uEH`8GUl%wB`MMQP%*v0<2a|lCDc6oNgTtS9ed@6TTIVaZV&N9dhkn9 z;$qSa3y$!xSiQ&sesPebc+{B;uPhXTx&4@u8a{e4cUB2ks@=KA_>bU&p;06Awpw7i zwtP}y+TNk)y}GEa&hiz8HkZZ5yG)C8fo`Xbz9q}io4coaO4vL+E$KRKEjFWy8D_WU zdG;ru+o3srB0r9i6l1p*-x?Mde~ZTQ878(bzEqB0n*;AN>+)#5 z2Y*#*_oN6soM|edtfE?T!H9iop#;f7t1>e9gJsC==aot)hRiQEjV|GEgJa-_)yg&* z%L4P~$?wmu<3j}5&{ZRUH``|$#JpO34%1%+e^|YeWl1vB6413=QTIWnQI*f|GyLA_ z9M$txyVg$=64LKXF+HBJ$si7-XDQ7I7;eWt>=d_0zU_!7cEkKWg%ul z-{DgzE1Nv42XulUBaz4{p+FL(Ee9fUdawFHalDhmO10*aorSrZ$b(vFpg-GDo=wPg zqpRHiF`$SGIo70m{+=GENH2bRE?R9s7RG4K1QD>@tU2e%h+XIGJq-$y&=>aYK4m`d z8A2(1-k(=0ynMIj+$Q3()SOv@%Q~<2l_*HqVI{9-8a3Wj+?p<+h(v`bxk@M5;`^I2 zt?hG}O+NXj39{I3&B9oNnl?kYbfG>fps#SMsO}R?g&(N5H^X(RkpNa6U!_}>?x>H! z@nRI)Bc#Y2X!@NyS(+Ak7uenAYrq&Qrr~O?onS7Pgc57%MR>g%cLt=P{NS%DMhtvb zJ#BqvyQCD<>wwOrdr+jr_nUl4IVEu?OcgI(#COA^sgyrjp)W}Vc&ieQL6vem$Oiw^ zwAOgH^?W@XVs7(!bTYA$m5XHScS5%L)))FVHqA|NB9hOic{QYp(MU10d)6d87r8OoSODuQ)tNq}JRUYa~AFVV{^svBn0o}ZZ6MO^YUxVw5> zx}7)c0Ili+UJU2urM6rmCU-09zXeWORy7L;faSW;=c%!Q(1F2^AI8WpzvPL6|I#CN z!SW<~o5P)MtJh>OY7vJ%8CEa%dY6ZLA3SO2M#0nV!o7-H+$_nE7N)?sK8d#>K2_am z`P>2{hVOxtwlrpjn97+C_9bmU1;Qt0M7BSz0VW6i z(zU0*N);$#CVFcrG&Ma~6ys9`f#qm%x+qU*5qr4L83@U9g`Bb2TEMe>Wd!wn1E4Gv z-4Yym3)7E(j)7>%dwgW#LUozBNIsD9u$=Q! zOwZ*;ua>(we~>Obj$vT9#A?r^jlVaE`|?lqsj!111I50RkH<6>kG86G+T^ysM9>qd zbOpi7DbmQ)=nR1ATW=3>h!vclS^042gPZb=-y&jtHez-6bmY?&Ni&?|U!I8pFo^34xA}+(7@`?iw|}pWpF8 zEr#uv!ZDHMXt6HkOypfHNo71kj36`mT4FixWCcI50%B|UcA-Td4yMZ2GGv%o*!Eri z&4b^=hdWHX)O`DbtNgdw2xHYZ2P^MgEbOz}piJRP!Aq)(+mC^LSQ(yYSJ(UkG8XQ~ zo8=y8Z)2fg+oQeZR{M_e4BH*_rEm|8u-!VRY3 z(EGY^QMr6w_#Cyi*6LwmJBlp`Zp9}ZoY%NO-m3f9nBO7;qAOG@SL4#SW?Ab%rI7r8 zowIB+wkXa;RU5oCSbHmc

{=u?s5HeGHa}ugUmbw?8%dI1xi`nI*6!VqIq5#l~Z~ z96t3U1KTHHIx92IW{|BUjf2^C{%lCD!))j+{BY~8YqJfJLz~;WKqL2C6a=s2Lr^*9 zlEtl!hG61=Wu`eWk#}ow8b)hZ3PS$+9~WNT;!Xfe_Tw z_4s&rBYaByo|uhc|MC_^^%tYb+&~&6b2=fbrR1Z=QfGX617e_WDATZ6$lpP`$aER7 zGdfiO)#-w{RZI$GOqCcz1TTx=2>H>z>2!*1wl@1ll2OAvuKC$1yN6HkcAul_)Y7?A zg)(VukF0GP)sLr10&OEeB1f1ErW!05o<@RZQr+5wrGA#wEYYiq=|)E-f*yf2r%RT(FGGY;*X{aj0ooSoSy9u)x{R zVlQf%E#gN2Tt**5iebH%&tVefZT*qJ`_`Pi@1C)P(`@LkYUW}m!zbKw%pgK#k~+1< z5hGt6M_KhX+MFJ9O>J#>Qdma9%iKm5IcvEaYSH*x;~2Pw9Y@$$ii5u6yUApa@cyhl8rSVMKW!t(8y(!meD>0GM zzD}zN-$TJlrWSE0RxgUXI1n~s-!wNL>F~NGsGyy}!w%+gST=|7V_^;IEa(w7w*}E- zG+P{AOif|ua}+n4^CJ4}wyB~EKc=M|6Yj8&krC{!QJ*R?osi-43_Y8DZ^_w^1VHHr*Dc((tZs zW^X;;Kn69?8@iASYJp1bxijKWF2~NEH*Vbf4WcFf@~0HT7{w#{W5t3K*r{{;-Rbo+ z4hv4u!I9=d=2^v5j_qH&wGt8%kO~Xpw={4zMa{b?=zna70E_eeoWOY$0WIc_HfjP7 z#tN|+@pY8g-Cr3FsTVEuD2@%MB#QdI?K!iK&i0c}6xAq6A{(Gm#nD zJ)}jW1MOaysbJ~?m5t-?a1p&fE<=MKTq+uGTFsv`L%h?MH^$O4N(EY$SEtBba6($A z`0IhRST}kaoDV=X9iBQYeMpY27Rz@lvU?OSGUW=5c7vBO&kw*7MSlX0;X;;kF}lL;u~NV-11ZA zRkho`M`58wg+c9e6iR0PMHB;yFTXfluq>>B-j0aZ>}snj6-r8RLIFmHa3UP%$Di1Myvkz~+_d5f{xEYBy_1i-@lBP%}(8P=H{SgFG{Yyu}(6e{& zyAw#huvBtZQA1I|9M^#q0OLQoMME}t-)vAyT25;$E37 zw%I*Oov1h5Zgf15<7S3@-@=k^puiJe63|?4mG+-^iXg4101nEbQCnP6XC^VQY zx9CzQ6JZZO?CM%>P>ON=NbuC~`7;%nfSqYx*>fN}ZCSL#`JnA#j^@`1eW3Mmv^crQ;8g+rf z_r^}&JFT;5+N@HiuBl}qBD|d`@A5MA;y^G}z0ot8hepE@NWnpF4zB5*3%5C_5`set zwU1}Z&&?HVhGryB>W*@hL(so-*t;+dl#=#zrl2ZcDT_gtu)@A ztBBuncqkphVLHK1pZdh5%=6U44DSj~{hE+<&y&0c0?T`gv{aYm0On5X;SONB!|3 z_4OlGAQI#R6RCcE))3TJqgfq|9#szjaLxXZ5?Ne9tu?>AxT#Q4k*DiaNRg|;o-#`D z4ONWtd5H`@a=?qeq@-lvhlkZCoPnXn+VvG}wbi9f#j{a_V;w&H3@A;j7oOw>KkD6F zV-OY4ezhp`t@1J}kI>g#xUxLfpmX?ou)bQ98UB2GoT=Aru+ne?Ms z4L{mx(dFM~+{AKu3$>F(4OiG7LSQAyJUV)td`QF4CE?1@7{3m${gKR;OL7B9LxTre zkZ}lUOAu&9qka})E+H#N0Qg~RK|G>Wb5$!rT` z3w-Q6!BJaz4MwlO^>3?+tg`J*)-H8qbWEpa>$%fL#7fXQ_5^3-3A z`{K!a=@xngzEA;8E|1^mJd8)_0;l*~9qeMo$WUI`+@fUU+_nW+jv6dByKTc^00&+h zWVo08PDFO|voJ9hNEy02H z&OFbl%X7WapR`DMFiU8T$JcvHS=1WXIFT!k+`Gx3y(Q2NcVUU?3V(bR_=1)&EH}4v zgOl%0QhTk$qG%$kAe)tz@Ys{$vnEy`IHng3jVK%~Gcw;&RjYxIkB>p2WW!vDO0!~E z@|6g1C|6Qn;B6c<5N1ZI-R8(G2SPqSpjqfxW+tE}qmT^s9bBRsR}71#CL?=COXz4H zq$3CcE9bm!a~cwNoj&%|Tk_rD6B-??0mTgFeW3+TGMGi8l;)ou1E6TXD^y=&AV1C2 zNirA5r^TRTr-~_0zIdvslk~MU@5PI@&?;UkndH~u{k36vDYCzaf-t7bBAbcNQ(yd%H>8zRRl)sM!m$_`D-*rG=Hvy%r&Nq92zEaFSVO%6`jEPzQlDU^ zEl<0ach-~fOD5lSadj+ZT_supX|#77b?>L=ItXdB+!P#D4JRc?m_)6J$$!m=Z8*OF zGMQp}z@mmF>=l!=Am0~S>RqyPRf3nSVMu$jQDvO z#`R8o`)I1~O1Wd*UfV=zOHADZte4{OQ-@doH|IjfY>$oU%Q~|uo-&iFX`jqL_d%GE zLSC18`34%Ct9Xy2B|8VatQ(m$`M%5Bx3w%bhr0Cl-@orYeN?l%O)+U7OqXw83frRu zo3@`-dLJ!>7*B2N+EBNR=kB%dp_cuGO)5Bggf_>Chk7n?`+G;-@jm+pJPxmIBXdZx zx3@TGJxpAi>X*|?FztfCP0%yY@IX+NL9X6L$yBl0p?WptG}8C%x=kC_SiEl(7@pL& zE0665qes76{9mdMq}Ym_pgZ!<9xd1o4fnXVt)wYj{TyHR_=@-A?pg8eNvqFuQ=X$| z&24qFx=)X&`HoW19>q@E8TtOhzyLtQl>hViw9Vg+Jy03#MgHgym}ReIGL1>oa!HZp zn}9gC99IINbNL3QGHQMPL`r@{^@Fe?DUF`@5WVAE>`G4d74>QH(_QN2<7d%_*;J>n zZOU3(k+FkI;m1!;?_*Dq9W(d$n-BBT&qOGfZ7yzac9tS;Wa;lsU(2?0Z)ZOHw;C#7 z#k->Q*-amYKL~iCua5Git!BI9Ei65r5#<@?6VcD;&|b~%}E$CMRr8hsHP zPwnsh+wt_iE8yR#G=aN+jB7iqC`ur`IO2m4e2P>F-;O;UIsOr6A3_;sOFgog=Z(l; zx)}X^BC05RVt*6nKY;e}Q297k6yNwB-X`KFvMwqsa(aBH0lO!scycsi5NU7wloBW2_q=cisPT>i_V5+{1qMAF!=b&5X>gQ)ZU3L2sl7%{kYFbu;S89* z*K@UjCr|#Y{{)^<8|O0C@t%Wz+4$-Ie&xT%A)rI`=`{4Q>%xJv@Evx(HLAIa>Ohn4 z`P(8xS5;bNgSwMoi{Idowfj(%015!uamN76{Qgg2`e8207S5q4|1_3=U05Ur$#4N$ z9D?KVfM#Cc`bIIo-~#_Iu4g>-OFvkqi5GEr(ZlvYo+C+`aqP4b7Yi&;=ATAUjFL~~ zf3N_5i*5Ck@h3a>V9lQ4rs0s6zhKurPWfi9316Q+$6_MBM<4yOCqH9Qd6VE&FuBMA zAa;LmaZ}mO{BUfp1Q^3zAo4#CMEiFn;Jx(!Hio}sV7I~nJZ=Yz_&@6l`=bAge?Ql# z5jHCS&zkiF`;r=+TNNpN2wqu`5D;Dp45HCrgUD*$2@TCo{9hvezj6m7L<#@ioBwrc z{So*JG?Wkbzue)~qW_adJTk<9|B9-c7+^|xe=Dd;hdwwdpAIvq@ zDk*iRGz(9ID}uJoY8(%Wzii%5tnm_z;FB9?xe0!6aR2uRqR=eHj*ff+hUN7?4@&@B z-lw$&m+Wb%KTKLRJlI)i^t&C11^`KhhOEclgwu6(v&Yd|YZM#}=>T0Yu4MV#;C$FH zj6gH6!*A$g1pkxs(r_d4vocK5$_X|I>g6P%HlDqX18K zGs^N$`uhaM4}t>R9~IjpdR_Z39`+|cgWwOm{=GSIi!3l_Y*=p9M`GbFq<=E@zcc}m zeieg*ppI1tj6Lqf4tz)H!!-#206_~}Z=5AS@P)yVVNfd_HEo!`%-#B625_S*6pVkq znpp5Ho=sz|9*cAwZ9X6E-*Z_$8G@o_c8&m+2^6PJB7X*&<(dDtzssjmPeNmQpn(Od zex#t@QfV(hig%s_2#V2~DjbjLz)(JMYtv1f6=y(+n7|f*C7PeU~#Up6=X($ z+V@IzRk|=j^)!JKx zb>G7>H$CG_Ou$)?#ZXbr>m&3&KxZ%hyJCeHYjX$n`%u7@Lwyzuq;0`TQ6|WW`3ajV zq;W6$^1vj8o+nP8v9#I=B0%obO}!K>4NO5ne@|x^y+F#x2%HN>Y)H^xZTO|5df%zZ&q>JG3gHP4ny`JRpR}CJyZrD^^4en4&~5&t zEzeyJg4DCiwZes236z z2;4(1av#3AOwVeYleQ(1yZK0-{Bp$2@=)UJ@irQApeWtBURE-`zEpe_P4$Gvm-8Y= z&HL-7w1Mw-u)JV`=A+sB*B&1%H9E{2XC*$gErmw+{IQA@3u#H4zCRCH!2pC)CGbzC z@l<8?_WhGri;qfQ-_S0eUCb=DeQ72W9{JAESBw48OJ7PQ$tS~7Q z3RE4l3*~E7hF87Dryw!CMJzm)`4BnF+Qj*W)6OyGxBt$?*A1LkRu{phiIn5(-_vjAFR7N@JloPz(!_Q1 z9hBE(Bj4M=0YBHQ<@G+Y#KzO&qx`9(ldC$4-++^@p`Z!rw(hN*Jv(^Co zBp6bCF;nuE-bDMX$a2h9UbBq~0~4Z#5IidQ@yC9OR#)boZ(+C#QvZvp{5jkza zL@$fz^2cd3?vzb~K?MQ{2qM4qpa5m7zC!I{l_Q&Jb3}rBjA?G4EL#1 zpEBxCqN)~ zk6{P(LRsE^1(D}6M&`xV$+%dlQZW>Uyy$gifpHE@R{1VHc1LpZ8YF1d{*ySxF*c~V_ z(%zKZ)5jl>?;cimsA9EpG|Pz2`TZEq78$9xfz4(0B}am>Sfhrd)D%r0ONV#S!WEY; zemq7bV~SNaBsZq4KZhy%fVvw-_58FF@g-Oz>dyvlIB!*}lT36O83MRrlsqk7Ox!Fv z&LyFD2Xi`;Zvi49T|$JX9{G_$xu@%S zNXwv%ntin_Z^*StBMIyd+`|Epr-r$(NWHXGh(lNX0u^HXlfi)bO*WoIvG#pJxD*C; zn3NTn3@I@(=fGFwpz4Ds67dj)HCm$Af4~6*AlyO!vL{6>JN!y(e7US(r82DCcLA%Q zAfg?OKr`8J7>0nf3ZGnV)VMbV>&og!?7Rf>wdhs+U1p zQf?}e+DcXMIEot-7h8}>B@TPi%NS>d5%-!=i2xr%;j1flzmhQ@X%rm1$BjwD7zjs? zqeD-7<&{RLBEv2ukTwwmmR<$R$vwmsdTc$Fj;IGQ*cM8&Dj-q1K~t)O`oB*971?Ytu_3lNkXOGiF>JOLvNAJOptn+py`tO&^C z#`IFnj-c_9Og~<1W96!4BT?I?!M)AF-^lUYKkwCd8{!Vy@xo`-@(!{d5v+VWa>*7T9G@u zr_DFMtT7Fm=*Ct=zWfk^7Jjs4P!{3`27v6W351(|kNj2uF zqvKQ~!`9xt2zI;lzHNKv;F>39*s-!rjgBK}qRK>;-xeV2k=;?eva+=8aUQDY`_TT7 z!-gMbH1$L&1j&8%hP)hLylpplIGwHQTJUoQm&dM`4Wr@%@&>5KLKje z7wN^V=4ED#sTToh8snOc}-o`qh)G7GaSF-rwsdO~RdiD`K?LQc&j? z;kR8JM`8tNIJr+X4Sz0?Kz)!pp9+O=sbXioCB>8Mpx<%#x^I`o&Nl^9MiaJ+70hm) zUeh4=hrX?TcnD~37pq!WSwBHVgJN64RQH(AQK(>0^U5!D*3o9FuRYioA5q7m2qDd+ z;l_NB-sk}r*eK&auc1#sz})hPtFnw5-Lf)Ud$?Q|9-5rxxft~AU9KdKe3?C+HF|$7 zWHkKS7%7gSl~?<2&*x#yQO6c%d4~vKOXuV@rH5<4h~p{gw%qD zfvCJ805d#mQdn}JmagXO4&hkq>Mpk&HMBm2ayOjcxnIwuo@y#(x46RB&bde`g&b|L zWSk;5O!&xYv@{KOMwaM-^d~?DwN^R_+`#8}V;4)aa>>rxng)GloxJhm{YdF)@`!$` z-^pSj*EUvY^4dNF)4uOl24hJ>e>m95G|B3K8h{)p}QQ|S)@IlEM)rM2-fkKaU&MJW!xe#NNpdtS(j?0DXk-C@2+Q2KZET@0qxBA;(8ZM84+QMBfPJ_-B&+<4zH{{OhA%Zz()DRNM2kdRU2b zDTYPYxu-ivm+UskYK|O<}Z`d!t>!wXLCWY zBEMdzl;#V=#S$I?ftO#7bHjuK`T`fSM~DYDDYa zGH;wn^SbLGt3kuFEE?~tQDlDZVhApZ`!l5Kvsxr!cFKA$6S#VIeLg`oNs_x3qmR8B z2&CC??VgxZ^YXQ1)C{NNGBEtEQ=!gOUp{OE*~IPH()UXiB~-Masdg1;TiiXfBf^Up z^NnvN>cziRxYX#g#*9+v)Sz&&EuO&)UVl*e=v&?+t&FvaGlzM+{N%2i0RDO_IEJjk zfgvta7hOP@Bb1pU%slDC!0}l)jJ#d6b_e&DfNzuVrr>#2Z?;$Ks>f}9znznPKCyy% zewZr*p`!;{vv}=bw;10pG?W;47rs>4D#`r*lldYGqj=}c?}=)g$F0m13RT}KH?eHT zswe``Vkma)I%$(INMA`hmasJ=MJV}um*%8nt8+lCcr>#N|L`w9O{U+u_-uMyc;Gxv z9qILPJTzu2epkr#0cM5%N@D)-DBp=mv**RulBCbi`!Ad2DnIZ4uzh;3@$b6y@2Z7_ zsPgKhh%`ZpIwX2|d6bV;d1DbDVgJ^hp@MLp_6OQEbJ$Di0O!rq)wOLW&5x#SA(1@d zl=5-9RBG1uD;mA{&iiF!H+TM)xt)Dp@fUq<}NbQ+T**{0pyQSY~)?7zOKIdD2a z9g2UwKW|}vk|mZPk*6@=lSUdz3qVOgGxa!Ku0t~odod(J5m_phMn*k{KT5#0DiDsN zKiyoPUz$wSG=lBzX7sbZ_fjwa{>I7iHi0bV)qqZ>UyC5*oqQNOyFJ9m!M|%W%FIRT znd&>z#Gk(7&rd>Yb*FLHvALFK6%Ai(;rt@b2zdW`mKwZ@6ys^0PaIi5i7SHznws3y;bcg}9LUJpYENOu^r`rkAn9Up{SydYYH6VR?E%Du<< zP1)w;+do2LLy~?Sc#j&=#4%7Yy>^q|dzx$IZ1m#U>B!g_ifPOig>52b7t1pRwIR>8 zX$&dOnCz>i_TOriQ~SFqUpn!hE9fdIkL}_-pW#vLYBa9^@nj%U*isl08aRkv=QfA6 zNL28$Gt{v^)x6BitRE$D@yXYnE<)9Q%Kp}~DhgG=#`VT`_-8Q95Nb&E549|h;Ew^4 zZF4dSYA-{Y;TIv9SF&~1}QYa>om`BlFus}>~0UYyc@&PD&DKQbSlK8O2#`ph2Ph|XQs zi&WV_RB7sTSy*zZHs!^?)lPA&hv#LwKu)~*Cb*b$7w`VmGWqSZcctI?NN5~zNL$ng z{K!&nY3o&{Ap_l-CL41sAHi8ZqBk2htn=&`<}>OR+DYBPFFsurYDO`wG|MK3C;Npy zS%|7AgAutlBBf8Gu26Ce+PK|DAaW*2hLf3IhYX{e2j!AR7n2BgCZ6CnxCewek6XD@ zkMdI}8CEw&n@`9vF_R>5&(}%Q*h1E3)SuP9uirS4{21VAC;__ZEcsr)dnWxccSxy? z*{qn8-Q6>=R$0%$|J%~8c0NJ=cMo|l+O(!eE0MZ{Np?CtY{eJx+)!m7+bDECs* z-9yH9Uoc|iOE0y8B^Ih=n~OzM$|ne3Fp@2Cmkm$zDHdXmzS5$FO`66lk|!ZaaTJ+4 zO1h<`i!d@`$=^SX?E`TJT#U9&!=4k%LNTP73cHwUv? zOM9Dd&-O=N_=AMiL;mVPZ$M~vu!Sh5um~dS+z#Y1S2AWtjfXPgNqPF&9O>YlH?g`j z3$tFi@0$OKft_*rJV@&xT-CGImGjS4%47eKO~^qsdlB}a?13;l%Un(wTK=c|punPI z$@yIjNus$Cu~`Bh40~6!!Q0c zw)G}sMN^=+d13FBW(h2RN-&zC;vb)#9Tz;Q5W|RaS@XkniBX zZ?>WXN~8Pn;UBGG>2JpCUb?)jiU;9F+p#yZK~ItL*M4| zz5QL|SyfdySq^M(u!>lB<;{KAr{EbO#BqY#c^~Y(T8)!M=dJ4Lxa*#Enj`bAztd_C zm_KwVpPnz(Xds`K(qFMh1)Snetxs=;P0A~{*wZw%1%SL{X1Jq z(oagHUAK78AdFDKjKQjxM1xck7(l6iFPB}+SWAYgyd7( zxkNRVrUM+wrDYXs`)5O+bKsJ-lejWmxSF)J38wh84$9SHKfS%|ea1=AKNH;t^q>Tf zT%t;IwjH=$w=U<*l1Fw=+U}>ra$ly0VX)eq6N9@vY0fP|VT8zYt=L7bu0)9g3r&eN zz2vSwZCFaZc?{?YXd!HbZ;Z@#UKq$MyUMe+IJ$rhMI}zw1=acR?In{Zl4k(p|ZWIZ}aUx^5XqNJ6rPbsQkEI3Kp)*ivCt?7?p)-A-wsaa_&lE z%4a~ZLraGT8J0mHO?NpTHET^jQjLB`-Nj&O!P)HBe*-^`)2Zs+XHn=0qm zw2IG{a*36!_eGmkSoF;T8N~?JjS0JwiaQ^V&<9odQjh^d%o3Zd8B|Q~Ekh zgsie3Dc7ii?EaY3rL-<7Nt0UCiBDf7hAqKh01pz>DH&bDyTFRh zuMA1k$8F~@T~$I~`2CGl0LH4!o~1WmI_G-%nlLLCo zJ|^=ln0_=pA{jw9-#rK)BsxJhY3k}Zv7s}Z*D`ps9g+Cfiy2>zrTH#Q5|svOiAIla zT;a$|(Qd__gd4m*mpE}FT{kO({Lb*jowinf-9X6Xp^MkE-NHcEZ%-v7&_`J`^_IWz zf+w-#oh-ETYMM4LfXK~XiA?Je?qFef0gQa`+qak4W_khxDgzZ-?&jm<@~@NKmNwqX z3R0kcHPret=H8L5&vbPsS1gz$OI*;8A(hhew6T|P7b&kQ66&v>{&-E#NV2c%7Ajh^ zgJGT&(e$xNYZ;gWS+|+X3lGh_r{S2MAIH7(nH6U%J~ROA$QfClOzA{V7Gr;Wf`6&` z=rp+-hAM%v6r9L%A{TDd^NW+9lDoP3SB>u5yQoaE29_?bx^d^*E>^>u<-LASukQ)) z3~RFJ?j+#{Goi$NTnH-F{l~Lu2Wx;gwIHxluYxBBi71*|;u5V^Nz=}^4Q8B*@)YlK zA_<04ees%t5_w^e^kw<7RZE+3jHr(71+rNpUq+G9MHeN?VvK9B;B{K>f7OT8FAO=a z7V#47KE0Iu*6hW!bJaU#*VuHP_?wP%;Q52G62~mRJ-#z9z~15x*o5hO*VBo^=!+Q zd77(`V`o7q4XuuT`bU4^r>so*-Szu(h>^5GwS*0hzAovtL8x$G8Pe zwjm0!FSzMord;IjKkOM{$|djN-*(!TP;CR5oxi!h!8yy&7jIZ5F4THCso7h+i2-8H ze`(=wc^=7`s_){15#EsbKfM5}9U7DtO!e#q$J4*M7-C*L#;uovcMQs7UT)onQD?G! zA8EeRG~t|Q+->hLGk(GjNw4~Z%;h6Jz5Y1_E{HB0E`t$;2|0pTGW*XWo2m+RldaCt z3ybtUeSTJVKec@SOzYtLJF8Bu^vZdLbKGLSWOtvgo;spIGfK;n#gnU_ye4)QCGVKJ z=>nZQ@ZPeLs_-<)UQbd!OEo<1EL$D|dn5p!;q&O)OW`8b@fP?=2OFiL&GVHw@sw$} zziv3BaZpFIk+R4>|0+qVSrKg20%LUeiW&Gh%B35ee0>M`^x=xQkhDNzQd1*)(IL>z@Ss# zmh{CKk0T`!{$FcWduW}mKTy<jW*kT+@VS_ zM6jvoqMfJJ+*SI!w}wrmhy#9#uM%+7hJWZsybfPI~B`NC&i`w=}G zj5m9xgk7R?jZVG+WR*ZYo)1Jg9XoHasg&p(o!a+EuCem-%|bjHtkAhJB)AdyZNAKd zS?bqvW_&@4nfrWqK_Beq$y4Toq&d&hb5H16f~eHW z%w+0W5at$jdIqT>emwu^rXKpl=c8tzO%Hw;W+GJ`lfXt_3TDyXK099=-zPpxxfB1L zdKAu`!eHAnmWAw)ub5$=eM|nu_=(Ev`n?W~$rWM>q^;YhT;oqK-xGQ?< zQvz0*ivAs?eT)QswQ+hRWX|*pzh6U8b4icTLbP&rVfiJS8xmJmAdRCcn=gIxB+psX z5atf<(CQVA{`Q!&@S$bqv!|5l4P#jY?y7UpZEdHqEtlK6?^c!_mr!eSZw;0mN0?yu z{;vb?l>Z}d-lr5A^@1aG?^&^|l7Le1b=H*LFCw zK>nHwdA-jZ45$8>GV%+`c>d_A#0({d*Qh)BwLd}HOG9H=;v$oZaq3fEImzmE*T|%@ zjaFjkwt1RHNlX)dTJ=}2Dw6f|pez%Ahv?jWLJjKJw`3l|t9FAyp=1epmtl$tT z57rRO7;Tm`?QGSy7vSZjdW%38b%@iv-eGy|u=D8+(W$93QGaETV`rkH)SISz)v(V| zXKjLc$ynU-u^oYnOxH+ScqR*9Y2+`8a!s1oMI3VdpME{s!IaXb{?V-gZcHnm4b0P89Wo{Y|TWvLG)yF(;OH5twaw=U~0kka4Z9 zCpkKz^%mfjfzf5%oBI{9?Ry++bKu*qx&K-LL*=rFx-ba-%o)hJOzdo=AEyUpjJ*k+ zNJ%LWcB+2(EaU|ypb9@};-mIxy0@o;eGy*x^}4J2w*=|wqo2m0e#Y2qOAo)$=O$H) zcA=+Fw$80NC=_|b@7+$H(;%N$i1;Ep`016eLd-^lI~=-eg8k+gV|D3XF%`t_i4#6b zGFC6FF}C`)nXE;8@j5oz-M0t%q*!pMzGztH?M_h(kox=)3$J?n8j)|niZ@HGwmq}T zrSpx3klS@46|rE`@`)H6HB!Ow<1b;Ox*G2YKl#oJ6Q-weMSl1BRo&ikp*$GySLKM^ zFcudRCi=J%{MrSCG3Qf~-lvFQ&H3Nq1zZh){qsIaRR5%F{t}dFL||_3^b>>uzeExh zW#HZ*@FN zMz^Tl%vS}Xp&P05KOnaKyB1o-V)MuedSU0s7dy~o;wln8N*rrj{@6j&7h)gM@NHr; zV#c#%p3WT)wrj)U6RgQ!L)CFU1gy*g9Fi{mnK>yP(%okS@{zCah=GMq=;$N-uH0-6_y6{bva z@;(-;&<;8O*|Tl{d=YL&lmBPp5xFF`pr}w9i79#a~RfX}jTGJ5)jcE~`5v76Yk z&qFj8_DIALZyEsfcJIY!@UfXy0jkb)iBo>DL?RVHY9=)Pf8h*Jk)+RjOJt5jl2{$$ zTNLbU%7hyJx~qDuohqn6-I#^L9a7lo=`JBDhIycZiKbngI=Wm(q1-=$n#Fg}<}JV? zY-IE$Fa7o3wHjd9zXzt6(uD4zE{++?yG#S6{wT3!;zo*b_WDre43PqJxmPiS5gUx9 zIL(PsW*sWbQB`d5h)Z_#|LC0?5VE}$-Ap^2!b6Mf7X3t4W0R4OV+>NQSM_=Jj^6RoAAvDuDgsYO(jA@CB6|&tk5WDhQp%h zJ&CSgA>(>)8-gVy`Ec%B{7p}0P8fOlJwUzUlkB z;$Qf+4_$upQR(04PJpY(V5|S0D+0tm>f^C7v3xwFF0i@#bhhj8LQ@reJ9{gHVSn!; zQa>H*K!K!@d^k!ahMP3}`%`)t7dE+9sb3oFF0lq0EP88Gc#GK=C}n$Z#})k2TK3a4 z1?4Ykq@ez{N+xRlFVKFo>|)QYx4jI@jsKaK4OFa2$M5 zY=!eYwmOgKvx&-C>7bXx-}~fwVu8gNySzI%_U1Tl{SF>^0bqJ9I(CSh`;WuNW_55? zx|QaT`D`g-rgI{8i0M z0E#U`*qX))P8^7VC|#`&*Sr_Jc@yi$<;zKBxgwNH#xNPK>CcQmk z@2&l|Oi!|o(NR*k?Umo! z(W6a?VWdna7oVMf^vSG@(1hHp#uNTBw1|wpKOGPMUkV&w|IY<@7UnFB$Xl#{HV+=L zyKJ#Zh1yL3(%un*KqR3Q?$x zIUAjlxk8*=K>|P()DzY5g=gSUoL^O*+nWd-In$?b6f}s=E{S689kje z|AW@)eg2Vj{l5Y(8{W9^JeS%n3yT+Nq3G%2H^vk#{uK-d#AtouOsWGRsgP<=c=W!F zY8kcbVB87IK1G>Q#}#7*mku$U;9zxpW+VODB#BuGOf=&DYY+f)S}(f1ZUTVJmiQRw zPBBm~mQh5EYrw--za${z>?TmDR=sP;qgSBRWRuGSkQ(qhX%~vTJm*HoLPinihVM5h z2E}D!vV*D`><(7P1+O^_cqGMMwq3XI^1;DSj_Ox9^5GvMJXvMop!bX78NxJ$IC*8C~sT5rIxiy`%&Qi z#xHexIiDW*AmH^7V8s9KW3yyC6r~@Di{{&87zblgTy90OfjY9P*h4ZxK7jaA<~S>N zC65S^D4=#)06ILiCl~~$DPTzSPyc033$tS{hzM5cryS)sFLvN4qAwzDMxtL2eLwzD zUH(Dx4*P^^&|a}3nKf<*2U&->AhBd;ju!8LG*h4~wOk|XkK2o9WQnfMHQn~-m>05$U(&@Q02?ms6uEN;J&TcpDzHOnMm#V}(XRDk;`UGp}0l9A!2UK=rmhYF{N zZ`)&#>rKvKp1W$GMHh3-@D;v^YukVO!oQ9Pys{vy1H7-VF*+CC?D6>qkssM{@YS9W z#4J6OdlO6nAA5ju?@>|cksb*r1wkNMR>p^!3#+sUpeM_uOQ&*%iASQq=?krgSFrso zpZkWFzyr~3Y;L>~3<+v5=nQjr3CxcsPC=2kjUgX#x@+xKbeA zF$+JAkep)bdhcJptdaDw3LH9SI;vBB$crNZs-$UnJoslJLR>cp0af~=AMWxChsbk% z_A>)kU2ZLUTxVn`1pcso4H`;1w8FvoK-1VBG;QEK>gw~(GOv>lHuCU8mxu|}i+^-( zvf)QXuyjPc3;&NgjeI>l#I|^0bj=3a!c5!3C}wyzESB&7gN9nxDE?FR^%ZrLa=#i! zI|FdYN6a&h+0T73a2~ZqL*;(=s~S1198b0uP^}BOm^ixx;y_mX+-my4XZ+anY~aw`tt}3C%`Xgky$_}%=+%3St=bo*iDUNpf@rcf z`|HE}2sahwnry7Rp+20Jz#d0^z+|RkzrkMV^Bes?FV-uwT{KQobg=xV(yN*&lR_R8 zk7O5b1RduqH!f~LBfU@b8u3GD)>7GO`50cMQYlx-YV2?H!A#7w;HW7xL(8`6XS5VY zlmOKl?Fzq^j@wJ?l9EGpbwpNXlY~@&m)-8c{jE>}BcG+9vN)Rx>JQmLnCyNFDJ!o$ zu-|4q)B1_XeK=Uk^+-v{&Hf}pk7k;M3H-<1*1w42f&ewb+MW;k1}KT=C|HLEM@JQB z^_#IWG06S4@oZZn(beL9dH`W%XqNW#nl$cIIDxNAAjvHS0qA{?ihDU>77p*Vtl? z6(f5q86u3;PYI{PWFM3KutK<^OGWY@5E9JznG1q8r=#gVf1(v936l{e?QAI;ASq!*ahB z(#z}4eLC+Q{f{BrtC5<03NBlCE?a(zTQIIZ1I0e{Z-tKVy|TGM)A5Z0b+DRPum+%) zY)*qoR#j=g`6D55SjLI;bAAend&MpB{F$(L~A}{ZR7AWQ;mu{b<^Y44M+NKoqhzu$aPUX3&tH(j5x61p^lK>r6+YvC zI-CqslI+uE1#v%e{Y7;Hd_;u>%kJ)*Ya5)TnuJ8$4lnj0uv)Lnz{Lzmz+OkrPUNZE z!LDA6Df5^TzIUV><$SsE`Yo_co4g|kcP)r#e50|{3pZMYE7Ncw4dC&9#BK~Gb@_=} zyl>Q$NxLEIZ5+nZ+pai?^+F2^r6;i~e)HsKE5~II|dwMDhxm=kKO&Vvf2ib%W8$7kxvq-ymCq4x{*u--nz3K*YBz~VH4o+BXtles|_^| zto%(`F@eu2DzJgiFC9p&b%C4Vq9SZKVA|*!MDqrWnY}UoZsqSYcCvIAnk*7*$~RT! zBtzSnjbSn;3}43RedJ-G7$KFcFKNdD16SeaS(oAv=7LVOZEn8F{ehrkOs4ljV=6vh z)?!izO8vT9+t^t#*ycnyv)gFR&#l{Z7S-4U%fq#r0pkQ|AISVzgQ#~4YwD6z_)=aP zG<6`!WC#_BaF7O0=XP=NwwDD|@li*$Gy{v{)WGxUYC z4YhMt$6SzByguW1XR9^u?sH>rR8H6$qH*m$eT_t*4^N=a&!rEh*mtNE-(c|=6eTWw z`U?KOR}c4IdQ05wn*@DOb4x?vbUK_EiRo2UCQw}^=Mu1$7kECp^st(fcXwxk+T{RU zmVsV|<8Km9pyYuOkFAOLW+!(G@~TQFg#t^6;% zYFX?0zGB9*ZhFOq`ay!5!yboeB=L84%k-&J2IMicx$mD{kE!S0uFviGT~ZC&i^zpd zV=8MKn$r$%#pH?lY|KTwp-1uFu9a{UI|2tQ^xTtZ*&oF)eI@!m(2A!*nz$x_5fi|0 z-F2*O!xwR0YiNru>V14THbRBw+o&xo;xvF6YD3W zZv6oYQExam3N+@x{u?(`&_8+Npp_NY59Jlgt%|B47`@Em0hbfgIk$*l-2JD)kZ7!f zrI*g=-N{`u1jU#pUv6gao3({BN=!-<4pheAS-D^#DWQPVj?V0csaeuZ=iUJ~qHpBB zB04OdGNqOu!n7DPlj0rWIrlKo74+dE4e0mqa}3(BtB?Dx6$2jJ5!>7+y{u+d;>q_& z#VJXehR&W5H`az6hh3WwYh!@-S>T0N+tvISNI!KzK4S`PT=onY2f^`rYZQfzWLA8u_?ubn|Es8hDMU&l`DC7!;T zsRnjd*vE>+qm!8|x5sJ>5p1nn+ZDy|R^T#kTX}|}&r-V=<12s9lig#XfBqYAH(g;- zQlgd(_Sl~uAun;tmGs+NpQr0nHz7XjSgHd6#0!7J>iwaiTWUKUif@{)@u?xq!Tp*pBl=dYfQ4$>*)S> zbU*6)Nl{&0MbXA2dcoY7QB(cjCdY*#GEM;h2+x*UUYqaj;u?CHuY-gAi#BNwo6Yt4 zp^0h2?)Jg$HDg9Z%lXv=T()i&KDP+=yhnA)cDFBEGZEk?lzd3Y16L)#Jeuea>dx_V z*Lv5;=v-3sT>uO9$W15Y@Xz|7S25#aGf^;-Z{i}O`DDPaff3fGw&Y7n{9>c9-S^PQ?9JH&IHf`B*9|FD*fB@ahCgSE z(2i5nDU>K}eTMNp=FFGM;6PSbc{jsV;&rJ*2}JTS_J9(BH*;k%bOs*H%B-=N@OeJQ zu~dd=7h6FVMH^=G&KsI~L(V)&w~e8_%Ub0kS07drLqC2YFZ38?XC8a3_b<|G>;jy3 zP8`zsG@Ly2hW4oVR25iWMiKbjkYfv#@cHh-i$;**wLIjFrL>r>Ud!?PxCxw&;Fbw; zn?KKiZfo811{~_@IpRVA7o7;S)v`t1_PXcGVm>_2mh?NF1{-jwnE(TC7LJ9WSSv4h ze;@CXr_)sTr7AAX0T%id1x2;j)n&nTt@BqKPK(!lCXn;l+q?$^A4-H?IcDO6#WJxC zz&vD35-r2WK&NAEw`-rQwDHXp8ZB@`2d>INiTeS3dED7hej$zqoOCs^*Q)wb@9@M? z+ezAJ6|uNr+mB-3!ZW24%WxR8>yo6wG4En|W}7Z_Q60=DUtRM_EbycgP7c)=B_1H% zmr_U`@W7Y5$3E_(I0@}2;Pnq%tI3PQ!2P-6y*W3ajGoLqu=TP?%Sz0oMJ(VD-jk*L z(rS3UP|f z+Y*c4ob?ONR`H!JQ_vI}jaK9S|0uxF*~F%tQp{FGs4q`-GON)>M*^bEdz{^Yf1D1^@Q$qg=3%7y;+?p~Ony#d9gKn<%6MoMQCxq0a<`YH$j0-RvA`L+$W6s!3sWvOo%r#UB;gkfpjD zKXu(<&%Ui9mKSfB`@Sy-W?^!o=4As!^5VG>@aG0?JR-*)DFsM`eZ;MCxehBH!1t<3 z#L$;_2TROjo8TNgT@_+5CZ<#2DM7N~nR9)(x?XxXnX=e|a_e@6S@**y6ZsFY56-rA zG)Pu&R6}mx-uIqC%Ap4p6;J{O0qNCQM-w3@wK2mh^QMK*EMk#laa4bdKz~10kQ0Z_ zK=9DhQz`i$IkEK4sa1Gg@47Q()6=?kKKuP%C+zkz9%1%8HfGQhxk@#~F=`BSalE-g zOZ8EeH`%w~vf8u*28n>vZm>bgsCu5H*WOhIT_4)beGWF!$2*@1q|L~|%#p9CrY&(i zw{~#-j-iNrBr;ulnuJ>P&zQ9(d;Ap#n8=|Bg_E=44 zE0Oru)Cz-)5biX27jkw@+q#Js?~UFw1lX=ZVV*aVY}vMAUJ7KpJDW4F)|RkwweSAI zX(`wK9CvzwVS9Tpjf7#8wAOP)JUn`4b+%^+Pzz3sr#4J#U(MfAz(6z6<_TS0!WIH_ zRpbv^$Cnli?rEc|EI{66$E91e`u67etySUDalO74ww80Xdbk$DhkZ%1I_hN+L%Kyw zEF`n|Ko*C zZ650Wb4G#*#kD2G^s0r`S-0vN8-Gj@eLnYR*c-Q_@M6TX9I@l>SA3S*5mx!3zH>$2 z%Hpy4r{jr72NuE@Ugv6FM`)w((Ro-SiHW+}ar|{u1rG6z!QzjE!vs6u@jsRDTCsMt zGKw-9kr59o)WkMGnQo$?Pf;E1cg5i#Cm0SUi>H|R>gR_Mjp<8qbBl z8;g1EES$Ki$pWfd35MDci=-RPrmf29wi=F7uh0R&xaB#af~rcH2;n=lWbHB4s^7jkKLiFh2XorY+O-OM>>VH^mZ{yg ztQMrD+#ZO#>qEZl>meb!n@ ztNXRq!)fKiKbNlhv|u^xi`fWU**e$bNbQ#+-|;P<`s@X$_gp{*LKbIdKxk~Um!+iwSh^}@^^J>B?HGI6&*XPd^?(aqaQk=?=nPDSqNwHuXFk>E#smCd^W z*`u40j71G0{I@FJ0Xut8#@8^ZXlPjyDur{G(6|uXJePmkIK#yY&r= zk~T>f>)o?(7{{nuK;UlgR*6%#D8TALGH|o^xE7$6lPAtxeOvZ;3hnL$5#(Wqv?t|C zz6!|mqb(Nr20qgmHdsp)Fkj~O;i0YVYNsx%mc&{LbPNnUIWFNSKj)=1jg`hdpacn1 zT}7HePD$DV0a-%{o*i8Qbyck`pQbl(Xhl2KtyhsgzFjf~1~ceZs6pOJCp{4&0e!m2 z4HkNo<-{cr1sz+w?b_PQip$UcK<8luz$fcZCOb%GmCLGd6i<4ruxd~jL40bvR^)o} zE83R*kI2H&BRko68XUzpJ`J#?#j-4RORO0pL(bID75&z#*W)T;9#U#NpzV^~OKqB1 zj4(mvb;6%DcV0We06{@ z*tW;bSDBjlDK-XCIE65Gs*J9-NMfO>{#D!`bV+I(-L%yFF9dw$u2eU3!gYRV(;?oS VZVAzj{5LR4@tvC7A6e6o{|B=b*sA~l literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/images/tally-vote.png b/docs/modules/ROOT/images/tally-vote.png index 76e9b3a981eb5bf394ccfc2814ee28d3c22e2d51..7d270fce091298627d9c181c315415c7710de784 100644 GIT binary patch literal 40507 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 H55fNfLFfw_ diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index d59e3ce9f..358a63e90 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -284,9 +284,9 @@ image::tally-vote.png[Voting in Tally] === Execute the Proposal -Once the voting period is over, if quorum was reached (enough voting power participated) and the majority voted in favor, the proposal is considered successful and can proceed to be executed. This can also be done in Tally in the "Administration Panel" section of a project. +Once the voting period is over, if quorum was reached (enough voting power participated) and the majority voted in favor, the proposal is considered successful and can proceed to be executed. Once a proposal passes, it can be queued and executed from the same place you voted. -image::tally-admin.png[Administration Panel in Tally] +image::tally-exec.png[Administration Panel in Tally] We will see now how to do this manually using Ethers.js. From 95027565c41aafd80512b63a4195194d78ae687d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 7 Feb 2023 19:15:24 -0600 Subject: [PATCH 023/197] Improve `release-cycle.yml` error reporting (#4034) --- scripts/release/workflow/start.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/release/workflow/start.sh b/scripts/release/workflow/start.sh index 6e5d78b82..7683ec5bc 100644 --- a/scripts/release/workflow/start.sh +++ b/scripts/release/workflow/start.sh @@ -29,4 +29,7 @@ git add . git commit -m "Start release candidate" # Push branch -git push origin "$RELEASE_BRANCH" +if ! git push origin "$RELEASE_BRANCH"; then + echo "::error file=scripts/release/start.sh::Can't push $RELEASE_BRANCH. Did you forget to run this workflow from $RELEASE_BRANCH?" + exit 1 +fi From 4d3e423443d0d0f80861235e5b58866572640114 Mon Sep 17 00:00:00 2001 From: Kimani Kelly Date: Tue, 7 Feb 2023 20:16:36 -0500 Subject: [PATCH 024/197] Make ERC20Wrapper.underlying variable private (#4029) Co-authored-by: Kimani Kelly Co-authored-by: Hadrien Croubois --- .changeset/proud-comics-deliver.md | 5 +++++ .../token/ERC20/extensions/ERC20Wrapper.sol | 19 +++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 .changeset/proud-comics-deliver.md diff --git a/.changeset/proud-comics-deliver.md b/.changeset/proud-comics-deliver.md new file mode 100644 index 000000000..e9c1015f8 --- /dev/null +++ b/.changeset/proud-comics-deliver.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC20Wrapper`: Make the `underlying` variable private and add a public accessor. diff --git a/contracts/token/ERC20/extensions/ERC20Wrapper.sol b/contracts/token/ERC20/extensions/ERC20Wrapper.sol index 8b153ffca..ce515693e 100644 --- a/contracts/token/ERC20/extensions/ERC20Wrapper.sol +++ b/contracts/token/ERC20/extensions/ERC20Wrapper.sol @@ -16,28 +16,35 @@ import "../utils/SafeERC20.sol"; * _Available since v4.2._ */ abstract contract ERC20Wrapper is ERC20 { - IERC20 public immutable underlying; + IERC20 private immutable _underlying; constructor(IERC20 underlyingToken) { - underlying = underlyingToken; + _underlying = underlyingToken; } /** * @dev See {ERC20-decimals}. */ function decimals() public view virtual override returns (uint8) { - try IERC20Metadata(address(underlying)).decimals() returns (uint8 value) { + try IERC20Metadata(address(_underlying)).decimals() returns (uint8 value) { return value; } catch { return super.decimals(); } } + /** + * @dev Returns the address of the underlying ERC-20 token that is being wrapped. + */ + function underlying() public view returns (IERC20) { + return _underlying; + } + /** * @dev Allow a user to deposit underlying tokens and mint the corresponding number of wrapped tokens. */ function depositFor(address account, uint256 amount) public virtual returns (bool) { - SafeERC20.safeTransferFrom(underlying, _msgSender(), address(this), amount); + SafeERC20.safeTransferFrom(_underlying, _msgSender(), address(this), amount); _mint(account, amount); return true; } @@ -47,7 +54,7 @@ abstract contract ERC20Wrapper is ERC20 { */ function withdrawTo(address account, uint256 amount) public virtual returns (bool) { _burn(_msgSender(), amount); - SafeERC20.safeTransfer(underlying, account, amount); + SafeERC20.safeTransfer(_underlying, account, amount); return true; } @@ -56,7 +63,7 @@ abstract contract ERC20Wrapper is ERC20 { * function that can be exposed with access control if desired. */ function _recover(address account) internal virtual returns (uint256) { - uint256 value = underlying.balanceOf(address(this)) - totalSupply(); + uint256 value = _underlying.balanceOf(address(this)) - totalSupply(); _mint(account, value); return value; } From 8177c4620e049b2749c2069651d7d5b4691e23d2 Mon Sep 17 00:00:00 2001 From: TheGreatHB <74754077+TheGreatHB@users.noreply.github.com> Date: Wed, 8 Feb 2023 10:18:33 +0900 Subject: [PATCH 025/197] Improve gas efficiency in ECDSA (#3853) Signed-off-by: Pascal Marco Caversaccio Co-authored-by: Hadrien Croubois Co-authored-by: Pascal Marco Caversaccio --- .changeset/thin-dragons-report.md | 5 +++++ contracts/utils/cryptography/ECDSA.sol | 20 ++++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 .changeset/thin-dragons-report.md diff --git a/.changeset/thin-dragons-report.md b/.changeset/thin-dragons-report.md new file mode 100644 index 000000000..b73730f7f --- /dev/null +++ b/.changeset/thin-dragons-report.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ECDSA`: optimize bytes32 computation by using assembly instead of `abi.encodePacked`. diff --git a/contracts/utils/cryptography/ECDSA.sol b/contracts/utils/cryptography/ECDSA.sol index 3f996520e..a499b546b 100644 --- a/contracts/utils/cryptography/ECDSA.sol +++ b/contracts/utils/cryptography/ECDSA.sol @@ -162,10 +162,15 @@ library ECDSA { * * See {recover}. */ - function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32 message) { // 32 is the length in bytes of hash, // enforced by the type signature above - return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, "\x19Ethereum Signed Message:\n32") + mstore(0x1c, hash) + message := keccak256(0x00, 0x3c) + } } /** @@ -189,7 +194,14 @@ library ECDSA { * * See {recover}. */ - function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32) { - return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 data) { + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, "\x19\x01") + mstore(add(ptr, 0x02), domainSeparator) + mstore(add(ptr, 0x22), structHash) + data := keccak256(ptr, 0x42) + } } } From d625cb45ea1aa83ee51c75a9e6cf824c38ee6426 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 8 Feb 2023 15:53:57 +0100 Subject: [PATCH 026/197] Include EIP-5267 discovery in EIP-712 (#3969) Co-authored-by: Francisco Co-authored-by: Francisco --- .changeset/short-roses-judge.md | 5 + CHANGELOG.md | 5 + contracts/interfaces/IERC5267.sol | 27 +++++ contracts/utils/cryptography/EIP712.sol | 95 ++++++++++----- test/governance/Governor.test.js | 35 +++--- .../extensions/GovernorWithParams.test.js | 39 +++--- test/governance/utils/Votes.behavior.js | 113 ++++++++---------- test/helpers/eip712.js | 39 ++++-- test/helpers/governance.js | 4 +- test/metatx/ERC2771Context.test.js | 15 +-- test/metatx/MinimalForwarder.test.js | 17 +-- .../token/ERC20/extensions/ERC20Votes.test.js | 104 +++++++--------- .../ERC20/extensions/ERC20VotesComp.test.js | 104 +++++++--------- .../extensions/draft-ERC20Permit.test.js | 44 +++---- test/token/ERC20/utils/SafeERC20.test.js | 14 +-- test/utils/cryptography/EIP712.test.js | 23 ++-- 16 files changed, 358 insertions(+), 325 deletions(-) create mode 100644 .changeset/short-roses-judge.md create mode 100644 contracts/interfaces/IERC5267.sol diff --git a/.changeset/short-roses-judge.md b/.changeset/short-roses-judge.md new file mode 100644 index 000000000..002aebb11 --- /dev/null +++ b/.changeset/short-roses-judge.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`EIP712`: add EIP-5267 support for better domain discovery. diff --git a/CHANGELOG.md b/CHANGELOG.md index e60161db0..ccd279d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### Breaking changes + +- `EIP712`: Addition of ERC5267 support requires support for user defined value types, which was released in Solidity version 0.8.8. This requires a pragma change from `^0.8.0` to `^0.8.8`. +- `EIP712`: Optimization of the cache for the upgradeable version affects the way `name` and `version` are set. This is no longer done through an initializer, and is instead part of the implementation's constructor. As a consequence, all proxies using the same implementation will necessarily share the same `name` and `version`. Additionally, an implementation upgrade risks changing the EIP712 domain unless the same `name` and `version` are used when deploying the new implementation contract. + ### Deprecations - `ERC20Permit`: Added the file `IERC20Permit.sol` and `ERC20Permit.sol` and deprecated `draft-IERC20Permit.sol` and `draft-ERC20Permit.sol` since [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) is no longer a Draft. Developers are encouraged to update their imports. ([#3793](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3793)) diff --git a/contracts/interfaces/IERC5267.sol b/contracts/interfaces/IERC5267.sol new file mode 100644 index 000000000..3adc4a703 --- /dev/null +++ b/contracts/interfaces/IERC5267.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface IERC5267 { + /** + * @dev MAY be emitted to signal that the domain could have changed. + */ + event EIP712DomainChanged(); + + /** + * @dev returns the fields and values that describe the domain separator used by this contract for EIP-712 + * signature. + */ + function eip712Domain() + external + view + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ); +} diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol index eb211a7e2..d0e52c396 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/EIP712.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.8; import "./ECDSA.sol"; +import "../ShortStrings.sol"; +import "../../interfaces/IERC5267.sol"; /** * @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data. @@ -22,19 +24,34 @@ import "./ECDSA.sol"; * NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method * https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask]. * + * NOTE: In the upgradeable version of this contract, the cached values will correspond to the address, and the domain + * separator of the implementation contract. This will cause the `_domainSeparatorV4` function to always rebuild the + * separator from the immutable values, which is cheaper than accessing a cached version in cold storage. + * * _Available since v3.4._ + * + * @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment */ -abstract contract EIP712 { +abstract contract EIP712 is IERC5267 { + using ShortStrings for *; + + bytes32 private constant _TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + /* solhint-disable var-name-mixedcase */ // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to // invalidate the cached domain separator if the chain id changes. - bytes32 private immutable _CACHED_DOMAIN_SEPARATOR; - uint256 private immutable _CACHED_CHAIN_ID; - address private immutable _CACHED_THIS; + bytes32 private immutable _cachedDomainSeparator; + uint256 private immutable _cachedChainId; + address private immutable _cachedThis; - bytes32 private immutable _HASHED_NAME; - bytes32 private immutable _HASHED_VERSION; - bytes32 private immutable _TYPE_HASH; + ShortString private immutable _name; + ShortString private immutable _version; + string private _nameFallback; + string private _versionFallback; + + bytes32 private immutable _hashedName; + bytes32 private immutable _hashedVersion; /* solhint-enable var-name-mixedcase */ @@ -51,36 +68,29 @@ abstract contract EIP712 { * contract upgrade]. */ constructor(string memory name, string memory version) { - bytes32 hashedName = keccak256(bytes(name)); - bytes32 hashedVersion = keccak256(bytes(version)); - bytes32 typeHash = keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ); - _HASHED_NAME = hashedName; - _HASHED_VERSION = hashedVersion; - _CACHED_CHAIN_ID = block.chainid; - _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(typeHash, hashedName, hashedVersion); - _CACHED_THIS = address(this); - _TYPE_HASH = typeHash; + _name = name.toShortStringWithFallback(_nameFallback); + _version = version.toShortStringWithFallback(_versionFallback); + _hashedName = keccak256(bytes(name)); + _hashedVersion = keccak256(bytes(version)); + + _cachedChainId = block.chainid; + _cachedDomainSeparator = _buildDomainSeparator(); + _cachedThis = address(this); } /** * @dev Returns the domain separator for the current chain. */ function _domainSeparatorV4() internal view returns (bytes32) { - if (address(this) == _CACHED_THIS && block.chainid == _CACHED_CHAIN_ID) { - return _CACHED_DOMAIN_SEPARATOR; + if (address(this) == _cachedThis && block.chainid == _cachedChainId) { + return _cachedDomainSeparator; } else { - return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION); + return _buildDomainSeparator(); } } - function _buildDomainSeparator( - bytes32 typeHash, - bytes32 nameHash, - bytes32 versionHash - ) private view returns (bytes32) { - return keccak256(abi.encode(typeHash, nameHash, versionHash, block.chainid, address(this))); + function _buildDomainSeparator() private view returns (bytes32) { + return keccak256(abi.encode(_TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this))); } /** @@ -101,4 +111,33 @@ abstract contract EIP712 { function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) { return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash); } + + /** + * @dev See {EIP-5267}. + */ + function eip712Domain() + public + view + virtual + override + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) + { + return ( + hex"0f", // 01111 + _name.toStringWithFallback(_nameFallback), + _version.toStringWithFallback(_versionFallback), + block.chainid, + address(this), + bytes32(0), + new uint256[](0) + ); + } } diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js index 4c083ed92..201895934 100644 --- a/test/governance/Governor.test.js +++ b/test/governance/Governor.test.js @@ -4,7 +4,7 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; const { fromRpcSig } = require('ethereumjs-util'); const Enums = require('../helpers/enums'); -const { EIP712Domain } = require('../helpers/eip712'); +const { getDomain, domainType } = require('../helpers/eip712'); const { GovernorHelper } = require('../helpers/governance'); const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); @@ -20,7 +20,6 @@ contract('Governor', function (accounts) { const empty = web3.utils.toChecksumAddress(web3.utils.randomHex(20)); const name = 'OZ-Governor'; - const version = '1'; const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); @@ -148,24 +147,22 @@ contract('Governor', function (accounts) { const voterBySig = Wallet.generate(); const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString()); - const signature = async message => { - return fromRpcSig( - ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { - data: { - types: { - EIP712Domain, - Ballot: [ - { name: 'proposalId', type: 'uint256' }, - { name: 'support', type: 'uint8' }, - ], - }, - domain: { name, version, chainId: this.chainId, verifyingContract: this.mock.address }, - primaryType: 'Ballot', - message, + const signature = (contract, message) => + getDomain(contract) + .then(domain => ({ + primaryType: 'Ballot', + types: { + EIP712Domain: domainType(domain), + Ballot: [ + { name: 'proposalId', type: 'uint256' }, + { name: 'support', type: 'uint8' }, + ], }, - }), - ); - }; + domain, + message, + })) + .then(data => ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { data })) + .then(fromRpcSig); await this.token.delegate(voterBySigAddress, { from: voter1 }); diff --git a/test/governance/extensions/GovernorWithParams.test.js b/test/governance/extensions/GovernorWithParams.test.js index 26e948544..86ebacc08 100644 --- a/test/governance/extensions/GovernorWithParams.test.js +++ b/test/governance/extensions/GovernorWithParams.test.js @@ -4,7 +4,7 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; const { fromRpcSig } = require('ethereumjs-util'); const Enums = require('../../helpers/enums'); -const { EIP712Domain } = require('../../helpers/eip712'); +const { getDomain, domainType } = require('../../helpers/eip712'); const { GovernorHelper } = require('../../helpers/governance'); const Token = artifacts.require('$ERC20VotesComp'); @@ -22,7 +22,6 @@ contract('GovernorWithParams', function (accounts) { const [owner, proposer, voter1, voter2, voter3, voter4] = accounts; const name = 'OZ-Governor'; - const version = '1'; const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); @@ -116,26 +115,24 @@ contract('GovernorWithParams', function (accounts) { const voterBySig = Wallet.generate(); const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString()); - const signature = async message => { - return fromRpcSig( - ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { - data: { - types: { - EIP712Domain, - ExtendedBallot: [ - { name: 'proposalId', type: 'uint256' }, - { name: 'support', type: 'uint8' }, - { name: 'reason', type: 'string' }, - { name: 'params', type: 'bytes' }, - ], - }, - domain: { name, version, chainId: this.chainId, verifyingContract: this.mock.address }, - primaryType: 'ExtendedBallot', - message, + const signature = (contract, message) => + getDomain(contract) + .then(domain => ({ + primaryType: 'ExtendedBallot', + types: { + EIP712Domain: domainType(domain), + ExtendedBallot: [ + { name: 'proposalId', type: 'uint256' }, + { name: 'support', type: 'uint8' }, + { name: 'reason', type: 'string' }, + { name: 'params', type: 'bytes' }, + ], }, - }), - ); - }; + domain, + message, + })) + .then(data => ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { data })) + .then(fromRpcSig); await this.token.delegate(voterBySigAddress, { from: voter2 }); diff --git a/test/governance/utils/Votes.behavior.js b/test/governance/utils/Votes.behavior.js index 5062a9c97..8fe34dc47 100644 --- a/test/governance/utils/Votes.behavior.js +++ b/test/governance/utils/Votes.behavior.js @@ -6,7 +6,7 @@ const { fromRpcSig } = require('ethereumjs-util'); const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; -const { EIP712Domain, domainSeparator } = require('../../helpers/eip712'); +const { getDomain, domainType, domainSeparator } = require('../../helpers/eip712'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -14,8 +14,6 @@ const Delegation = [ { name: 'expiry', type: 'uint256' }, ]; -const version = '1'; - function shouldBehaveLikeVotes() { describe('run votes workflow', function () { it('initial nonce is 0', async function () { @@ -23,14 +21,7 @@ function shouldBehaveLikeVotes() { }); it('domain separator', async function () { - expect(await this.votes.DOMAIN_SEPARATOR()).to.equal( - await domainSeparator({ - name: this.name, - version, - chainId: this.chainId, - verifyingContract: this.votes.address, - }), - ); + expect(await this.votes.DOMAIN_SEPARATOR()).to.equal(domainSeparator(await getDomain(this.votes))); }); describe('delegation with signature', function () { @@ -38,29 +29,29 @@ function shouldBehaveLikeVotes() { const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); const nonce = 0; - const buildData = (chainId, verifyingContract, name, message) => ({ - data: { + const buildAndSignData = async (contract, message, pk) => { + const data = await getDomain(contract).then(domain => ({ primaryType: 'Delegation', - types: { EIP712Domain, Delegation }, - domain: { name, version, chainId, verifyingContract }, + types: { EIP712Domain: domainType(domain), Delegation }, + domain, message, - }, - }); + })); + return fromRpcSig(ethSigUtil.signTypedMessage(pk, { data })); + }; beforeEach(async function () { await this.votes.$_mint(delegatorAddress, this.NFT0); }); it('accept signed delegation', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.votes.address, this.name, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), + const { v, r, s } = await buildAndSignData( + this.votes, + { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }, + delegator.getPrivateKey(), ); expect(await this.votes.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); @@ -86,15 +77,14 @@ function shouldBehaveLikeVotes() { }); it('rejects reused signature', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.votes.address, this.name, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), + const { v, r, s } = await buildAndSignData( + this.votes, + { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }, + delegator.getPrivateKey(), ); await this.votes.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); @@ -106,15 +96,14 @@ function shouldBehaveLikeVotes() { }); it('rejects bad delegatee', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.votes.address, this.name, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), + const { v, r, s } = await buildAndSignData( + this.votes, + { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }, + delegator.getPrivateKey(), ); const receipt = await this.votes.delegateBySig(this.account1Delegatee, nonce, MAX_UINT256, v, r, s); @@ -125,16 +114,16 @@ function shouldBehaveLikeVotes() { }); it('rejects bad nonce', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.votes.address, this.name, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), + const { v, r, s } = await buildAndSignData( + this.votes, + { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }, + delegator.getPrivateKey(), ); + await expectRevert( this.votes.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), 'Votes: invalid nonce', @@ -143,15 +132,15 @@ function shouldBehaveLikeVotes() { it('rejects expired permit', async function () { const expiry = (await time.latest()) - time.duration.weeks(1); - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.votes.address, this.name, { - delegatee: delegatorAddress, - nonce, - expiry, - }), - ), + + const { v, r, s } = await buildAndSignData( + this.votes, + { + delegatee: delegatorAddress, + nonce, + expiry, + }, + delegator.getPrivateKey(), ); await expectRevert( diff --git a/test/helpers/eip712.js b/test/helpers/eip712.js index 9851df662..b12a6233e 100644 --- a/test/helpers/eip712.js +++ b/test/helpers/eip712.js @@ -6,6 +6,7 @@ const EIP712Domain = [ { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' }, + { name: 'salt', type: 'bytes32' }, ]; const Permit = [ @@ -24,25 +25,43 @@ function hexStringToBuffer(hexstr) { return Buffer.from(hexstr.replace(/^0x/, ''), 'hex'); } -async function domainSeparator({ name, version, chainId, verifyingContract }) { +async function getDomain(contract) { + const { fields, name, version, chainId, verifyingContract, salt, extensions } = await contract.eip712Domain(); + + if (extensions.length > 0) { + throw Error('Extensions not implemented'); + } + + const domain = { name, version, chainId, verifyingContract, salt }; + for (const [i, { name }] of EIP712Domain.entries()) { + if (!(fields & (1 << i))) { + delete domain[name]; + } + } + + return domain; +} + +function domainType(domain) { + return EIP712Domain.filter(({ name }) => domain[name] !== undefined); +} + +function domainSeparator(domain) { return bufferToHexString( - ethSigUtil.TypedDataUtils.hashStruct( - 'EIP712Domain', - { name, version, chainId, verifyingContract }, - { EIP712Domain }, - ), + ethSigUtil.TypedDataUtils.hashStruct('EIP712Domain', domain, { EIP712Domain: domainType(domain) }), ); } -async function hashTypedData(domain, structHash) { - return domainSeparator(domain).then(separator => - bufferToHexString(keccak256(Buffer.concat(['0x1901', separator, structHash].map(str => hexStringToBuffer(str))))), +function hashTypedData(domain, structHash) { + return bufferToHexString( + keccak256(Buffer.concat(['0x1901', domainSeparator(domain), structHash].map(str => hexStringToBuffer(str)))), ); } module.exports = { - EIP712Domain, Permit, + getDomain, + domainType, domainSeparator, hashTypedData, }; diff --git a/test/helpers/governance.js b/test/helpers/governance.js index f933f389d..b945a16e0 100644 --- a/test/helpers/governance.js +++ b/test/helpers/governance.js @@ -84,7 +84,7 @@ class GovernorHelper { ? // if signature, and either params or reason → vote.params || vote.reason ? vote - .signature({ + .signature(this.governor, { proposalId: proposal.id, support: vote.support, reason: vote.reason || '', @@ -96,7 +96,7 @@ class GovernorHelper { ), ) : vote - .signature({ + .signature(this.governor, { proposalId: proposal.id, support: vote.support, }) diff --git a/test/metatx/ERC2771Context.test.js b/test/metatx/ERC2771Context.test.js index 788777726..6c298d3d9 100644 --- a/test/metatx/ERC2771Context.test.js +++ b/test/metatx/ERC2771Context.test.js @@ -1,6 +1,6 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; -const { EIP712Domain } = require('../helpers/eip712'); +const { getDomain, domainType } = require('../helpers/eip712'); const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); @@ -10,24 +10,15 @@ const MinimalForwarder = artifacts.require('MinimalForwarder'); const ContextMockCaller = artifacts.require('ContextMockCaller'); const { shouldBehaveLikeRegularContext } = require('../utils/Context.behavior'); -const { getChainId } = require('../helpers/chainid'); - -const name = 'MinimalForwarder'; -const version = '0.0.1'; contract('ERC2771Context', function (accounts) { beforeEach(async function () { this.forwarder = await MinimalForwarder.new(); this.recipient = await ERC2771ContextMock.new(this.forwarder.address); - this.domain = { - name, - version, - chainId: await getChainId(), - verifyingContract: this.forwarder.address, - }; + this.domain = await getDomain(this.forwarder); this.types = { - EIP712Domain, + EIP712Domain: domainType(this.domain), ForwardRequest: [ { name: 'from', type: 'address' }, { name: 'to', type: 'address' }, diff --git a/test/metatx/MinimalForwarder.test.js b/test/metatx/MinimalForwarder.test.js index 24de1719d..4884cc760 100644 --- a/test/metatx/MinimalForwarder.test.js +++ b/test/metatx/MinimalForwarder.test.js @@ -1,29 +1,20 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; -const { EIP712Domain } = require('../helpers/eip712'); +const { getDomain, domainType } = require('../helpers/eip712'); const { expectRevert, constants } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); -const { getChainId } = require('../helpers/chainid'); - const MinimalForwarder = artifacts.require('MinimalForwarder'); const CallReceiverMock = artifacts.require('CallReceiverMock'); -const name = 'MinimalForwarder'; -const version = '0.0.1'; - contract('MinimalForwarder', function (accounts) { beforeEach(async function () { this.forwarder = await MinimalForwarder.new(); - this.domain = { - name, - version, - chainId: await getChainId(), - verifyingContract: this.forwarder.address, - }; + + this.domain = await getDomain(this.forwarder); this.types = { - EIP712Domain, + EIP712Domain: domainType(this.domain), ForwardRequest: [ { name: 'from', type: 'address' }, { name: 'to', type: 'address' }, diff --git a/test/token/ERC20/extensions/ERC20Votes.test.js b/test/token/ERC20/extensions/ERC20Votes.test.js index a340e71a7..9759c1162 100644 --- a/test/token/ERC20/extensions/ERC20Votes.test.js +++ b/test/token/ERC20/extensions/ERC20Votes.test.js @@ -11,7 +11,7 @@ const Wallet = require('ethereumjs-wallet').default; const ERC20Votes = artifacts.require('$ERC20Votes'); const { batchInBlock } = require('../../../helpers/txpool'); -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); +const { getDomain, domainType, domainSeparator } = require('../../../helpers/eip712'); const { getChainId } = require('../../../helpers/chainid'); const Delegation = [ @@ -38,9 +38,7 @@ contract('ERC20Votes', function (accounts) { }); it('domain separator', async function () { - expect(await this.token.DOMAIN_SEPARATOR()).to.equal( - await domainSeparator({ name, version, chainId: this.chainId, verifyingContract: this.token.address }), - ); + expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator)); }); it('minting restriction', async function () { @@ -107,30 +105,26 @@ contract('ERC20Votes', function (accounts) { const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); const nonce = 0; - const buildData = (chainId, verifyingContract, message) => ({ - data: { + const buildData = (contract, message) => + getDomain(contract).then(domain => ({ primaryType: 'Delegation', - types: { EIP712Domain, Delegation }, - domain: { name, version, chainId, verifyingContract }, + types: { EIP712Domain: domainType(domain), Delegation }, + domain, message, - }, - }); + })); beforeEach(async function () { await this.token.$_mint(delegatorAddress, supply); }); it('accept signed delegation', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), - ); + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }) + .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) + .then(fromRpcSig); expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); @@ -155,16 +149,13 @@ contract('ERC20Votes', function (accounts) { }); it('rejects reused signature', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), - ); + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }) + .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) + .then(fromRpcSig); await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); @@ -175,16 +166,13 @@ contract('ERC20Votes', function (accounts) { }); it('rejects bad delegatee', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), - ); + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }) + .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) + .then(fromRpcSig); const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged'); @@ -194,16 +182,14 @@ contract('ERC20Votes', function (accounts) { }); it('rejects bad nonce', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), - ); + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }) + .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) + .then(fromRpcSig); + await expectRevert( this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), 'ERC20Votes: invalid nonce', @@ -212,16 +198,14 @@ contract('ERC20Votes', function (accounts) { it('rejects expired permit', async function () { const expiry = (await time.latest()) - time.duration.weeks(1); - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry, - }), - ), - ); + + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry, + }) + .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) + .then(fromRpcSig); await expectRevert( this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), diff --git a/test/token/ERC20/extensions/ERC20VotesComp.test.js b/test/token/ERC20/extensions/ERC20VotesComp.test.js index 6a7c00013..660ed2124 100644 --- a/test/token/ERC20/extensions/ERC20VotesComp.test.js +++ b/test/token/ERC20/extensions/ERC20VotesComp.test.js @@ -11,7 +11,7 @@ const Wallet = require('ethereumjs-wallet').default; const ERC20VotesComp = artifacts.require('$ERC20VotesComp'); const { batchInBlock } = require('../../../helpers/txpool'); -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); +const { getDomain, domainType, domainSeparator } = require('../../../helpers/eip712'); const { getChainId } = require('../../../helpers/chainid'); const Delegation = [ @@ -38,9 +38,7 @@ contract('ERC20VotesComp', function (accounts) { }); it('domain separator', async function () { - expect(await this.token.DOMAIN_SEPARATOR()).to.equal( - await domainSeparator({ name, version, chainId: this.chainId, verifyingContract: this.token.address }), - ); + expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator)); }); it('minting restriction', async function () { @@ -94,30 +92,26 @@ contract('ERC20VotesComp', function (accounts) { const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); const nonce = 0; - const buildData = (chainId, verifyingContract, message) => ({ - data: { + const buildData = (contract, message) => + getDomain(contract).then(domain => ({ primaryType: 'Delegation', - types: { EIP712Domain, Delegation }, - domain: { name, version, chainId, verifyingContract }, + types: { EIP712Domain: domainType(domain), Delegation }, + domain, message, - }, - }); + })); beforeEach(async function () { await this.token.$_mint(delegatorAddress, supply); }); it('accept signed delegation', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), - ); + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }) + .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) + .then(fromRpcSig); expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); @@ -142,16 +136,13 @@ contract('ERC20VotesComp', function (accounts) { }); it('rejects reused signature', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), - ); + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }) + .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) + .then(fromRpcSig); await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); @@ -162,16 +153,13 @@ contract('ERC20VotesComp', function (accounts) { }); it('rejects bad delegatee', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), - ); + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }) + .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) + .then(fromRpcSig); const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged'); @@ -181,16 +169,14 @@ contract('ERC20VotesComp', function (accounts) { }); it('rejects bad nonce', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), - ); + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }) + .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) + .then(fromRpcSig); + await expectRevert( this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), 'ERC20Votes: invalid nonce', @@ -199,16 +185,14 @@ contract('ERC20VotesComp', function (accounts) { it('rejects expired permit', async function () { const expiry = (await time.latest()) - time.duration.weeks(1); - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry, - }), - ), - ); + + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry, + }) + .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) + .then(fromRpcSig); await expectRevert( this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), diff --git a/test/token/ERC20/extensions/draft-ERC20Permit.test.js b/test/token/ERC20/extensions/draft-ERC20Permit.test.js index eb6737826..33c43c479 100644 --- a/test/token/ERC20/extensions/draft-ERC20Permit.test.js +++ b/test/token/ERC20/extensions/draft-ERC20Permit.test.js @@ -10,7 +10,7 @@ const Wallet = require('ethereumjs-wallet').default; const ERC20Permit = artifacts.require('$ERC20Permit'); -const { EIP712Domain, Permit, domainSeparator } = require('../../../helpers/eip712'); +const { Permit, getDomain, domainType, domainSeparator } = require('../../../helpers/eip712'); const { getChainId } = require('../../../helpers/chainid'); contract('ERC20Permit', function (accounts) { @@ -34,9 +34,7 @@ contract('ERC20Permit', function (accounts) { }); it('domain separator', async function () { - expect(await this.token.DOMAIN_SEPARATOR()).to.equal( - await domainSeparator({ name, version, chainId: this.chainId, verifyingContract: this.token.address }), - ); + expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator)); }); describe('permit', function () { @@ -47,17 +45,18 @@ contract('ERC20Permit', function (accounts) { const nonce = 0; const maxDeadline = MAX_UINT256; - const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ - primaryType: 'Permit', - types: { EIP712Domain, Permit }, - domain: { name, version, chainId, verifyingContract }, - message: { owner, spender, value, nonce, deadline }, - }); + const buildData = (contract, deadline = maxDeadline) => + getDomain(contract).then(domain => ({ + primaryType: 'Permit', + types: { EIP712Domain: domainType(domain), Permit }, + domain, + message: { owner, spender, value, nonce, deadline }, + })); it('accepts owner signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); + const { v, r, s } = await buildData(this.token) + .then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data })) + .then(fromRpcSig); await this.token.permit(owner, spender, value, maxDeadline, v, r, s); @@ -66,9 +65,9 @@ contract('ERC20Permit', function (accounts) { }); it('rejects reused signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); + const { v, r, s } = await buildData(this.token) + .then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data })) + .then(fromRpcSig); await this.token.permit(owner, spender, value, maxDeadline, v, r, s); @@ -80,9 +79,10 @@ contract('ERC20Permit', function (accounts) { it('rejects other signature', async function () { const otherWallet = Wallet.generate(); - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); + + const { v, r, s } = await buildData(this.token) + .then(data => ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data })) + .then(fromRpcSig); await expectRevert( this.token.permit(owner, spender, value, maxDeadline, v, r, s), @@ -93,9 +93,9 @@ contract('ERC20Permit', function (accounts) { it('rejects expired permit', async function () { const deadline = (await time.latest()) - time.duration.weeks(1); - const data = buildData(this.chainId, this.token.address, deadline); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); + const { v, r, s } = await buildData(this.token, deadline) + .then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data })) + .then(fromRpcSig); await expectRevert(this.token.permit(owner, spender, value, deadline, v, r, s), 'ERC20Permit: expired deadline'); }); diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index 878989cb1..a6b10fae3 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -6,8 +6,7 @@ const ERC20ReturnTrueMock = artifacts.require('ERC20ReturnTrueMock'); const ERC20NoReturnMock = artifacts.require('ERC20NoReturnMock'); const ERC20PermitNoRevertMock = artifacts.require('ERC20PermitNoRevertMock'); -const { EIP712Domain, Permit } = require('../../../helpers/eip712'); -const { getChainId } = require('../../../helpers/chainid'); +const { getDomain, domainType, Permit } = require('../../../helpers/eip712'); const { fromRpcSig } = require('ethereumjs-util'); const ethSigUtil = require('eth-sig-util'); @@ -58,16 +57,15 @@ contract('SafeERC20', function (accounts) { const spender = hasNoCode; beforeEach(async function () { - const chainId = await getChainId(); - this.token = await ERC20PermitNoRevertMock.new(); - this.data = { + this.data = await getDomain(this.token).then(domain => ({ primaryType: 'Permit', - types: { EIP712Domain, Permit }, - domain: { name: 'ERC20PermitNoRevertMock', version: '1', chainId, verifyingContract: this.token.address }, + types: { EIP712Domain: domainType(domain), Permit }, + domain, message: { owner, spender, value: '42', nonce: '0', deadline: constants.MAX_UINT256 }, - }; + })); + this.signature = fromRpcSig(ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data: this.data })); }); diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js index 8c20b91ba..f12f22673 100644 --- a/test/utils/cryptography/EIP712.test.js +++ b/test/utils/cryptography/EIP712.test.js @@ -1,8 +1,9 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; -const { EIP712Domain, domainSeparator, hashTypedData } = require('../../helpers/eip712'); +const { getDomain, domainType, domainSeparator, hashTypedData } = require('../../helpers/eip712'); const { getChainId } = require('../../helpers/chainid'); +const { mapValues } = require('../../helpers/map-values'); const EIP712Verifier = artifacts.require('$EIP712Verifier'); @@ -21,19 +22,25 @@ contract('EIP712', function (accounts) { chainId: await getChainId(), verifyingContract: this.eip712.address, }; + this.domainType = domainType(this.domain); }); - it('domain separator', async function () { - const expected = await domainSeparator(this.domain); + describe('domain separator', function () { + it('is internally available', async function () { + const expected = await domainSeparator(this.domain); - expect(await this.eip712.$_domainSeparatorV4()).to.equal(expected); + expect(await this.eip712.$_domainSeparatorV4()).to.equal(expected); + }); + + it("can be rebuilt using EIP-5267's eip712Domain", async function () { + const rebuildDomain = await getDomain(this.eip712); + expect(mapValues(rebuildDomain, String)).to.be.deep.equal(mapValues(this.domain, String)); + }); }); it('hash digest', async function () { const structhash = web3.utils.randomHex(32); - const expected = await hashTypedData(this.domain, structhash); - - expect(await this.eip712.$_hashTypedDataV4(structhash)).to.be.equal(expected); + expect(await this.eip712.$_hashTypedDataV4(structhash)).to.be.equal(hashTypedData(this.domain, structhash)); }); it('digest', async function () { @@ -44,7 +51,7 @@ contract('EIP712', function (accounts) { const data = { types: { - EIP712Domain, + EIP712Domain: this.domainType, Mail: [ { name: 'to', type: 'address' }, { name: 'contents', type: 'string' }, From 5b027e517e6aee69f4b4b2f5e78274ac8ee53513 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 8 Feb 2023 22:03:27 +0100 Subject: [PATCH 027/197] Add missing `await` in tests (#4037) --- .../GovernorTimelockCompound.test.js | 2 +- .../GovernorTimelockControl.test.js | 2 +- test/token/ERC20/extensions/ERC4626.test.js | 100 +++++++++--------- 3 files changed, 52 insertions(+), 52 deletions(-) diff --git a/test/governance/extensions/GovernorTimelockCompound.test.js b/test/governance/extensions/GovernorTimelockCompound.test.js index fb6803383..446d74999 100644 --- a/test/governance/extensions/GovernorTimelockCompound.test.js +++ b/test/governance/extensions/GovernorTimelockCompound.test.js @@ -253,7 +253,7 @@ contract('GovernorTimelockCompound', function (accounts) { expect(await this.token.balanceOf(this.mock.address), 0); expect(await this.token.balanceOf(other), 1); - expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { + await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { from: this.mock.address, to: other, value: '1', diff --git a/test/governance/extensions/GovernorTimelockControl.test.js b/test/governance/extensions/GovernorTimelockControl.test.js index 34fdfe92f..2a55787cb 100644 --- a/test/governance/extensions/GovernorTimelockControl.test.js +++ b/test/governance/extensions/GovernorTimelockControl.test.js @@ -264,7 +264,7 @@ contract('GovernorTimelockControl', function (accounts) { expect(await this.token.balanceOf(this.mock.address), 0); expect(await this.token.balanceOf(other), 1); - expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { + await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { from: this.mock.address, to: other, value: '1', diff --git a/test/token/ERC20/extensions/ERC4626.test.js b/test/token/ERC20/extensions/ERC4626.test.js index f85ffd56d..5ed2aa132 100644 --- a/test/token/ERC20/extensions/ERC4626.test.js +++ b/test/token/ERC20/extensions/ERC4626.test.js @@ -49,13 +49,13 @@ contract('ERC4626', function (accounts) { const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: holder, to: this.vault.address, value: parseToken(1), }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, to: recipient, value: parseShare(1), @@ -68,13 +68,13 @@ contract('ERC4626', function (accounts) { const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: holder, to: this.vault.address, value: parseToken(1), }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, to: recipient, value: parseShare(1), @@ -87,13 +87,13 @@ contract('ERC4626', function (accounts) { const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: recipient, value: '0', }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: holder, to: constants.ZERO_ADDRESS, value: '0', @@ -106,13 +106,13 @@ contract('ERC4626', function (accounts) { const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: recipient, value: '0', }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: holder, to: constants.ZERO_ADDRESS, value: '0', @@ -135,13 +135,13 @@ contract('ERC4626', function (accounts) { const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: holder, to: this.vault.address, value: parseToken(1), }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, to: recipient, value: parseShare(1), @@ -154,13 +154,13 @@ contract('ERC4626', function (accounts) { const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: holder, to: this.vault.address, value: parseToken(1), }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, to: recipient, value: parseShare(1), @@ -173,13 +173,13 @@ contract('ERC4626', function (accounts) { const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: recipient, value: '0', }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: holder, to: constants.ZERO_ADDRESS, value: '0', @@ -192,13 +192,13 @@ contract('ERC4626', function (accounts) { const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: recipient, value: '0', }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: holder, to: constants.ZERO_ADDRESS, value: '0', @@ -221,16 +221,16 @@ contract('ERC4626', function (accounts) { // Can deposit 0 (max deposit) const { tx } = await this.vault.deposit(0, recipient, { from: holder }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: holder, to: this.vault.address, - value: 0, + value: '0', }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, to: recipient, - value: 0, + value: '0', }); // Cannot deposit more than 0 @@ -247,13 +247,13 @@ contract('ERC4626', function (accounts) { const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: holder, to: this.vault.address, value: '0', }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, to: recipient, value: parseShare(1), @@ -267,13 +267,13 @@ contract('ERC4626', function (accounts) { const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: recipient, value: '0', }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: holder, to: constants.ZERO_ADDRESS, value: '0', @@ -286,13 +286,13 @@ contract('ERC4626', function (accounts) { const { tx } = await this.vault.redeem(parseShare(1), recipient, holder, { from: holder }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: recipient, value: '0', }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: holder, to: constants.ZERO_ADDRESS, value: parseShare(1), @@ -316,13 +316,13 @@ contract('ERC4626', function (accounts) { const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: holder, to: this.vault.address, value: parseToken(1), }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, to: recipient, value: parseShare(100), @@ -335,13 +335,13 @@ contract('ERC4626', function (accounts) { const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: holder, to: this.vault.address, value: parseToken(1).divn(100), }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, to: recipient, value: parseShare(1), @@ -354,13 +354,13 @@ contract('ERC4626', function (accounts) { const { tx } = await this.vault.withdraw(parseToken(1), recipient, holder, { from: holder }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: recipient, value: parseToken(1), }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: holder, to: constants.ZERO_ADDRESS, value: parseShare(100), @@ -382,13 +382,13 @@ contract('ERC4626', function (accounts) { const { tx } = await this.vault.redeem(parseShare(100), recipient, holder, { from: holder }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: recipient, value: parseToken(1), }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: holder, to: constants.ZERO_ADDRESS, value: parseShare(100), @@ -420,12 +420,12 @@ contract('ERC4626', function (accounts) { // 1. Alice mints 2000 shares (costs 2000 tokens) { const { tx } = await this.vault.mint(2000, user1, { from: user1 }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: user1, to: this.vault.address, value: '2000', }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, to: user1, value: '2000', @@ -443,12 +443,12 @@ contract('ERC4626', function (accounts) { // 2. Bob deposits 4000 tokens (mints 4000 shares) { const { tx } = await this.vault.mint(4000, user2, { from: user2 }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: user2, to: this.vault.address, value: '4000', }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, to: user2, value: '4000', @@ -476,12 +476,12 @@ contract('ERC4626', function (accounts) { // 4. Alice deposits 2000 tokens (mints 1333 shares) { const { tx } = await this.vault.deposit(2000, user1, { from: user1 }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: user1, to: this.vault.address, value: '2000', }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, to: user1, value: '1333', @@ -500,12 +500,12 @@ contract('ERC4626', function (accounts) { // NOTE: Alices's vault assets got rounded up { const { tx } = await this.vault.mint(2000, user2, { from: user2 }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: user2, to: this.vault.address, value: '3001', }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, to: user2, value: '2000', @@ -533,12 +533,12 @@ contract('ERC4626', function (accounts) { // 7. Alice redeem 1333 shares (2428 assets) { const { tx } = await this.vault.redeem(1333, user1, user1, { from: user1 }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: user1, to: constants.ZERO_ADDRESS, value: '1333', }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: user1, value: '2428', @@ -555,12 +555,12 @@ contract('ERC4626', function (accounts) { // 8. Bob withdraws 2929 assets (1608 shares) { const { tx } = await this.vault.withdraw(2929, user2, user2, { from: user2 }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: user2, to: constants.ZERO_ADDRESS, value: '1608', }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: user2, value: '2929', @@ -578,12 +578,12 @@ contract('ERC4626', function (accounts) { // NOTE: Bob's assets have been rounded back up { const { tx } = await this.vault.withdraw(3643, user1, user1, { from: user1 }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: user1, to: constants.ZERO_ADDRESS, value: '2000', }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: user1, value: '3643', @@ -600,12 +600,12 @@ contract('ERC4626', function (accounts) { // 10. Bob redeem 4392 shares (8001 tokens) { const { tx } = await this.vault.redeem(4392, user2, user2, { from: user2 }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: user2, to: constants.ZERO_ADDRESS, value: '4392', }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: user2, value: '8001', From 94cd8ef12e0ee6d6a929eae83e28598e5d017270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 9 Feb 2023 13:43:34 -0600 Subject: [PATCH 028/197] Add ERC721 Wrapper (#3863) Co-authored-by: Hadrien Croubois --- .changeset/early-oranges-raise.md | 5 + contracts/token/ERC721/README.adoc | 3 + .../token/ERC721/extensions/ERC721Wrapper.sol | 102 ++++++ docs/modules/ROOT/pages/governance.adoc | 2 +- test/token/ERC721/ERC721.behavior.js | 5 +- .../ERC721/extensions/ERC721Wrapper.test.js | 338 ++++++++++++++++++ 6 files changed, 452 insertions(+), 3 deletions(-) create mode 100644 .changeset/early-oranges-raise.md create mode 100644 contracts/token/ERC721/extensions/ERC721Wrapper.sol create mode 100644 test/token/ERC721/extensions/ERC721Wrapper.test.js diff --git a/.changeset/early-oranges-raise.md b/.changeset/early-oranges-raise.md new file mode 100644 index 000000000..af60a4432 --- /dev/null +++ b/.changeset/early-oranges-raise.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC721Wrapper`: add a new extension of the `ERC721` token which wraps an underlying token. Deposit and withdraw guarantee that the ownership of each token is backed by a corresponding underlying token with the same identifier. diff --git a/contracts/token/ERC721/README.adoc b/contracts/token/ERC721/README.adoc index b3377afef..f571f5c57 100644 --- a/contracts/token/ERC721/README.adoc +++ b/contracts/token/ERC721/README.adoc @@ -28,6 +28,7 @@ Additionally there are a few of other extensions: * {ERC721Royalty}: A way to signal royalty information following ERC2981. * {ERC721Pausable}: A primitive to pause contract operation. * {ERC721Burnable}: A way for token holders to burn their own tokens. +* {ERC721Wrapper}: Wrapper to create an ERC721 backed by another ERC721, with deposit and withdraw methods. Useful in conjunction with {ERC721Votes}. NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC721 (such as <>) and expose them as external functions in the way they prefer. On the other hand, xref:ROOT:erc721.adoc#Presets[ERC721 Presets] (such as {ERC721PresetMinterPauserAutoId}) are designed using opinionated patterns to provide developers with ready to use, deployable contracts. @@ -59,6 +60,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel {{ERC721Royalty}} +{{ERC721Wrapper}} + == Presets These contracts are preconfigured combinations of the above features. They can be used through inheritance or as models to copy and paste their source code. diff --git a/contracts/token/ERC721/extensions/ERC721Wrapper.sol b/contracts/token/ERC721/extensions/ERC721Wrapper.sol new file mode 100644 index 000000000..87729188a --- /dev/null +++ b/contracts/token/ERC721/extensions/ERC721Wrapper.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../ERC721.sol"; +import "../utils/ERC721Holder.sol"; + +/** + * @dev Extension of the ERC721 token contract to support token wrapping. + * + * Users can deposit and withdraw an "underlying token" and receive a "wrapped token" with a matching tokenId. This is useful + * in conjunction with other modules. For example, combining this wrapping mechanism with {ERC721Votes} will allow the + * wrapping of an existing "basic" ERC721 into a governance token. + * + * _Available since v4.9.0_ + */ +abstract contract ERC721Wrapper is ERC721, ERC721Holder { + IERC721 private immutable _underlying; + + // Kept as bytes12 so it can be packed with an address + // Equal to 0xb125e89df18e2ceac5fd2fa8 + bytes12 public constant WRAPPER_ACCEPT_MAGIC = bytes12(keccak256("WRAPPER_ACCEPT_MAGIC")); + + constructor(IERC721 underlyingToken) { + _underlying = underlyingToken; + } + + /** + * @dev Allow a user to deposit underlying tokens and mint the corresponding tokenIds. + */ + function depositFor(address account, uint256[] memory tokenIds) public virtual returns (bool) { + bytes memory data = abi.encodePacked(WRAPPER_ACCEPT_MAGIC, account); + + uint256 length = tokenIds.length; + for (uint256 i = 0; i < length; ++i) { + underlying().safeTransferFrom(_msgSender(), address(this), tokenIds[i], data); + } + + return true; + } + + /** + * @dev Allow a user to burn wrapped tokens and withdraw the corresponding tokenIds of the underlying tokens. + */ + function withdrawTo(address account, uint256[] memory tokenIds) public virtual returns (bool) { + 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"); + _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. + // slither-disable-next-line reentrancy-no-eth + underlying().safeTransferFrom(address(this), account, tokenId); + } + + return true; + } + + /** + * @dev Overrides {IERC721Receiver-onERC721Received} to allow minting on direct ERC721 transfers to + * this contract. + * + * In case there's data attached, it validates that the sender is aware of this contract's existence and behavior + * by checking a magic value (`WRAPPER_ACCEPT_MAGIC`) in the first 12 bytes. If it also matches, the rest 20 + * bytes are used as an address to send the tokens to. + * + * WARNING: Doesn't work with unsafe transfers (eg. {IERC721-transferFrom}). Use {ERC721Wrapper-_recover} + * for recovering in that scenario. + */ + function onERC721Received( + address, + address from, + uint256 tokenId, + bytes memory data + ) public override returns (bytes4) { + require(address(underlying()) == _msgSender(), "ERC721Wrapper: caller is not underlying"); + if (data.length > 0) { + require(data.length == 32 && WRAPPER_ACCEPT_MAGIC == bytes12(data), "ERC721Wrapper: Invalid data format"); + from = address(bytes20(bytes32(data) << 96)); + } + _safeMint(from, tokenId); + return IERC721Receiver.onERC721Received.selector; + } + + /** + * @dev Mint a wrapped token to cover any underlyingToken that would have been transferred by mistake. Internal + * 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"); + _safeMint(account, tokenId); + return tokenId; + } + + /** + * @dev Returns the underlying token. + */ + function underlying() public view virtual returns (IERC721) { + return _underlying; + } +} diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 358a63e90..eb76321bf 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -119,7 +119,7 @@ contract MyToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper { } ``` -NOTE: Voting power could be determined in different ways: multiple ERC20 tokens, ERC721 tokens, sybil resistant identities, etc. All of these options are potentially supported by writing a custom Votes module for your Governor. The only other source of voting power available in OpenZeppelin Contracts currently is xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`]. +NOTE:The only other source of voting power available in OpenZeppelin Contracts currently is xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`]. ERC721 tokens that don't provide this functionality can be wrapped into a voting tokens using a combination of xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`] and xref:api:token/ERC721Wrapper.adoc#ERC721Wrapper[`ERC721Wrapper`]. === Governor diff --git a/test/token/ERC721/ERC721.behavior.js b/test/token/ERC721/ERC721.behavior.js index c09479572..6867db31f 100644 --- a/test/token/ERC721/ERC721.behavior.js +++ b/test/token/ERC721/ERC721.behavior.js @@ -5,6 +5,7 @@ const { ZERO_ADDRESS } = constants; const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); const ERC721ReceiverMock = artifacts.require('ERC721ReceiverMock'); +const NonERC721ReceiverMock = artifacts.require('CallReceiverMock'); const Error = ['None', 'RevertWithMessage', 'RevertWithoutMessage', 'Panic'].reduce( (acc, entry, idx) => Object.assign({ [entry]: idx }, acc), @@ -316,7 +317,7 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('to a contract that does not implement the required function', function () { it('reverts', async function () { - const nonReceiver = this.token; + const nonReceiver = await NonERC721ReceiverMock.new(); await expectRevert( this.token.safeTransferFrom(owner, nonReceiver.address, tokenId, { from: owner }), 'ERC721: transfer to non ERC721Receiver implementer', @@ -392,7 +393,7 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('to a contract that does not implement the required function', function () { it('reverts', async function () { - const nonReceiver = this.token; + const nonReceiver = await NonERC721ReceiverMock.new(); await expectRevert( this.token.$_safeMint(nonReceiver.address, tokenId), 'ERC721: transfer to non ERC721Receiver implementer', diff --git a/test/token/ERC721/extensions/ERC721Wrapper.test.js b/test/token/ERC721/extensions/ERC721Wrapper.test.js new file mode 100644 index 000000000..5c83d73cc --- /dev/null +++ b/test/token/ERC721/extensions/ERC721Wrapper.test.js @@ -0,0 +1,338 @@ +const { BN, expectEvent, constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { keccakFromString, bufferToHex } = require('ethereumjs-util'); + +const { shouldBehaveLikeERC721 } = require('../ERC721.behavior'); + +const ERC721 = artifacts.require('$ERC721'); +const ERC721Wrapper = artifacts.require('$ERC721Wrapper'); + +contract('ERC721Wrapper', function (accounts) { + const [initialHolder, anotherAccount, approvedAccount] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const firstTokenId = new BN(1); + const secondTokenId = new BN(2); + + beforeEach(async function () { + this.underlying = await ERC721.new(name, symbol); + this.token = await ERC721Wrapper.new(`Wrapped ${name}`, `W${symbol}`, this.underlying.address); + + await this.underlying.$_safeMint(initialHolder, firstTokenId); + await this.underlying.$_safeMint(initialHolder, secondTokenId); + }); + + it('has a name', async function () { + expect(await this.token.name()).to.equal(`Wrapped ${name}`); + }); + + it('has a symbol', async function () { + expect(await this.token.symbol()).to.equal(`W${symbol}`); + }); + + it('has underlying', async function () { + expect(await this.token.underlying()).to.be.bignumber.equal(this.underlying.address); + }); + + describe('depositFor', function () { + it('works with token approval', async function () { + await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder }); + + const { tx } = await this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: initialHolder, + to: this.token.address, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: initialHolder, + tokenId: firstTokenId, + }); + }); + + it('works with approval for all', async function () { + await this.underlying.setApprovalForAll(this.token.address, true, { from: initialHolder }); + + const { tx } = await this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: initialHolder, + to: this.token.address, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: initialHolder, + tokenId: firstTokenId, + }); + }); + + it('works sending to another account', async function () { + await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder }); + + const { tx } = await this.token.depositFor(anotherAccount, [firstTokenId], { from: initialHolder }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: initialHolder, + to: this.token.address, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: anotherAccount, + tokenId: firstTokenId, + }); + }); + + it('works with multiple tokens', async function () { + await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder }); + await this.underlying.approve(this.token.address, secondTokenId, { from: initialHolder }); + + const { tx } = await this.token.depositFor(initialHolder, [firstTokenId, secondTokenId], { from: initialHolder }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: initialHolder, + to: this.token.address, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: initialHolder, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: initialHolder, + to: this.token.address, + tokenId: secondTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: initialHolder, + tokenId: secondTokenId, + }); + }); + + it('reverts with missing approval', async function () { + await expectRevert( + this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }), + 'ERC721: caller is not token owner or approved', + ); + }); + }); + + describe('withdrawTo', function () { + beforeEach(async function () { + await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder }); + await this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }); + }); + + it('works for an owner', async function () { + const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId], { from: initialHolder }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: this.token.address, + to: initialHolder, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: initialHolder, + to: constants.ZERO_ADDRESS, + tokenId: firstTokenId, + }); + }); + + it('works for an approved', async function () { + await this.token.approve(approvedAccount, firstTokenId, { from: initialHolder }); + + const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId], { from: approvedAccount }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: this.token.address, + to: initialHolder, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: initialHolder, + to: constants.ZERO_ADDRESS, + tokenId: firstTokenId, + }); + }); + + it('works for an approved for all', async function () { + await this.token.setApprovalForAll(approvedAccount, true, { from: initialHolder }); + + const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId], { from: approvedAccount }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: this.token.address, + to: initialHolder, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: initialHolder, + to: constants.ZERO_ADDRESS, + tokenId: firstTokenId, + }); + }); + + it("doesn't work for a non-owner nor approved", async function () { + await expectRevert( + this.token.withdrawTo(initialHolder, [firstTokenId], { from: anotherAccount }), + 'ERC721Wrapper: caller is not token owner or approved', + ); + }); + + it('works with multiple tokens', async function () { + await this.underlying.approve(this.token.address, secondTokenId, { from: initialHolder }); + await this.token.depositFor(initialHolder, [secondTokenId], { from: initialHolder }); + + const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId, secondTokenId], { from: initialHolder }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: this.token.address, + to: initialHolder, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: initialHolder, + to: constants.ZERO_ADDRESS, + tokenId: firstTokenId, + }); + }); + + it('works to another account', async function () { + const { tx } = await this.token.withdrawTo(anotherAccount, [firstTokenId], { from: initialHolder }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: this.token.address, + to: anotherAccount, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: initialHolder, + to: constants.ZERO_ADDRESS, + tokenId: firstTokenId, + }); + }); + }); + + describe('onERC721Received', function () { + const WRAPPER_ACCEPT_MAGIC = bufferToHex(keccakFromString('WRAPPER_ACCEPT_MAGIC')).slice(0, 26); // Include 0x + + const magicWithAddresss = address => + web3.utils.encodePacked( + { + value: WRAPPER_ACCEPT_MAGIC, + type: 'bytes12', + }, + { + value: address, + type: 'address', + }, + ); + + it('only allows calls from underlying', async function () { + await expectRevert( + this.token.onERC721Received( + initialHolder, + this.token.address, + firstTokenId, + magicWithAddresss(anotherAccount), // Correct data + { from: anotherAccount }, + ), + 'ERC721Wrapper: caller is not underlying', + ); + }); + + describe('when data length is > 0', function () { + it('reverts with arbitrary data', async function () { + await expectRevert( + this.underlying.methods['safeTransferFrom(address,address,uint256,bytes)']( + initialHolder, + this.token.address, + firstTokenId, + '0x0123', + { + from: initialHolder, + }, + ), + 'ERC721Wrapper: Invalid data format', + ); + }); + + it('reverts with the magic value and data length different to 32', async function () { + await expectRevert( + this.underlying.methods['safeTransferFrom(address,address,uint256,bytes)']( + initialHolder, + this.token.address, + firstTokenId, + WRAPPER_ACCEPT_MAGIC, // Reverts for any non-32 bytes value + { + from: initialHolder, + }, + ), + 'ERC721Wrapper: Invalid data format', + ); + }); + + it('mints token to specific holder with address after magic value', async function () { + const { tx } = await this.underlying.methods['safeTransferFrom(address,address,uint256,bytes)']( + initialHolder, + this.token.address, + firstTokenId, + magicWithAddresss(anotherAccount), + { + from: initialHolder, + }, + ); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: anotherAccount, + tokenId: firstTokenId, + }); + }); + }); + + it('mints a token to from if no data is specified', async function () { + const { tx } = await this.underlying.safeTransferFrom(initialHolder, this.token.address, firstTokenId, { + from: initialHolder, + }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: initialHolder, + tokenId: firstTokenId, + }); + }); + }); + + describe('_recover', function () { + it('works if there is something to recover', async function () { + // Should use `transferFrom` to avoid `onERC721Received` minting + await this.underlying.transferFrom(initialHolder, this.token.address, firstTokenId, { from: initialHolder }); + + const { tx } = await this.token.$_recover(anotherAccount, firstTokenId); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: anotherAccount, + tokenId: firstTokenId, + }); + }); + + it('reverts if there is nothing to recover', async function () { + await expectRevert( + this.token.$_recover(initialHolder, firstTokenId), + 'ERC721Wrapper: wrapper is not token owner', + ); + }); + }); + + describe('ERC712 behavior', function () { + shouldBehaveLikeERC721('ERC721', ...accounts); + }); +}); From 790cc5b65a66e0515bcbf97c430975bb7fbef8f7 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 9 Feb 2023 22:33:55 +0100 Subject: [PATCH 029/197] Add timestamp based governor with EIP-6372 and EIP-5805 (#3934) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Francisco Giordano Co-authored-by: Ernesto García Co-authored-by: Francisco --- .changeset/four-bats-sniff.md | 5 + .changeset/ninety-hornets-kick.md | 5 + .github/actions/storage-layout/action.yml | 2 +- contracts/governance/Governor.sol | 85 +- contracts/governance/IGovernor.sol | 62 +- contracts/governance/README.adoc | 6 +- .../GovernorCompatibilityBravo.sol | 6 +- .../extensions/GovernorPreventLateQuorum.sol | 19 +- .../extensions/GovernorTimelockCompound.sol | 17 +- .../governance/extensions/GovernorVotes.sol | 34 +- .../extensions/GovernorVotesComp.sol | 30 +- .../GovernorVotesQuorumFraction.sol | 29 +- contracts/governance/utils/IVotes.sol | 10 +- contracts/governance/utils/Votes.sol | 81 +- contracts/interfaces/IERC5805.sol | 9 + contracts/interfaces/IERC6372.sol | 17 + contracts/mocks/VotesMock.sol | 11 + .../mocks/token/ERC20VotesLegacyMock.sol | 262 ++++ contracts/mocks/token/VotesTimestamp.sol | 40 + .../token/ERC20/extensions/ERC20Votes.sol | 63 +- contracts/utils/Checkpoints.sol | 50 + scripts/checks/compare-layout.js | 1 - scripts/checks/compareGasReports.js | 16 +- scripts/generate/templates/Checkpoints.js | 66 +- test/governance/Governor.test.js | 1266 +++++++++-------- .../GovernorCompatibilityBravo.test.js | 425 +++--- .../extensions/GovernorComp.test.js | 115 +- .../extensions/GovernorERC721.test.js | 172 +-- .../GovernorPreventLateQuorum.test.js | 309 ++-- .../GovernorTimelockCompound.test.js | 577 ++++---- .../GovernorTimelockControl.test.js | 729 +++++----- .../GovernorVotesQuorumFraction.test.js | 242 ++-- .../extensions/GovernorWithParams.test.js | 252 ++-- test/governance/utils/EIP6372.behavior.js | 23 + test/governance/utils/Votes.behavior.js | 91 +- test/governance/utils/Votes.test.js | 99 +- test/helpers/governance.js | 15 +- test/helpers/time.js | 16 + .../token/ERC20/extensions/ERC20Votes.test.js | 1036 +++++++------- .../ERC20/extensions/ERC20VotesComp.test.js | 967 ++++++------- .../ERC721/extensions/ERC721Votes.test.js | 1 + test/utils/Checkpoints.test.js | 29 +- 42 files changed, 4081 insertions(+), 3209 deletions(-) create mode 100644 .changeset/four-bats-sniff.md create mode 100644 .changeset/ninety-hornets-kick.md create mode 100644 contracts/interfaces/IERC5805.sol create mode 100644 contracts/interfaces/IERC6372.sol create mode 100644 contracts/mocks/token/ERC20VotesLegacyMock.sol create mode 100644 contracts/mocks/token/VotesTimestamp.sol create mode 100644 test/governance/utils/EIP6372.behavior.js create mode 100644 test/helpers/time.js diff --git a/.changeset/four-bats-sniff.md b/.changeset/four-bats-sniff.md new file mode 100644 index 000000000..137b5e515 --- /dev/null +++ b/.changeset/four-bats-sniff.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Governor`: Enable timestamp operation for blockchains without a stable block time. This is achieved by connecting a Governor's internal clock to match a voting token's EIP-6372 interface. diff --git a/.changeset/ninety-hornets-kick.md b/.changeset/ninety-hornets-kick.md new file mode 100644 index 000000000..16886c5c1 --- /dev/null +++ b/.changeset/ninety-hornets-kick.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Votes`, `ERC20Votes`, `ERC721Votes`: support timestamp checkpointing using EIP-6372. diff --git a/.github/actions/storage-layout/action.yml b/.github/actions/storage-layout/action.yml index e3e104a01..fdfd5a25b 100644 --- a/.github/actions/storage-layout/action.yml +++ b/.github/actions/storage-layout/action.yml @@ -40,7 +40,7 @@ runs: - name: Compare layouts if: steps.reference.outcome == 'success' && github.event_name == 'pull_request' run: | - node scripts/checks/compare-layout.js --head ${{ inputs.layout }} --ref ${{ inputs.ref_layout }} >> $GITHUB_STEP_SUMMARY + node scripts/checks/compare-layout.js --head ${{ inputs.layout }} --ref ${{ inputs.ref_layout }} shell: bash - name: Rename artifacts for upload if: github.event_name != 'pull_request' diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index f82f9fea8..e3691a3ed 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -12,7 +12,6 @@ import "../utils/math/SafeCast.sol"; import "../utils/structs/DoubleEndedQueue.sol"; import "../utils/Address.sol"; import "../utils/Context.sol"; -import "../utils/Timers.sol"; import "./IGovernor.sol"; /** @@ -29,22 +28,29 @@ import "./IGovernor.sol"; abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receiver, IERC1155Receiver { using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; using SafeCast for uint256; - using Timers for Timers.BlockNumber; bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,uint8 support)"); bytes32 public constant EXTENDED_BALLOT_TYPEHASH = keccak256("ExtendedBallot(uint256 proposalId,uint8 support,string reason,bytes params)"); + // solhint-disable var-name-mixedcase struct ProposalCore { - Timers.BlockNumber voteStart; - Timers.BlockNumber voteEnd; + // --- start retyped from Timers.BlockNumber at offset 0x00 --- + uint64 voteStart; + address proposer; + bytes4 __gap_unused0; + // --- start retyped from Timers.BlockNumber at offset 0x20 --- + uint64 voteEnd; + bytes24 __gap_unused1; + // --- Remaining fields starting at offset 0x40 --------------- bool executed; bool canceled; - address proposer; } + // solhint-enable var-name-mixedcase string private _name; + /// @custom:oz-retyped-from mapping(uint256 => Governor.ProposalCore) mapping(uint256 => ProposalCore) private _proposals; // This queue keeps track of the governor operating on itself. Calls to functions protected by the @@ -96,12 +102,13 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive return interfaceId == (type(IGovernor).interfaceId ^ + type(IERC6372).interfaceId ^ this.cancel.selector ^ this.castVoteWithReasonAndParams.selector ^ this.castVoteWithReasonAndParamsBySig.selector ^ this.getVotesWithParams.selector) || - interfaceId == (type(IGovernor).interfaceId ^ this.cancel.selector) || - interfaceId == type(IGovernor).interfaceId || + // Previous interface for backwards compatibility + interfaceId == (type(IGovernor).interfaceId ^ type(IERC6372).interfaceId ^ this.cancel.selector) || interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); } @@ -162,13 +169,15 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive revert("Governor: unknown proposal id"); } - if (snapshot >= block.number) { + uint256 currentTimepoint = clock(); + + if (snapshot >= currentTimepoint) { return ProposalState.Pending; } uint256 deadline = proposalDeadline(proposalId); - if (deadline >= block.number) { + if (deadline >= currentTimepoint) { return ProposalState.Active; } @@ -179,25 +188,32 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive } } + /** + * @dev Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_. + */ + function proposalThreshold() public view virtual returns (uint256) { + return 0; + } + /** * @dev See {IGovernor-proposalSnapshot}. */ function proposalSnapshot(uint256 proposalId) public view virtual override returns (uint256) { - return _proposals[proposalId].voteStart.getDeadline(); + return _proposals[proposalId].voteStart; } /** * @dev See {IGovernor-proposalDeadline}. */ function proposalDeadline(uint256 proposalId) public view virtual override returns (uint256) { - return _proposals[proposalId].voteEnd.getDeadline(); + return _proposals[proposalId].voteEnd; } /** - * @dev Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_. + * @dev Address of the proposer */ - function proposalThreshold() public view virtual returns (uint256) { - return 0; + function _proposalProposer(uint256 proposalId) internal view virtual returns (address) { + return _proposals[proposalId].proposer; } /** @@ -211,13 +227,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive function _voteSucceeded(uint256 proposalId) internal view virtual returns (bool); /** - * @dev Get the voting weight of `account` at a specific `blockNumber`, for a vote as described by `params`. + * @dev Get the voting weight of `account` at a specific `timepoint`, for a vote as described by `params`. */ - function _getVotes( - address account, - uint256 blockNumber, - bytes memory params - ) internal view virtual returns (uint256); + function _getVotes(address account, uint256 timepoint, bytes memory params) internal view virtual returns (uint256); /** * @dev Register a vote for `proposalId` by `account` with a given `support`, voting `weight` and voting `params`. @@ -252,9 +264,10 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive string memory description ) public virtual override returns (uint256) { address proposer = _msgSender(); + uint256 currentTimepoint = clock(); require( - getVotes(proposer, block.number - 1) >= proposalThreshold(), + getVotes(proposer, currentTimepoint - 1) >= proposalThreshold(), "Governor: proposer votes below proposal threshold" ); @@ -263,16 +276,20 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive 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].proposer == address(0), "Governor: proposal already exists"); - ProposalCore storage proposal = _proposals[proposalId]; - require(proposal.voteStart.isUnset(), "Governor: proposal already exists"); + uint256 snapshot = currentTimepoint + votingDelay(); + uint256 deadline = snapshot + votingPeriod(); - uint64 snapshot = block.number.toUint64() + votingDelay().toUint64(); - uint64 deadline = snapshot + votingPeriod().toUint64(); - - proposal.voteStart.setDeadline(snapshot); - proposal.voteEnd.setDeadline(deadline); - proposal.proposer = proposer; + _proposals[proposalId] = ProposalCore({ + proposer: proposer, + voteStart: snapshot.toUint64(), + voteEnd: deadline.toUint64(), + executed: false, + canceled: false, + __gap_unused0: 0, + __gap_unused1: 0 + }); emit ProposalCreated( proposalId, @@ -416,8 +433,8 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive /** * @dev See {IGovernor-getVotes}. */ - function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { - return _getVotes(account, blockNumber, _defaultParams()); + function getVotes(address account, uint256 timepoint) public view virtual override returns (uint256) { + return _getVotes(account, timepoint, _defaultParams()); } /** @@ -425,10 +442,10 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive */ function getVotesWithParams( address account, - uint256 blockNumber, + uint256 timepoint, bytes memory params ) public view virtual override returns (uint256) { - return _getVotes(account, blockNumber, params); + return _getVotes(account, timepoint, params); } /** @@ -546,7 +563,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive ProposalCore storage proposal = _proposals[proposalId]; require(state(proposalId) == ProposalState.Active, "Governor: vote not currently active"); - uint256 weight = _getVotes(account, proposal.voteStart.getDeadline(), params); + uint256 weight = _getVotes(account, proposal.voteStart, params); _countVote(proposalId, account, support, weight, params); if (params.length == 0) { diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 60a4b5470..b12d2f632 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -3,14 +3,15 @@ pragma solidity ^0.8.0; -import "../utils/introspection/ERC165.sol"; +import "../interfaces/IERC165.sol"; +import "../interfaces/IERC6372.sol"; /** * @dev Interface of the {Governor} core. * * _Available since v4.3._ */ -abstract contract IGovernor is IERC165 { +abstract contract IGovernor is IERC165, IERC6372 { enum ProposalState { Pending, Active, @@ -32,8 +33,8 @@ abstract contract IGovernor is IERC165 { uint256[] values, string[] signatures, bytes[] calldatas, - uint256 startBlock, - uint256 endBlock, + uint256 voteStart, + uint256 voteEnd, string description ); @@ -81,6 +82,19 @@ abstract contract IGovernor is IERC165 { */ function version() public view virtual returns (string memory); + /** + * @notice module:core + * @dev See {IERC6372} + */ + function clock() public view virtual override returns (uint48); + + /** + * @notice module:core + * @dev See EIP-6372. + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory); + /** * @notice module:voting * @dev A description of the possible `support` values for {castVote} and the way these votes are counted, meant to @@ -104,7 +118,7 @@ abstract contract IGovernor is IERC165 { * JavaScript class. */ // solhint-disable-next-line func-name-mixedcase - function COUNTING_MODE() public pure virtual returns (string memory); + function COUNTING_MODE() public view virtual returns (string memory); /** * @notice module:core @@ -125,29 +139,33 @@ abstract contract IGovernor is IERC165 { /** * @notice module:core - * @dev Block number used to retrieve user's votes and quorum. As per Compound's Comp and OpenZeppelin's - * ERC20Votes, the snapshot is performed at the end of this block. Hence, voting for this proposal starts at the - * beginning of the following block. + * @dev Timepoint used to retrieve user's votes and quorum. If using block number (as per Compound's Comp), the + * snapshot is performed at the end of this block. Hence, voting for this proposal starts at the beginning of the + * following block. */ function proposalSnapshot(uint256 proposalId) public view virtual returns (uint256); /** * @notice module:core - * @dev Block number at which votes close. Votes close at the end of this block, so it is possible to cast a vote - * during this block. + * @dev Timepoint at which votes close. If using block number, votes close at the end of this block, so it is + * possible to cast a vote during this block. */ function proposalDeadline(uint256 proposalId) public view virtual returns (uint256); /** * @notice module:user-config - * @dev Delay, in number of block, between the proposal is created and the vote starts. This can be increased to - * leave time for users to buy voting power, or delegate it, before the voting of a proposal starts. + * @dev Delay, between the proposal is created and the vote starts. The unit this duration is expressed in depends + * on the clock (see EIP-6372) this contract uses. + * + * This can be increased to leave time for users to buy voting power, or delegate it, before the voting of a + * proposal starts. */ function votingDelay() public view virtual returns (uint256); /** * @notice module:user-config - * @dev Delay, in number of blocks, between the vote start and vote ends. + * @dev Delay, between the vote start and vote ends. The unit this duration is expressed in depends on the clock + * (see EIP-6372) this contract uses. * * NOTE: The {votingDelay} can delay the start of the vote. This must be considered when setting the voting * duration compared to the voting delay. @@ -158,27 +176,27 @@ abstract contract IGovernor is IERC165 { * @notice module:user-config * @dev Minimum number of cast voted required for a proposal to be successful. * - * Note: The `blockNumber` parameter corresponds to the snapshot used for counting vote. This allows to scale the - * quorum depending on values such as the totalSupply of a token at this block (see {ERC20Votes}). + * NOTE: The `timepoint` parameter corresponds to the snapshot used for counting vote. This allows to scale the + * quorum depending on values such as the totalSupply of a token at this timepoint (see {ERC20Votes}). */ - function quorum(uint256 blockNumber) public view virtual returns (uint256); + function quorum(uint256 timepoint) public view virtual returns (uint256); /** * @notice module:reputation - * @dev Voting power of an `account` at a specific `blockNumber`. + * @dev Voting power of an `account` at a specific `timepoint`. * * Note: this can be implemented in a number of ways, for example by reading the delegated balance from one (or * multiple), {ERC20Votes} tokens. */ - function getVotes(address account, uint256 blockNumber) public view virtual returns (uint256); + function getVotes(address account, uint256 timepoint) public view virtual returns (uint256); /** * @notice module:reputation - * @dev Voting power of an `account` at a specific `blockNumber` given additional encoded parameters. + * @dev Voting power of an `account` at a specific `timepoint` given additional encoded parameters. */ function getVotesWithParams( address account, - uint256 blockNumber, + uint256 timepoint, bytes memory params ) public view virtual returns (uint256); @@ -189,8 +207,8 @@ abstract contract IGovernor is IERC165 { function hasVoted(uint256 proposalId, address account) public view virtual returns (bool); /** - * @dev Create a new proposal. Vote start {IGovernor-votingDelay} blocks after the proposal is created and ends - * {IGovernor-votingPeriod} blocks after the voting starts. + * @dev Create a new proposal. Vote start after a delay specified by {IGovernor-votingDelay} and lasts for a + * duration specified by {IGovernor-votingPeriod}. * * Emits a {ProposalCreated} event. */ diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index 9bfa64fea..6d53e973d 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -46,9 +46,9 @@ Other extensions can customize the behavior or interface in multiple ways. In addition to modules and extensions, the core contract requires a few virtual functions to be implemented to your particular specifications: -* <>: Delay (in number of blocks) since the proposal is submitted until voting power is fixed and voting starts. This can be used to enforce a delay after a proposal is published for users to buy tokens, or delegate their votes. -* <>: Delay (in number of blocks) since the proposal starts until voting ends. -* <>: Quorum required for a proposal to be successful. This function includes a `blockNumber` argument so the quorum can adapt through time, for example, to follow a token's `totalSupply`. +* <>: Delay (in EIP-6372 clock) since the proposal is submitted until voting power is fixed and voting starts. This can be used to enforce a delay after a proposal is published for users to buy tokens, or delegate their votes. +* <>: Delay (in EIP-6372 clock) since the proposal starts until voting ends. +* <>: Quorum required for a proposal to be successful. This function includes a `timepoint` argument (see EIP-6372) so the quorum can adapt through time, for example, to follow a token's `totalSupply`. NOTE: Functions of the `Governor` contract do not include access control. If you want to restrict access, you should add these checks by overloading the particular functions. Among these, {Governor-_cancel} is internal by default, and you will have to expose it (with the right access control mechanism) yourself if this function is needed. diff --git a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol index 912d02f8a..1b8c09368 100644 --- a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol @@ -100,10 +100,10 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp } function cancel(uint256 proposalId) public virtual override(IGovernor, Governor) { - ProposalDetails storage details = _proposalDetails[proposalId]; + address proposer = _proposalDetails[proposalId].proposer; require( - _msgSender() == details.proposer || getVotes(details.proposer, block.number - 1) < proposalThreshold(), + _msgSender() == proposer || getVotes(proposer, clock() - 1) < proposalThreshold(), "GovernorBravo: proposer above threshold" ); @@ -225,7 +225,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp * @dev See {IGovernorCompatibilityBravo-quorumVotes}. */ function quorumVotes() public view virtual override returns (uint256) { - return quorum(block.number - 1); + return quorum(clock() - 1); } // ==================================================== Voting ==================================================== diff --git a/contracts/governance/extensions/GovernorPreventLateQuorum.sol b/contracts/governance/extensions/GovernorPreventLateQuorum.sol index a26bbf059..676d2b13e 100644 --- a/contracts/governance/extensions/GovernorPreventLateQuorum.sol +++ b/contracts/governance/extensions/GovernorPreventLateQuorum.sol @@ -19,10 +19,11 @@ import "../../utils/math/Math.sol"; */ abstract contract GovernorPreventLateQuorum is Governor { using SafeCast for uint256; - using Timers for Timers.BlockNumber; uint64 private _voteExtension; - mapping(uint256 => Timers.BlockNumber) private _extendedDeadlines; + + /// @custom:oz-retyped-from mapping(uint256 => Timers.BlockNumber) + mapping(uint256 => uint64) private _extendedDeadlines; /// @dev Emitted when a proposal deadline is pushed back due to reaching quorum late in its voting period. event ProposalExtended(uint256 indexed proposalId, uint64 extendedDeadline); @@ -44,7 +45,7 @@ abstract contract GovernorPreventLateQuorum is Governor { * proposal reached quorum late in the voting period. See {Governor-proposalDeadline}. */ function proposalDeadline(uint256 proposalId) public view virtual override returns (uint256) { - return Math.max(super.proposalDeadline(proposalId), _extendedDeadlines[proposalId].getDeadline()); + return Math.max(super.proposalDeadline(proposalId), _extendedDeadlines[proposalId]); } /** @@ -62,16 +63,14 @@ abstract contract GovernorPreventLateQuorum is Governor { ) internal virtual override returns (uint256) { uint256 result = super._castVote(proposalId, account, support, reason, params); - Timers.BlockNumber storage extendedDeadline = _extendedDeadlines[proposalId]; + if (_extendedDeadlines[proposalId] == 0 && _quorumReached(proposalId)) { + uint64 extendedDeadline = clock() + lateQuorumVoteExtension(); - if (extendedDeadline.isUnset() && _quorumReached(proposalId)) { - uint64 extendedDeadlineValue = block.number.toUint64() + lateQuorumVoteExtension(); - - if (extendedDeadlineValue > proposalDeadline(proposalId)) { - emit ProposalExtended(proposalId, extendedDeadlineValue); + if (extendedDeadline > proposalDeadline(proposalId)) { + emit ProposalExtended(proposalId, extendedDeadline); } - extendedDeadline.setDeadline(extendedDeadlineValue); + _extendedDeadlines[proposalId] = extendedDeadline; } return result; diff --git a/contracts/governance/extensions/GovernorTimelockCompound.sol b/contracts/governance/extensions/GovernorTimelockCompound.sol index 2fa539ead..629f8f800 100644 --- a/contracts/governance/extensions/GovernorTimelockCompound.sol +++ b/contracts/governance/extensions/GovernorTimelockCompound.sol @@ -22,15 +22,11 @@ import "../../vendor/compound/ICompoundTimelock.sol"; */ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { using SafeCast for uint256; - using Timers for Timers.Timestamp; - - struct ProposalTimelock { - Timers.Timestamp timer; - } ICompoundTimelock private _timelock; - mapping(uint256 => ProposalTimelock) private _proposalTimelocks; + /// @custom:oz-retyped-from mapping(uint256 => GovernorTimelockCompound.ProposalTimelock) + mapping(uint256 => uint64) private _proposalTimelocks; /** * @dev Emitted when the timelock controller used for proposal execution is modified. @@ -82,7 +78,7 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { * @dev Public accessor to check the eta of a queued proposal */ function proposalEta(uint256 proposalId) public view virtual override returns (uint256) { - return _proposalTimelocks[proposalId].timer.getDeadline(); + return _proposalTimelocks[proposalId]; } /** @@ -99,7 +95,8 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { require(state(proposalId) == ProposalState.Succeeded, "Governor: proposal not successful"); uint256 eta = block.timestamp + _timelock.delay(); - _proposalTimelocks[proposalId].timer.setDeadline(eta.toUint64()); + _proposalTimelocks[proposalId] = eta.toUint64(); + for (uint256 i = 0; i < targets.length; ++i) { require( !_timelock.queuedTransactions(keccak256(abi.encode(targets[i], values[i], "", calldatas[i], eta))), @@ -145,10 +142,12 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { uint256 eta = proposalEta(proposalId); if (eta > 0) { + // update state first + delete _proposalTimelocks[proposalId]; + // do external call later for (uint256 i = 0; i < targets.length; ++i) { _timelock.cancelTransaction(targets[i], values[i], "", calldatas[i], eta); } - _proposalTimelocks[proposalId].timer.reset(); } return proposalId; diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol index b328c25d1..644317111 100644 --- a/contracts/governance/extensions/GovernorVotes.sol +++ b/contracts/governance/extensions/GovernorVotes.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import "../Governor.sol"; -import "../utils/IVotes.sol"; +import "../../interfaces/IERC5805.sol"; /** * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token, or since v4.5 an {ERC721Votes} token. @@ -12,10 +12,34 @@ import "../utils/IVotes.sol"; * _Available since v4.3._ */ abstract contract GovernorVotes is Governor { - IVotes public immutable token; + IERC5805 public immutable token; constructor(IVotes tokenAddress) { - token = tokenAddress; + token = IERC5805(address(tokenAddress)); + } + + /** + * @dev Clock (as specified in EIP-6372) is set to match the token's clock. Fallback to block numbers if the token + * does not implement EIP-6372. + */ + function clock() public view virtual override returns (uint48) { + try token.clock() returns (uint48 timepoint) { + return timepoint; + } catch { + return SafeCast.toUint48(block.number); + } + } + + /** + * @dev Machine-readable description of the clock as specified in EIP-6372. + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + try token.CLOCK_MODE() returns (string memory clockmode) { + return clockmode; + } catch { + return "mode=blocknumber&from=default"; + } } /** @@ -23,9 +47,9 @@ abstract contract GovernorVotes is Governor { */ function _getVotes( address account, - uint256 blockNumber, + uint256 timepoint, bytes memory /*params*/ ) internal view virtual override returns (uint256) { - return token.getPastVotes(account, blockNumber); + return token.getPastVotes(account, timepoint); } } diff --git a/contracts/governance/extensions/GovernorVotesComp.sol b/contracts/governance/extensions/GovernorVotesComp.sol index 8c4a1f8c1..17250ad7b 100644 --- a/contracts/governance/extensions/GovernorVotesComp.sol +++ b/contracts/governance/extensions/GovernorVotesComp.sol @@ -19,13 +19,37 @@ abstract contract GovernorVotesComp is Governor { } /** - * Read the voting weight from the token's built in snapshot mechanism (see {Governor-_getVotes}). + * @dev Clock (as specified in EIP-6372) is set to match the token's clock. Fallback to block numbers if the token + * does not implement EIP-6372. + */ + function clock() public view virtual override returns (uint48) { + try token.clock() returns (uint48 timepoint) { + return timepoint; + } catch { + return SafeCast.toUint48(block.number); + } + } + + /** + * @dev Machine-readable description of the clock as specified in EIP-6372. + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + try token.CLOCK_MODE() returns (string memory clockmode) { + return clockmode; + } catch { + return "mode=blocknumber&from=default"; + } + } + + /** + * Read the voting weight from the token's built-in snapshot mechanism (see {Governor-_getVotes}). */ function _getVotes( address account, - uint256 blockNumber, + uint256 timepoint, bytes memory /*params*/ ) internal view virtual override returns (uint256) { - return token.getPriorVotes(account, blockNumber); + return token.getPriorVotes(account, timepoint); } } diff --git a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol index 8efefce39..19c5b194d 100644 --- a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol +++ b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol @@ -14,10 +14,13 @@ import "../../utils/math/SafeCast.sol"; * _Available since v4.3._ */ abstract contract GovernorVotesQuorumFraction is GovernorVotes { - using Checkpoints for Checkpoints.History; + using SafeCast for *; + using Checkpoints for Checkpoints.Trace224; - uint256 private _quorumNumerator; // DEPRECATED - Checkpoints.History private _quorumNumeratorHistory; + uint256 private _quorumNumerator; // DEPRECATED in favor of _quorumNumeratorHistory + + /// @custom:oz-retyped-from Checkpoints.History + Checkpoints.Trace224 private _quorumNumeratorHistory; event QuorumNumeratorUpdated(uint256 oldQuorumNumerator, uint256 newQuorumNumerator); @@ -40,9 +43,9 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes { } /** - * @dev Returns the quorum numerator at a specific block number. See {quorumDenominator}. + * @dev Returns the quorum numerator at a specific timepoint. See {quorumDenominator}. */ - function quorumNumerator(uint256 blockNumber) public view virtual returns (uint256) { + function quorumNumerator(uint256 timepoint) public view virtual returns (uint256) { // If history is empty, fallback to old storage uint256 length = _quorumNumeratorHistory._checkpoints.length; if (length == 0) { @@ -50,13 +53,13 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes { } // Optimistic search, check the latest checkpoint - Checkpoints.Checkpoint memory latest = _quorumNumeratorHistory._checkpoints[length - 1]; - if (latest._blockNumber <= blockNumber) { + Checkpoints.Checkpoint224 memory latest = _quorumNumeratorHistory._checkpoints[length - 1]; + if (latest._key <= timepoint) { return latest._value; } // Otherwise, do the binary search - return _quorumNumeratorHistory.getAtBlock(blockNumber); + return _quorumNumeratorHistory.upperLookupRecent(timepoint.toUint32()); } /** @@ -67,10 +70,10 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes { } /** - * @dev Returns the quorum for a block number, in terms of number of votes: `supply * numerator / denominator`. + * @dev Returns the quorum for a timepoint, in terms of number of votes: `supply * numerator / denominator`. */ - function quorum(uint256 blockNumber) public view virtual override returns (uint256) { - return (token.getPastTotalSupply(blockNumber) * quorumNumerator(blockNumber)) / quorumDenominator(); + function quorum(uint256 timepoint) public view virtual override returns (uint256) { + return (token.getPastTotalSupply(timepoint) * quorumNumerator(timepoint)) / quorumDenominator(); } /** @@ -107,12 +110,12 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes { // Make sure we keep track of the original numerator in contracts upgraded from a version without checkpoints. if (oldQuorumNumerator != 0 && _quorumNumeratorHistory._checkpoints.length == 0) { _quorumNumeratorHistory._checkpoints.push( - Checkpoints.Checkpoint({_blockNumber: 0, _value: SafeCast.toUint224(oldQuorumNumerator)}) + Checkpoints.Checkpoint224({_key: 0, _value: oldQuorumNumerator.toUint224()}) ); } // Set new quorum for future proposals - _quorumNumeratorHistory.push(newQuorumNumerator); + _quorumNumeratorHistory.push(clock().toUint32(), newQuorumNumerator.toUint224()); emit QuorumNumeratorUpdated(oldQuorumNumerator, newQuorumNumerator); } diff --git a/contracts/governance/utils/IVotes.sol b/contracts/governance/utils/IVotes.sol index 0bef3f920..c73d0732b 100644 --- a/contracts/governance/utils/IVotes.sol +++ b/contracts/governance/utils/IVotes.sol @@ -24,18 +24,20 @@ interface IVotes { function getVotes(address account) external view returns (uint256); /** - * @dev Returns the amount of votes that `account` had at the end of a past block (`blockNumber`). + * @dev Returns the amount of votes that `account` had at a specific moment in the past. If the `clock()` is + * configured to use block numbers, this will return the value the end of the corresponding block. */ - function getPastVotes(address account, uint256 blockNumber) external view returns (uint256); + function getPastVotes(address account, uint256 timepoint) external view returns (uint256); /** - * @dev Returns the total supply of votes available at the end of a past block (`blockNumber`). + * @dev Returns the total supply of votes available at a specific moment in the past. If the `clock()` is + * configured to use block numbers, this will return the value the end of the corresponding block. * * NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. * Votes that have not been delegated are still part of total supply, even though they would not participate in a * vote. */ - function getPastTotalSupply(uint256 blockNumber) external view returns (uint256); + function getPastTotalSupply(uint256 timepoint) external view returns (uint256); /** * @dev Returns the delegate that `account` has chosen. diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index 2eecf32c6..f70cf3831 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -2,11 +2,11 @@ // OpenZeppelin Contracts (last updated v4.8.0) (governance/utils/Votes.sol) pragma solidity ^0.8.0; +import "../../interfaces/IERC5805.sol"; import "../../utils/Context.sol"; import "../../utils/Counters.sol"; import "../../utils/Checkpoints.sol"; import "../../utils/cryptography/EIP712.sol"; -import "./IVotes.sol"; /** * @dev This is a base abstract contract that tracks voting units, which are a measure of voting power that can be @@ -28,19 +28,41 @@ import "./IVotes.sol"; * * _Available since v4.5._ */ -abstract contract Votes is IVotes, Context, EIP712 { - using Checkpoints for Checkpoints.History; +abstract contract Votes is Context, EIP712, IERC5805 { + using Checkpoints for Checkpoints.Trace224; using Counters for Counters.Counter; bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); mapping(address => address) private _delegation; - mapping(address => Checkpoints.History) private _delegateCheckpoints; - Checkpoints.History private _totalCheckpoints; + + /// @custom:oz-retyped-from mapping(address => Checkpoints.History) + mapping(address => Checkpoints.Trace224) private _delegateCheckpoints; + + /// @custom:oz-retyped-from Checkpoints.History + Checkpoints.Trace224 private _totalCheckpoints; mapping(address => Counters.Counter) private _nonces; + /** + * @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. + */ + function clock() public view virtual override returns (uint48) { + return SafeCast.toUint48(block.number); + } + + /** + * @dev Machine-readable description of the clock as specified in EIP-6372. + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + // Check that the clock was not modified + require(clock() == block.number); + return "mode=blocknumber&from=default"; + } + /** * @dev Returns the current amount of votes that `account` has. */ @@ -49,18 +71,21 @@ abstract contract Votes is IVotes, Context, EIP712 { } /** - * @dev Returns the amount of votes that `account` had at the end of a past block (`blockNumber`). + * @dev Returns the amount of votes that `account` had at a specific moment in the past. If the `clock()` is + * configured to use block numbers, this will return the value the end of the corresponding block. * * Requirements: * - * - `blockNumber` must have been already mined + * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. */ - function getPastVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { - return _delegateCheckpoints[account].getAtProbablyRecentBlock(blockNumber); + function getPastVotes(address account, uint256 timepoint) public view virtual override returns (uint256) { + require(timepoint < clock(), "Votes: future lookup"); + return _delegateCheckpoints[account].upperLookupRecent(SafeCast.toUint32(timepoint)); } /** - * @dev Returns the total supply of votes available at the end of a past block (`blockNumber`). + * @dev Returns the total supply of votes available at a specific moment in the past. If the `clock()` is + * configured to use block numbers, this will return the value the end of the corresponding block. * * NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. * Votes that have not been delegated are still part of total supply, even though they would not participate in a @@ -68,11 +93,11 @@ abstract contract Votes is IVotes, Context, EIP712 { * * Requirements: * - * - `blockNumber` must have been already mined + * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. */ - function getPastTotalSupply(uint256 blockNumber) public view virtual override returns (uint256) { - require(blockNumber < block.number, "Votes: block not yet mined"); - return _totalCheckpoints.getAtProbablyRecentBlock(blockNumber); + function getPastTotalSupply(uint256 timepoint) public view virtual override returns (uint256) { + require(timepoint < clock(), "Votes: future lookup"); + return _totalCheckpoints.upperLookupRecent(SafeCast.toUint32(timepoint)); } /** @@ -138,10 +163,10 @@ abstract contract Votes is IVotes, Context, EIP712 { */ function _transferVotingUnits(address from, address to, uint256 amount) internal virtual { if (from == address(0)) { - _totalCheckpoints.push(_add, amount); + _push(_totalCheckpoints, _add, SafeCast.toUint224(amount)); } if (to == address(0)) { - _totalCheckpoints.push(_subtract, amount); + _push(_totalCheckpoints, _subtract, SafeCast.toUint224(amount)); } _moveDelegateVotes(delegates(from), delegates(to), amount); } @@ -152,21 +177,37 @@ abstract contract Votes is IVotes, Context, EIP712 { function _moveDelegateVotes(address from, address to, uint256 amount) private { if (from != to && amount > 0) { if (from != address(0)) { - (uint256 oldValue, uint256 newValue) = _delegateCheckpoints[from].push(_subtract, amount); + (uint256 oldValue, uint256 newValue) = _push( + _delegateCheckpoints[from], + _subtract, + SafeCast.toUint224(amount) + ); emit DelegateVotesChanged(from, oldValue, newValue); } if (to != address(0)) { - (uint256 oldValue, uint256 newValue) = _delegateCheckpoints[to].push(_add, amount); + (uint256 oldValue, uint256 newValue) = _push( + _delegateCheckpoints[to], + _add, + SafeCast.toUint224(amount) + ); emit DelegateVotesChanged(to, oldValue, newValue); } } } - function _add(uint256 a, uint256 b) private pure returns (uint256) { + function _push( + Checkpoints.Trace224 storage store, + function(uint224, uint224) view returns (uint224) op, + uint224 delta + ) private returns (uint224, uint224) { + return store.push(SafeCast.toUint32(clock()), op(store.latest(), delta)); + } + + function _add(uint224 a, uint224 b) private pure returns (uint224) { return a + b; } - function _subtract(uint256 a, uint256 b) private pure returns (uint256) { + function _subtract(uint224 a, uint224 b) private pure returns (uint224) { return a - b; } diff --git a/contracts/interfaces/IERC5805.sol b/contracts/interfaces/IERC5805.sol new file mode 100644 index 000000000..2c2e5e345 --- /dev/null +++ b/contracts/interfaces/IERC5805.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (interfaces/IERC5805.sol) + +pragma solidity ^0.8.0; + +import "../governance/utils/IVotes.sol"; +import "./IERC6372.sol"; + +interface IERC5805 is IERC6372, IVotes {} diff --git a/contracts/interfaces/IERC6372.sol b/contracts/interfaces/IERC6372.sol new file mode 100644 index 000000000..e1a0bf8b0 --- /dev/null +++ b/contracts/interfaces/IERC6372.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (interfaces/IERC6372.sol) + +pragma solidity ^0.8.0; + +interface IERC6372 { + /** + * @dev Clock used for flagging checkpoints. Can be overridden to implement timestamp based checkpoints (and voting). + */ + function clock() external view returns (uint48); + + /** + * @dev Description of the clock + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() external view returns (string memory); +} diff --git a/contracts/mocks/VotesMock.sol b/contracts/mocks/VotesMock.sol index ece08b075..829504e3a 100644 --- a/contracts/mocks/VotesMock.sol +++ b/contracts/mocks/VotesMock.sol @@ -32,3 +32,14 @@ abstract contract VotesMock is Votes { _transferVotingUnits(owner, address(0), 1); } } + +abstract contract VotesTimestampMock is VotesMock { + function clock() public view override returns (uint48) { + return uint48(block.timestamp); + } + + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + return "mode=timestamp"; + } +} diff --git a/contracts/mocks/token/ERC20VotesLegacyMock.sol b/contracts/mocks/token/ERC20VotesLegacyMock.sol new file mode 100644 index 000000000..09a2675d9 --- /dev/null +++ b/contracts/mocks/token/ERC20VotesLegacyMock.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../../token/ERC20/extensions/ERC20Permit.sol"; +import "../../utils/math/Math.sol"; +import "../../governance/utils/IVotes.sol"; +import "../../utils/math/SafeCast.sol"; +import "../../utils/cryptography/ECDSA.sol"; + +/** + * @dev Copied from the master branch at commit 86de1e8b6c3fa6b4efa4a5435869d2521be0f5f5 + */ +abstract contract ERC20VotesLegacyMock is IVotes, ERC20Permit { + struct Checkpoint { + uint32 fromBlock; + uint224 votes; + } + + bytes32 private constant _DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + mapping(address => address) private _delegates; + mapping(address => Checkpoint[]) private _checkpoints; + Checkpoint[] private _totalSupplyCheckpoints; + + /** + * @dev Get the `pos`-th checkpoint for `account`. + */ + function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoint memory) { + return _checkpoints[account][pos]; + } + + /** + * @dev Get number of checkpoints for `account`. + */ + function numCheckpoints(address account) public view virtual returns (uint32) { + return SafeCast.toUint32(_checkpoints[account].length); + } + + /** + * @dev Get the address `account` is currently delegating to. + */ + function delegates(address account) public view virtual override returns (address) { + return _delegates[account]; + } + + /** + * @dev Gets the current votes balance for `account` + */ + function getVotes(address account) public view virtual override returns (uint256) { + uint256 pos = _checkpoints[account].length; + unchecked { + return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes; + } + } + + /** + * @dev Retrieve the number of votes for `account` at the end of `blockNumber`. + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { + require(blockNumber < block.number, "ERC20Votes: block not yet mined"); + return _checkpointsLookup(_checkpoints[account], blockNumber); + } + + /** + * @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. + * It is NOT the sum of all the delegated votes! + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastTotalSupply(uint256 blockNumber) public view virtual override returns (uint256) { + require(blockNumber < block.number, "ERC20Votes: block not yet mined"); + return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber); + } + + /** + * @dev Lookup a value in a list of (sorted) checkpoints. + */ + function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) { + // We run a binary search to look for the earliest checkpoint taken after `blockNumber`. + // + // Initially we check if the block is recent to narrow the search range. + // During the loop, the index of the wanted checkpoint remains in the range [low-1, high). + // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant. + // - If the middle checkpoint is after `blockNumber`, we look in [low, mid) + // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high) + // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not + // out of bounds (in which case we're looking too far in the past and the result is 0). + // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is + // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out + // the same. + uint256 length = ckpts.length; + + uint256 low = 0; + uint256 high = length; + + if (length > 5) { + uint256 mid = length - Math.sqrt(length); + if (_unsafeAccess(ckpts, mid).fromBlock > blockNumber) { + high = mid; + } else { + low = mid + 1; + } + } + + while (low < high) { + uint256 mid = Math.average(low, high); + if (_unsafeAccess(ckpts, mid).fromBlock > blockNumber) { + high = mid; + } else { + low = mid + 1; + } + } + + unchecked { + return high == 0 ? 0 : _unsafeAccess(ckpts, high - 1).votes; + } + } + + /** + * @dev Delegate votes from the sender to `delegatee`. + */ + function delegate(address delegatee) public virtual override { + _delegate(_msgSender(), delegatee); + } + + /** + * @dev Delegates votes from signer to `delegatee` + */ + function delegateBySig( + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + require(block.timestamp <= expiry, "ERC20Votes: signature expired"); + address signer = ECDSA.recover( + _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), + v, + r, + s + ); + require(nonce == _useNonce(signer), "ERC20Votes: invalid nonce"); + _delegate(signer, delegatee); + } + + /** + * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). + */ + function _maxSupply() internal view virtual returns (uint224) { + return type(uint224).max; + } + + /** + * @dev Snapshots the totalSupply after it has been increased. + */ + function _mint(address account, uint256 amount) internal virtual override { + super._mint(account, amount); + require(totalSupply() <= _maxSupply(), "ERC20Votes: total supply risks overflowing votes"); + + _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + } + + /** + * @dev Snapshots the totalSupply after it has been decreased. + */ + function _burn(address account, uint256 amount) internal virtual override { + super._burn(account, amount); + + _writeCheckpoint(_totalSupplyCheckpoints, _subtract, amount); + } + + /** + * @dev Move voting power when tokens are transferred. + * + * Emits a {IVotes-DelegateVotesChanged} event. + */ + function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual override { + super._afterTokenTransfer(from, to, amount); + + _moveVotingPower(delegates(from), delegates(to), amount); + } + + /** + * @dev Change delegation for `delegator` to `delegatee`. + * + * Emits events {IVotes-DelegateChanged} and {IVotes-DelegateVotesChanged}. + */ + function _delegate(address delegator, address delegatee) internal virtual { + address currentDelegate = delegates(delegator); + uint256 delegatorBalance = balanceOf(delegator); + _delegates[delegator] = delegatee; + + emit DelegateChanged(delegator, currentDelegate, delegatee); + + _moveVotingPower(currentDelegate, delegatee, delegatorBalance); + } + + function _moveVotingPower(address src, address dst, uint256 amount) private { + if (src != dst && amount > 0) { + if (src != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[src], _subtract, amount); + emit DelegateVotesChanged(src, oldWeight, newWeight); + } + + if (dst != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[dst], _add, amount); + emit DelegateVotesChanged(dst, oldWeight, newWeight); + } + } + } + + function _writeCheckpoint( + Checkpoint[] storage ckpts, + function(uint256, uint256) view returns (uint256) op, + uint256 delta + ) private returns (uint256 oldWeight, uint256 newWeight) { + uint256 pos = ckpts.length; + + unchecked { + Checkpoint memory oldCkpt = pos == 0 ? Checkpoint(0, 0) : _unsafeAccess(ckpts, pos - 1); + + oldWeight = oldCkpt.votes; + newWeight = op(oldWeight, delta); + + if (pos > 0 && oldCkpt.fromBlock == block.number) { + _unsafeAccess(ckpts, pos - 1).votes = SafeCast.toUint224(newWeight); + } else { + ckpts.push( + Checkpoint({fromBlock: SafeCast.toUint32(block.number), votes: SafeCast.toUint224(newWeight)}) + ); + } + } + } + + function _add(uint256 a, uint256 b) private pure returns (uint256) { + return a + b; + } + + function _subtract(uint256 a, uint256 b) private pure returns (uint256) { + return a - b; + } + + /** + * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds. + */ + function _unsafeAccess(Checkpoint[] storage ckpts, uint256 pos) private pure returns (Checkpoint storage result) { + assembly { + mstore(0, ckpts.slot) + result.slot := add(keccak256(0, 0x20), pos) + } + } +} diff --git a/contracts/mocks/token/VotesTimestamp.sol b/contracts/mocks/token/VotesTimestamp.sol new file mode 100644 index 000000000..179c500f4 --- /dev/null +++ b/contracts/mocks/token/VotesTimestamp.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../../token/ERC20/extensions/ERC20Votes.sol"; +import "../../token/ERC20/extensions/ERC20VotesComp.sol"; +import "../../token/ERC721/extensions/ERC721Votes.sol"; + +abstract contract ERC20VotesTimestampMock is ERC20Votes { + function clock() public view virtual override returns (uint48) { + return SafeCast.toUint48(block.timestamp); + } + + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + return "mode=timestamp"; + } +} + +abstract contract ERC20VotesCompTimestampMock is ERC20VotesComp { + function clock() public view virtual override returns (uint48) { + return SafeCast.toUint48(block.timestamp); + } + + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + return "mode=timestamp"; + } +} + +abstract contract ERC721VotesTimestampMock is ERC721Votes { + function clock() public view virtual override returns (uint48) { + return SafeCast.toUint48(block.timestamp); + } + + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + return "mode=timestamp"; + } +} diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index bf1c2fd5d..273bb8c50 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.0; import "./ERC20Permit.sol"; +import "../../../interfaces/IERC5805.sol"; import "../../../utils/math/Math.sol"; -import "../../../governance/utils/IVotes.sol"; import "../../../utils/math/SafeCast.sol"; import "../../../utils/cryptography/ECDSA.sol"; @@ -24,7 +24,7 @@ import "../../../utils/cryptography/ECDSA.sol"; * * _Available since v4.2._ */ -abstract contract ERC20Votes is IVotes, ERC20Permit { +abstract contract ERC20Votes is ERC20Permit, IERC5805 { struct Checkpoint { uint32 fromBlock; uint224 votes; @@ -37,6 +37,23 @@ abstract contract ERC20Votes is IVotes, ERC20Permit { mapping(address => Checkpoint[]) private _checkpoints; Checkpoint[] private _totalSupplyCheckpoints; + /** + * @dev Clock used for flagging checkpoints. Can be overridden to implement timestamp based checkpoints (and voting). + */ + function clock() public view virtual override returns (uint48) { + return SafeCast.toUint48(block.number); + } + + /** + * @dev Description of the clock + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + // Check that the clock was not modified + require(clock() == block.number); + return "mode=blocknumber&from=default"; + } + /** * @dev Get the `pos`-th checkpoint for `account`. */ @@ -69,45 +86,45 @@ abstract contract ERC20Votes is IVotes, ERC20Permit { } /** - * @dev Retrieve the number of votes for `account` at the end of `blockNumber`. + * @dev Retrieve the number of votes for `account` at the end of `timepoint`. * * Requirements: * - * - `blockNumber` must have been already mined + * - `timepoint` must be in the past */ - function getPastVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { - require(blockNumber < block.number, "ERC20Votes: block not yet mined"); - return _checkpointsLookup(_checkpoints[account], blockNumber); + function getPastVotes(address account, uint256 timepoint) public view virtual override returns (uint256) { + require(timepoint < clock(), "ERC20Votes: future lookup"); + return _checkpointsLookup(_checkpoints[account], timepoint); } /** - * @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. + * @dev Retrieve the `totalSupply` at the end of `timepoint`. Note, this value is the sum of all balances. * It is NOT the sum of all the delegated votes! * * Requirements: * - * - `blockNumber` must have been already mined + * - `timepoint` must be in the past */ - function getPastTotalSupply(uint256 blockNumber) public view virtual override returns (uint256) { - require(blockNumber < block.number, "ERC20Votes: block not yet mined"); - return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber); + function getPastTotalSupply(uint256 timepoint) public view virtual override returns (uint256) { + require(timepoint < clock(), "ERC20Votes: future lookup"); + return _checkpointsLookup(_totalSupplyCheckpoints, timepoint); } /** * @dev Lookup a value in a list of (sorted) checkpoints. */ - function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) { - // We run a binary search to look for the earliest checkpoint taken after `blockNumber`. + function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 timepoint) private view returns (uint256) { + // We run a binary search to look for the earliest checkpoint taken after `timepoint`. // // Initially we check if the block is recent to narrow the search range. // During the loop, the index of the wanted checkpoint remains in the range [low-1, high). // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant. - // - If the middle checkpoint is after `blockNumber`, we look in [low, mid) - // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high) + // - If the middle checkpoint is after `timepoint`, we look in [low, mid) + // - If the middle checkpoint is before or equal to `timepoint`, we look in [mid+1, high) // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not // out of bounds (in which case we're looking too far in the past and the result is 0). - // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is - // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out + // Note that if the latest checkpoint available is exactly for `timepoint`, we end up with an index that is + // past the end of the array, so we technically don't find a checkpoint after `timepoint`, but it works out // the same. uint256 length = ckpts.length; @@ -116,7 +133,7 @@ abstract contract ERC20Votes is IVotes, ERC20Permit { if (length > 5) { uint256 mid = length - Math.sqrt(length); - if (_unsafeAccess(ckpts, mid).fromBlock > blockNumber) { + if (_unsafeAccess(ckpts, mid).fromBlock > timepoint) { high = mid; } else { low = mid + 1; @@ -125,7 +142,7 @@ abstract contract ERC20Votes is IVotes, ERC20Permit { while (low < high) { uint256 mid = Math.average(low, high); - if (_unsafeAccess(ckpts, mid).fromBlock > blockNumber) { + if (_unsafeAccess(ckpts, mid).fromBlock > timepoint) { high = mid; } else { low = mid + 1; @@ -245,12 +262,10 @@ abstract contract ERC20Votes is IVotes, ERC20Permit { oldWeight = oldCkpt.votes; newWeight = op(oldWeight, delta); - if (pos > 0 && oldCkpt.fromBlock == block.number) { + if (pos > 0 && oldCkpt.fromBlock == clock()) { _unsafeAccess(ckpts, pos - 1).votes = SafeCast.toUint224(newWeight); } else { - ckpts.push( - Checkpoint({fromBlock: SafeCast.toUint32(block.number), votes: SafeCast.toUint224(newWeight)}) - ); + ckpts.push(Checkpoint({fromBlock: SafeCast.toUint32(clock()), votes: SafeCast.toUint224(newWeight)})); } } } diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index 0e0a136a0..025b13d0a 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -242,6 +242,31 @@ library Checkpoints { return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; } + /** + * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key. + * + * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys). + */ + function upperLookupRecent(Trace224 storage self, uint32 key) internal view returns (uint224) { + uint256 len = self._checkpoints.length; + + uint256 low = 0; + uint256 high = len; + + if (len > 5) { + uint256 mid = len - Math.sqrt(len); + if (key < _unsafeAccess(self._checkpoints, mid)._key) { + high = mid; + } else { + low = mid + 1; + } + } + + uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high); + + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + /** * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. */ @@ -393,6 +418,31 @@ library Checkpoints { return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; } + /** + * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key. + * + * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys). + */ + function upperLookupRecent(Trace160 storage self, uint96 key) internal view returns (uint160) { + uint256 len = self._checkpoints.length; + + uint256 low = 0; + uint256 high = len; + + if (len > 5) { + uint256 mid = len - Math.sqrt(len); + if (key < _unsafeAccess(self._checkpoints, mid)._key) { + high = mid; + } else { + low = mid + 1; + } + } + + uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high); + + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + /** * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. */ diff --git a/scripts/checks/compare-layout.js b/scripts/checks/compare-layout.js index ff8cb712e..7b7df9928 100644 --- a/scripts/checks/compare-layout.js +++ b/scripts/checks/compare-layout.js @@ -10,7 +10,6 @@ for (const name in oldLayout) { if (name in newLayout) { const report = getStorageUpgradeReport(oldLayout[name], newLayout[name], {}); if (!report.ok) { - console.log(`ERROR: Storage incompatibility in ${name}`); console.log(report.explain()); process.exitCode = 1; } diff --git a/scripts/checks/compareGasReports.js b/scripts/checks/compareGasReports.js index ca99dea0d..160c8cc52 100755 --- a/scripts/checks/compareGasReports.js +++ b/scripts/checks/compareGasReports.js @@ -10,6 +10,14 @@ const { argv } = require('yargs') choices: ['shell', 'markdown'], default: 'shell', }, + hideEqual: { + type: 'boolean', + default: true, + }, + strictTesting: { + type: 'boolean', + default: false, + }, }); // Deduce base tx cost from the percentage denominator @@ -40,7 +48,7 @@ class Report { } // Compare two reports - static compare(update, ref, opts = { hideEqual: true }) { + static compare(update, ref, opts = { hideEqual: true, strictTesting: false }) { if (JSON.stringify(update.config.metadata) !== JSON.stringify(ref.config.metadata)) { throw new Error('Reports produced with non matching metadata'); } @@ -70,7 +78,9 @@ class Report { const methods = Object.keys(update.info.methods) .filter(key => ref.info.methods[key]) .filter(key => update.info.methods[key].numberOfCalls > 0) - .filter(key => update.info.methods[key].numberOfCalls === ref.info.methods[key].numberOfCalls) + .filter( + key => !opts.strictTesting || update.info.methods[key].numberOfCalls === ref.info.methods[key].numberOfCalls, + ) .map(key => ({ contract: ref.info.methods[key].contract, method: ref.info.methods[key].fnSig, @@ -220,7 +230,7 @@ function formatCmpMarkdown(rows) { } // MAIN -const report = Report.compare(Report.load(argv._[0]), Report.load(argv._[1])); +const report = Report.compare(Report.load(argv._[0]), Report.load(argv._[1]), argv); switch (argv.style) { case 'markdown': diff --git a/scripts/generate/templates/Checkpoints.js b/scripts/generate/templates/Checkpoints.js index afac25991..e51e8b8d1 100644 --- a/scripts/generate/templates/Checkpoints.js +++ b/scripts/generate/templates/Checkpoints.js @@ -1,7 +1,28 @@ const format = require('../format-lines'); +// OPTIONS +const defaultOpts = size => ({ + historyTypeName: `Trace${size}`, + checkpointTypeName: `Checkpoint${size}`, + checkpointFieldName: '_checkpoints', + keyTypeName: `uint${256 - size}`, + keyFieldName: '_key', + valueTypeName: `uint${size}`, + valueFieldName: '_value', +}); + const VALUE_SIZES = [224, 160]; +const OPTS = VALUE_SIZES.map(size => defaultOpts(size)); + +const LEGACY_OPTS = { + ...defaultOpts(224), + historyTypeName: 'History', + checkpointTypeName: 'Checkpoint', + keyFieldName: '_blockNumber', +}; + +// TEMPLATE const header = `\ pragma solidity ^0.8.0; @@ -62,6 +83,31 @@ function upperLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} k uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, 0, len); return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName}; } + +/** + * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key. + * + * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys). + */ +function upperLookupRecent(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) { + uint256 len = self.${opts.checkpointFieldName}.length; + + uint256 low = 0; + uint256 high = len; + + if (len > 5) { + uint256 mid = len - Math.sqrt(len); + if (key < _unsafeAccess(self.${opts.checkpointFieldName}, mid)._key) { + high = mid; + } else { + low = mid + 1; + } + } + + uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, low, high); + + return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName}; +} `; const legacyOperations = opts => `\ @@ -263,26 +309,6 @@ function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos) `; /* eslint-enable max-len */ -// OPTIONS -const defaultOpts = size => ({ - historyTypeName: `Trace${size}`, - checkpointTypeName: `Checkpoint${size}`, - checkpointFieldName: '_checkpoints', - keyTypeName: `uint${256 - size}`, - keyFieldName: '_key', - valueTypeName: `uint${size}`, - valueFieldName: '_value', -}); - -const OPTS = VALUE_SIZES.map(size => defaultOpts(size)); - -const LEGACY_OPTS = { - ...defaultOpts(224), - historyTypeName: 'History', - checkpointTypeName: 'Checkpoint', - keyFieldName: '_blockNumber', -}; - // GENERATE module.exports = format( header.trimEnd(), diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js index 201895934..1eabaccf0 100644 --- a/test/governance/Governor.test.js +++ b/test/governance/Governor.test.js @@ -1,4 +1,4 @@ -const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; @@ -6,676 +6,694 @@ const { fromRpcSig } = require('ethereumjs-util'); const Enums = require('../helpers/enums'); const { getDomain, domainType } = require('../helpers/eip712'); const { GovernorHelper } = require('../helpers/governance'); +const { clockFromReceipt } = require('../helpers/time'); const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); +const { shouldBehaveLikeEIP6372 } = require('./utils/EIP6372.behavior'); -const Token = artifacts.require('$ERC20Votes'); const Governor = artifacts.require('$GovernorMock'); const CallReceiver = artifacts.require('CallReceiverMock'); const ERC721 = artifacts.require('$ERC721'); const ERC1155 = artifacts.require('$ERC1155'); +const TOKENS = [ + { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, + { Token: artifacts.require('$ERC20VotesLegacyMock'), mode: 'blocknumber' }, +]; + contract('Governor', function (accounts) { const [owner, proposer, voter1, voter2, voter3, voter4] = accounts; - const empty = web3.utils.toChecksumAddress(web3.utils.randomHex(20)); const name = 'OZ-Governor'; const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); - const votingDelay = new BN(4); - const votingPeriod = new BN(16); + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); const value = web3.utils.toWei('1'); - beforeEach(async function () { - this.chainId = await web3.eth.getChainId(); - this.token = await Token.new(tokenName, tokenSymbol, tokenName); - this.mock = await Governor.new( - name, // name - votingDelay, // initialVotingDelay - votingPeriod, // initialVotingPeriod - 0, // initialProposalThreshold - this.token.address, // tokenAddress - 10, // quorumNumeratorValue - ); - this.receiver = await CallReceiver.new(); + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + this.chainId = await web3.eth.getChainId(); + this.token = await Token.new(tokenName, tokenSymbol, tokenName); + this.mock = await Governor.new( + name, // name + votingDelay, // initialVotingDelay + votingPeriod, // initialVotingPeriod + 0, // initialProposalThreshold + this.token.address, // tokenAddress + 10, // quorumNumeratorValue + ); + this.receiver = await CallReceiver.new(); - this.helper = new GovernorHelper(this.mock); + this.helper = new GovernorHelper(this.mock, mode); - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + await this.token.$_mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - value, - }, - ], - '', - ); - }); + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + value, + }, + ], + '', + ); + }); - shouldSupportInterfaces(['ERC165', 'ERC1155Receiver', 'Governor', 'GovernorWithParams']); + shouldSupportInterfaces(['ERC165', 'ERC1155Receiver', 'Governor', 'GovernorWithParams']); + shouldBehaveLikeEIP6372(mode); - it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=for,abstain'); - }); + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=for,abstain'); + }); - it('nominal workflow', async function () { - // Before - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value); - expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0'); + it('nominal workflow', async function () { + // Before + expect(await this.mock.$_proposalProposer(this.proposal.id)).to.be.equal(constants.ZERO_ADDRESS); + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0'); - // Run proposal - const txPropose = await this.helper.propose({ from: proposer }); + // Run proposal + const txPropose = await this.helper.propose({ from: proposer }); - expectEvent(txPropose, 'ProposalCreated', { - proposalId: this.proposal.id, - proposer, - targets: this.proposal.targets, - // values: this.proposal.values, - signatures: this.proposal.signatures, - calldatas: this.proposal.data, - startBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay), - endBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod), - description: this.proposal.description, - }); + expectEvent(txPropose, 'ProposalCreated', { + proposalId: this.proposal.id, + proposer, + targets: this.proposal.targets, + // values: this.proposal.values, + signatures: this.proposal.signatures, + calldatas: this.proposal.data, + voteStart: web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay), + voteEnd: web3.utils + .toBN(await clockFromReceipt[mode](txPropose.receipt)) + .add(votingDelay) + .add(votingPeriod), + description: this.proposal.description, + }); - await this.helper.waitForSnapshot(); + await this.helper.waitForSnapshot(); - expectEvent( - await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }), - 'VoteCast', - { - voter: voter1, - support: Enums.VoteType.For, - reason: 'This is nice', - weight: web3.utils.toWei('10'), - }, - ); + expectEvent( + await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }), + 'VoteCast', + { + voter: voter1, + support: Enums.VoteType.For, + reason: 'This is nice', + weight: web3.utils.toWei('10'), + }, + ); - expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', { - voter: voter2, - support: Enums.VoteType.For, - weight: web3.utils.toWei('7'), - }); + expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', { + voter: voter2, + support: Enums.VoteType.For, + weight: web3.utils.toWei('7'), + }); - expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', { - voter: voter3, - support: Enums.VoteType.Against, - weight: web3.utils.toWei('5'), - }); + expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', { + voter: voter3, + support: Enums.VoteType.Against, + weight: web3.utils.toWei('5'), + }); - expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', { - voter: voter4, - support: Enums.VoteType.Abstain, - weight: web3.utils.toWei('2'), - }); + expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', { + voter: voter4, + support: Enums.VoteType.Abstain, + weight: web3.utils.toWei('2'), + }); - await this.helper.waitForDeadline(); + await this.helper.waitForDeadline(); - const txExecute = await this.helper.execute(); + const txExecute = await this.helper.execute(); - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); + expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); - // After - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); - expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value); - }); + // After + expect(await this.mock.$_proposalProposer(this.proposal.id)).to.be.equal(proposer); + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value); + }); - it('vote with signature', async function () { - const voterBySig = Wallet.generate(); - const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString()); + it('vote with signature', async function () { + const voterBySig = Wallet.generate(); + const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString()); - const signature = (contract, message) => - getDomain(contract) - .then(domain => ({ - primaryType: 'Ballot', - types: { - EIP712Domain: domainType(domain), - Ballot: [ - { name: 'proposalId', type: 'uint256' }, - { name: 'support', type: 'uint8' }, + const signature = (contract, message) => + getDomain(contract) + .then(domain => ({ + primaryType: 'Ballot', + types: { + EIP712Domain: domainType(domain), + Ballot: [ + { name: 'proposalId', type: 'uint256' }, + { name: 'support', type: 'uint8' }, + ], + }, + domain, + message, + })) + .then(data => ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { data })) + .then(fromRpcSig); + + await this.token.delegate(voterBySigAddress, { from: voter1 }); + + // Run proposal + await this.helper.propose(); + await this.helper.waitForSnapshot(); + expectEvent(await this.helper.vote({ support: Enums.VoteType.For, signature }), 'VoteCast', { + voter: voterBySigAddress, + support: Enums.VoteType.For, + }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + // After + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voterBySigAddress)).to.be.equal(true); + }); + + it('send ethers', async function () { + const empty = web3.utils.toChecksumAddress(web3.utils.randomHex(20)); + + this.proposal = this.helper.setProposal( + [ + { + target: empty, + value, + }, + ], + '', + ); + + // Before + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value); + expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal('0'); + + // Run proposal + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + // After + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal(value); + }); + + describe('should revert', function () { + describe('on propose', function () { + it('if proposal already exists', async function () { + await this.helper.propose(); + await expectRevert(this.helper.propose(), 'Governor: proposal already exists'); + }); + }); + + describe('on vote', function () { + it('if proposal does not exist', async function () { + await expectRevert( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'Governor: unknown proposal id', + ); + }); + + it('if voting has not started', async function () { + await this.helper.propose(); + await expectRevert( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'Governor: vote not currently active', + ); + }); + + it('if support value is invalid', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await expectRevert( + this.helper.vote({ support: web3.utils.toBN('255') }), + 'GovernorVotingSimple: invalid value for enum VoteType', + ); + }); + + it('if vote was already casted', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await expectRevert( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'GovernorVotingSimple: vote already cast', + ); + }); + + it('if voting is over', async function () { + await this.helper.propose(); + await this.helper.waitForDeadline(); + await expectRevert( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'Governor: vote not currently active', + ); + }); + }); + + describe('on execute', function () { + it('if proposal does not exist', async function () { + await expectRevert(this.helper.execute(), 'Governor: unknown 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'); + }); + + 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'); + }); + + 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'); + }); + + it('if receiver revert without reason', async function () { + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + data: this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(), + }, + ], + '', + ); + + 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.execute(), 'Governor: call reverted without message'); + }); + + it('if receiver revert with reason', async function () { + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + data: this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI(), + }, + ], + '', + ); + + 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.execute(), 'CallReceiverMock: reverting'); + }); + + it('if proposal was already executed', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + }); + }); + + describe('state', function () { + it('Unset', async function () { + await expectRevert(this.mock.state(this.proposal.id), 'Governor: unknown proposal id'); + }); + + it('Pending & Active', async function () { + await this.helper.propose(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending); + await this.helper.waitForSnapshot(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending); + await this.helper.waitForSnapshot(+1); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + }); + + it('Defeated', async function () { + await this.helper.propose(); + await this.helper.waitForDeadline(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + await this.helper.waitForDeadline(+1); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated); + }); + + it('Succeeded', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + await this.helper.waitForDeadline(+1); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + }); + + it('Executed', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Executed); + }); + }); + + describe('cancel', function () { + describe('internal', function () { + it('before proposal', async function () { + await expectRevert(this.helper.cancel('internal'), 'Governor: unknown proposal id'); + }); + + it('after proposal', async function () { + await this.helper.propose(); + + await this.helper.cancel('internal'); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + + await this.helper.waitForSnapshot(); + await expectRevert( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'Governor: vote not currently active', + ); + }); + + it('after vote', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + + await this.helper.cancel('internal'); + 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'); + }); + + it('after deadline', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + 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'); + }); + + it('after execution', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + await expectRevert(this.helper.cancel('internal'), 'Governor: proposal not active'); + }); + }); + + describe('public', function () { + it('before proposal', async function () { + await expectRevert(this.helper.cancel('external'), 'Governor: unknown proposal id'); + }); + + it('after proposal', async function () { + await this.helper.propose(); + + await this.helper.cancel('external'); + }); + + 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'); + }); + + 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'); + }); + + it('after vote', async function () { + await this.helper.propose(); + 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'); + }); + + it('after deadline', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel'); + }); + + it('after execution', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel'); + }); + }); + }); + + describe('proposal length', function () { + it('empty', async function () { + this.helper.setProposal([], ''); + await expectRevert(this.helper.propose(), 'Governor: empty proposal'); + }); + + it('mismatch #1', async function () { + this.helper.setProposal( + { + targets: [], + values: [web3.utils.toWei('0')], + data: [this.receiver.contract.methods.mockFunction().encodeABI()], + }, + '', + ); + await expectRevert(this.helper.propose(), 'Governor: invalid proposal length'); + }); + + it('mismatch #2', async function () { + this.helper.setProposal( + { + targets: [this.receiver.address], + values: [], + data: [this.receiver.contract.methods.mockFunction().encodeABI()], + }, + '', + ); + await expectRevert(this.helper.propose(), 'Governor: invalid proposal length'); + }); + + it('mismatch #3', async function () { + this.helper.setProposal( + { + targets: [this.receiver.address], + values: [web3.utils.toWei('0')], + data: [], + }, + '', + ); + await expectRevert(this.helper.propose(), 'Governor: invalid proposal length'); + }); + }); + + describe('onlyGovernance updates', function () { + it('setVotingDelay is protected', async function () { + await expectRevert(this.mock.setVotingDelay('0'), 'Governor: onlyGovernance'); + }); + + it('setVotingPeriod is protected', async function () { + await expectRevert(this.mock.setVotingPeriod('32'), 'Governor: onlyGovernance'); + }); + + it('setProposalThreshold is protected', async function () { + await expectRevert(this.mock.setProposalThreshold('1000000000000000000'), 'Governor: onlyGovernance'); + }); + + it('can setVotingDelay through governance', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.setVotingDelay('0').encodeABI(), + }, ], - }, - domain, - message, - })) - .then(data => ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { data })) - .then(fromRpcSig); + '', + ); - await this.token.delegate(voterBySigAddress, { from: voter1 }); + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); - // Run proposal - await this.helper.propose(); - await this.helper.waitForSnapshot(); - expectEvent(await this.helper.vote({ support: Enums.VoteType.For, signature }), 'VoteCast', { - voter: voterBySigAddress, - support: Enums.VoteType.For, - }); - await this.helper.waitForDeadline(); - await this.helper.execute(); + expectEvent(await this.helper.execute(), 'VotingDelaySet', { oldVotingDelay: '4', newVotingDelay: '0' }); - // After - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voterBySigAddress)).to.be.equal(true); - }); + expect(await this.mock.votingDelay()).to.be.bignumber.equal('0'); + }); - it('send ethers', async function () { - this.proposal = this.helper.setProposal( - [ - { - target: empty, - value, - }, - ], - '', - ); + it('can setVotingPeriod through governance', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.setVotingPeriod('32').encodeABI(), + }, + ], + '', + ); - // Before - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value); - expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal('0'); + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); - // Run proposal - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); + expectEvent(await this.helper.execute(), 'VotingPeriodSet', { oldVotingPeriod: '16', newVotingPeriod: '32' }); - // After - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); - expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal(value); - }); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32'); + }); - describe('should revert', function () { - describe('on propose', function () { - it('if proposal already exists', async function () { - await this.helper.propose(); - await expectRevert(this.helper.propose(), 'Governor: proposal already exists'); + it('cannot setVotingPeriod to 0 through governance', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.setVotingPeriod('0').encodeABI(), + }, + ], + '', + ); + + 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.execute(), 'GovernorSettings: voting period too low'); + }); + + it('can setProposalThreshold to 0 through governance', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + expectEvent(await this.helper.execute(), 'ProposalThresholdSet', { + oldProposalThreshold: '0', + newProposalThreshold: '1000000000000000000', + }); + + expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000'); + }); + }); + + describe('safe receive', function () { + describe('ERC721', function () { + const name = 'Non Fungible Token'; + const symbol = 'NFT'; + const tokenId = web3.utils.toBN(1); + + beforeEach(async function () { + this.token = await ERC721.new(name, symbol); + await this.token.$_mint(owner, tokenId); + }); + + it('can receive an ERC721 safeTransfer', async function () { + await this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner }); + }); + }); + + describe('ERC1155', function () { + const uri = 'https://token-cdn-domain/{id}.json'; + const tokenIds = { + 1: web3.utils.toBN(1000), + 2: web3.utils.toBN(2000), + 3: web3.utils.toBN(3000), + }; + + beforeEach(async function () { + this.token = await ERC1155.new(uri); + await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); + }); + + it('can receive ERC1155 safeTransfer', async function () { + await this.token.safeTransferFrom( + owner, + this.mock.address, + ...Object.entries(tokenIds)[0], // id + amount + '0x', + { from: owner }, + ); + }); + + it('can receive ERC1155 safeBatchTransfer', async function () { + await this.token.safeBatchTransferFrom( + owner, + this.mock.address, + Object.keys(tokenIds), + Object.values(tokenIds), + '0x', + { from: owner }, + ); + }); + }); }); }); - - describe('on vote', function () { - it('if proposal does not exist', async function () { - await expectRevert( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: unknown proposal id', - ); - }); - - it('if voting has not started', async function () { - await this.helper.propose(); - await expectRevert( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: vote not currently active', - ); - }); - - it('if support value is invalid', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await expectRevert( - this.helper.vote({ support: new BN('255') }), - 'GovernorVotingSimple: invalid value for enum VoteType', - ); - }); - - it('if vote was already casted', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await expectRevert( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'GovernorVotingSimple: vote already cast', - ); - }); - - it('if voting is over', async function () { - await this.helper.propose(); - await this.helper.waitForDeadline(); - await expectRevert( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: vote not currently active', - ); - }); - }); - - describe('on execute', function () { - it('if proposal does not exist', async function () { - await expectRevert(this.helper.execute(), 'Governor: unknown 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'); - }); - - 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'); - }); - - 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'); - }); - - it('if receiver revert without reason', async function () { - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(), - }, - ], - '', - ); - - 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.execute(), 'Governor: call reverted without message'); - }); - - it('if receiver revert with reason', async function () { - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI(), - }, - ], - '', - ); - - 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.execute(), 'CallReceiverMock: reverting'); - }); - - it('if proposal was already executed', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - }); - }); - - describe('state', function () { - it('Unset', async function () { - await expectRevert(this.mock.state(this.proposal.id), 'Governor: unknown proposal id'); - }); - - it('Pending & Active', async function () { - await this.helper.propose(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending); - await this.helper.waitForSnapshot(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending); - await this.helper.waitForSnapshot(+1); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); - }); - - it('Defeated', async function () { - await this.helper.propose(); - await this.helper.waitForDeadline(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); - await this.helper.waitForDeadline(+1); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated); - }); - - it('Succeeded', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); - await this.helper.waitForDeadline(+1); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); - }); - - it('Executed', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Executed); - }); - }); - - describe('cancel', function () { - describe('internal', function () { - it('before proposal', async function () { - await expectRevert(this.helper.cancel('internal'), 'Governor: unknown proposal id'); - }); - - it('after proposal', async function () { - await this.helper.propose(); - - await this.helper.cancel('internal'); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - - await this.helper.waitForSnapshot(); - await expectRevert( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: vote not currently active', - ); - }); - - it('after vote', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - - await this.helper.cancel('internal'); - 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'); - }); - - it('after deadline', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - 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'); - }); - - it('after execution', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - - await expectRevert(this.helper.cancel('internal'), 'Governor: proposal not active'); - }); - }); - - describe('public', function () { - it('before proposal', async function () { - await expectRevert(this.helper.cancel('external'), 'Governor: unknown proposal id'); - }); - - it('after proposal', async function () { - await this.helper.propose(); - - await this.helper.cancel('external'); - }); - - 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'); - }); - - 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'); - }); - - it('after vote', async function () { - await this.helper.propose(); - 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'); - }); - - it('after deadline', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel'); - }); - - it('after execution', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - - await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel'); - }); - }); - }); - - describe('proposal length', function () { - it('empty', async function () { - this.helper.setProposal([], ''); - await expectRevert(this.helper.propose(), 'Governor: empty proposal'); - }); - - it('mismatch #1', async function () { - this.helper.setProposal( - { - targets: [], - values: [web3.utils.toWei('0')], - data: [this.receiver.contract.methods.mockFunction().encodeABI()], - }, - '', - ); - await expectRevert(this.helper.propose(), 'Governor: invalid proposal length'); - }); - - it('mismatch #2', async function () { - this.helper.setProposal( - { - targets: [this.receiver.address], - values: [], - data: [this.receiver.contract.methods.mockFunction().encodeABI()], - }, - '', - ); - await expectRevert(this.helper.propose(), 'Governor: invalid proposal length'); - }); - - it('mismatch #3', async function () { - this.helper.setProposal( - { - targets: [this.receiver.address], - values: [web3.utils.toWei('0')], - data: [], - }, - '', - ); - await expectRevert(this.helper.propose(), 'Governor: invalid proposal length'); - }); - }); - - describe('onlyGovernance updates', function () { - it('setVotingDelay is protected', async function () { - await expectRevert(this.mock.setVotingDelay('0'), 'Governor: onlyGovernance'); - }); - - it('setVotingPeriod is protected', async function () { - await expectRevert(this.mock.setVotingPeriod('32'), 'Governor: onlyGovernance'); - }); - - it('setProposalThreshold is protected', async function () { - await expectRevert(this.mock.setProposalThreshold('1000000000000000000'), 'Governor: onlyGovernance'); - }); - - it('can setVotingDelay through governance', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.setVotingDelay('0').encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - expectEvent(await this.helper.execute(), 'VotingDelaySet', { oldVotingDelay: '4', newVotingDelay: '0' }); - - expect(await this.mock.votingDelay()).to.be.bignumber.equal('0'); - }); - - it('can setVotingPeriod through governance', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.setVotingPeriod('32').encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - expectEvent(await this.helper.execute(), 'VotingPeriodSet', { oldVotingPeriod: '16', newVotingPeriod: '32' }); - - expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32'); - }); - - it('cannot setVotingPeriod to 0 through governance', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.setVotingPeriod('0').encodeABI(), - }, - ], - '', - ); - - 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.execute(), 'GovernorSettings: voting period too low'); - }); - - it('can setProposalThreshold to 0 through governance', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - expectEvent(await this.helper.execute(), 'ProposalThresholdSet', { - oldProposalThreshold: '0', - newProposalThreshold: '1000000000000000000', - }); - - expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000'); - }); - }); - - describe('safe receive', function () { - describe('ERC721', function () { - const name = 'Non Fungible Token'; - const symbol = 'NFT'; - const tokenId = new BN(1); - - beforeEach(async function () { - this.token = await ERC721.new(name, symbol); - await this.token.$_mint(owner, tokenId); - }); - - it('can receive an ERC721 safeTransfer', async function () { - await this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner }); - }); - }); - - describe('ERC1155', function () { - const uri = 'https://token-cdn-domain/{id}.json'; - const tokenIds = { - 1: new BN(1000), - 2: new BN(2000), - 3: new BN(3000), - }; - - beforeEach(async function () { - this.token = await ERC1155.new(uri); - await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); - }); - - it('can receive ERC1155 safeTransfer', async function () { - await this.token.safeTransferFrom( - owner, - this.mock.address, - ...Object.entries(tokenIds)[0], // id + amount - '0x', - { from: owner }, - ); - }); - - it('can receive ERC1155 safeBatchTransfer', async function () { - await this.token.safeBatchTransferFrom( - owner, - this.mock.address, - Object.keys(tokenIds), - Object.values(tokenIds), - '0x', - { from: owner }, - ); - }); - }); - }); + } }); diff --git a/test/governance/compatibility/GovernorCompatibilityBravo.test.js b/test/governance/compatibility/GovernorCompatibilityBravo.test.js index 3666c4548..9e551949f 100644 --- a/test/governance/compatibility/GovernorCompatibilityBravo.test.js +++ b/test/governance/compatibility/GovernorCompatibilityBravo.test.js @@ -1,14 +1,16 @@ -const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent, expectRevert } = 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 Token = artifacts.require('$ERC20VotesComp'); const Timelock = artifacts.require('CompTimelock'); const Governor = artifacts.require('$GovernorCompatibilityBravoMock'); const CallReceiver = artifacts.require('CallReceiverMock'); +const { shouldBehaveLikeEIP6372 } = require('../utils/EIP6372.behavior'); + function makeContractAddress(creator, nonce) { return web3.utils.toChecksumAddress( web3.utils @@ -18,6 +20,11 @@ function makeContractAddress(creator, nonce) { ); } +const TOKENS = [ + { Token: artifacts.require('$ERC20VotesComp'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesCompTimestampMock'), mode: 'timestamp' }, +]; + contract('GovernorCompatibilityBravo', function (accounts) { const [owner, proposer, voter1, voter2, voter3, voter4, other] = accounts; @@ -26,218 +33,236 @@ contract('GovernorCompatibilityBravo', function (accounts) { const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); - const votingDelay = new BN(4); - const votingPeriod = new BN(16); + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); const proposalThreshold = web3.utils.toWei('10'); const value = web3.utils.toWei('1'); - beforeEach(async function () { - const [deployer] = await web3.eth.getAccounts(); + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + const [deployer] = await web3.eth.getAccounts(); - this.token = await Token.new(tokenName, tokenSymbol, tokenName); + this.token = await Token.new(tokenName, tokenSymbol, tokenName); - // Need to predict governance address to set it as timelock admin with a delayed transfer - const nonce = await web3.eth.getTransactionCount(deployer); - const predictGovernor = makeContractAddress(deployer, nonce + 1); + // Need to predict governance address to set it as timelock admin with a delayed transfer + const nonce = await web3.eth.getTransactionCount(deployer); + const predictGovernor = makeContractAddress(deployer, nonce + 1); - this.timelock = await Timelock.new(predictGovernor, 2 * 86400); - this.mock = await Governor.new( - name, - votingDelay, - votingPeriod, - proposalThreshold, - this.timelock.address, - this.token.address, - ); - this.receiver = await CallReceiver.new(); + this.timelock = await Timelock.new(predictGovernor, 2 * 86400); + this.mock = await Governor.new( + name, + votingDelay, + votingPeriod, + proposalThreshold, + this.timelock.address, + this.token.address, + ); + this.receiver = await CallReceiver.new(); - this.helper = new GovernorHelper(this.mock); + this.helper = new GovernorHelper(this.mock, mode); - await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); + 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.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 }); - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - signature: 'mockFunction()', - }, - ], - '', - ); - }); - - it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - expect(await this.mock.quorumVotes()).to.be.bignumber.equal('0'); - expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=bravo'); - }); - - it('nominal workflow', async function () { - // Before - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); - expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(value); - expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0'); - - // Run proposal - const txPropose = await this.helper.propose({ from: proposer }); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - // After - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); - expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal('0'); - expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value); - - const proposal = await this.mock.proposals(this.proposal.id); - expect(proposal.id).to.be.bignumber.equal(this.proposal.id); - expect(proposal.proposer).to.be.equal(proposer); - expect(proposal.eta).to.be.bignumber.equal(await this.mock.proposalEta(this.proposal.id)); - expect(proposal.startBlock).to.be.bignumber.equal(await this.mock.proposalSnapshot(this.proposal.id)); - expect(proposal.endBlock).to.be.bignumber.equal(await this.mock.proposalDeadline(this.proposal.id)); - expect(proposal.canceled).to.be.equal(false); - expect(proposal.executed).to.be.equal(true); - - const action = await this.mock.getActions(this.proposal.id); - expect(action.targets).to.be.deep.equal(this.proposal.targets); - // expect(action.values).to.be.deep.equal(this.proposal.values); - expect(action.signatures).to.be.deep.equal(this.proposal.signatures); - expect(action.calldatas).to.be.deep.equal(this.proposal.data); - - const voteReceipt1 = await this.mock.getReceipt(this.proposal.id, voter1); - expect(voteReceipt1.hasVoted).to.be.equal(true); - expect(voteReceipt1.support).to.be.bignumber.equal(Enums.VoteType.For); - expect(voteReceipt1.votes).to.be.bignumber.equal(web3.utils.toWei('10')); - - const voteReceipt2 = await this.mock.getReceipt(this.proposal.id, voter2); - expect(voteReceipt2.hasVoted).to.be.equal(true); - expect(voteReceipt2.support).to.be.bignumber.equal(Enums.VoteType.For); - expect(voteReceipt2.votes).to.be.bignumber.equal(web3.utils.toWei('7')); - - const voteReceipt3 = await this.mock.getReceipt(this.proposal.id, voter3); - expect(voteReceipt3.hasVoted).to.be.equal(true); - expect(voteReceipt3.support).to.be.bignumber.equal(Enums.VoteType.Against); - expect(voteReceipt3.votes).to.be.bignumber.equal(web3.utils.toWei('5')); - - const voteReceipt4 = await this.mock.getReceipt(this.proposal.id, voter4); - expect(voteReceipt4.hasVoted).to.be.equal(true); - expect(voteReceipt4.support).to.be.bignumber.equal(Enums.VoteType.Abstain); - expect(voteReceipt4.votes).to.be.bignumber.equal(web3.utils.toWei('2')); - - expectEvent(txPropose, 'ProposalCreated', { - proposalId: this.proposal.id, - proposer, - targets: this.proposal.targets, - // values: this.proposal.values, - signatures: this.proposal.signatures.map(() => ''), // this event doesn't contain the proposal detail - calldatas: this.proposal.fulldata, - startBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay), - endBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod), - description: this.proposal.description, - }); - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); - }); - - it('double voting is forbidden', async function () { - await this.helper.propose({ from: proposer }); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await expectRevert( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'GovernorCompatibilityBravo: vote already cast', - ); - }); - - it('with function selector and arguments', async function () { - const target = this.receiver.address; - this.helper.setProposal( - [ - { target, data: this.receiver.contract.methods.mockFunction().encodeABI() }, - { target, data: this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI() }, - { target, signature: 'mockFunctionNonPayable()' }, - { - target, - signature: 'mockFunctionWithArgs(uint256,uint256)', - data: web3.eth.abi.encodeParameters(['uint256', 'uint256'], [18, 43]), - }, - ], - '', - ); - - await this.helper.propose({ from: proposer }); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalledWithArgs', { a: '17', b: '42' }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalledWithArgs', { a: '18', b: '43' }); - }); - - 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'); - }); - }); - - describe('on vote', function () { - it('if vote type is invalide', async function () { - await this.helper.propose({ from: proposer }); - await this.helper.waitForSnapshot(); - await expectRevert( - this.helper.vote({ support: 5 }, { from: voter1 }), - 'GovernorCompatibilityBravo: invalid vote type', + // default proposal + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + value, + signature: 'mockFunction()', + }, + ], + '', ); }); - }); - }); - describe('cancel', function () { - it('proposer can cancel', async function () { - await this.helper.propose({ from: proposer }); - await this.helper.cancel('external', { from: proposer }); - }); + shouldBehaveLikeEIP6372(mode); - it('anyone can cancel if proposer drop below threshold', async function () { - await this.helper.propose({ from: proposer }); - await this.token.transfer(voter1, web3.utils.toWei('1'), { from: proposer }); - await this.helper.cancel('external'); - }); + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + expect(await this.mock.quorumVotes()).to.be.bignumber.equal('0'); + expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=bravo'); + }); - 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'); + it('nominal workflow', async function () { + // Before + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(value); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0'); + + // Run proposal + const txPropose = await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + // After + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value); + + const proposal = await this.mock.proposals(this.proposal.id); + expect(proposal.id).to.be.bignumber.equal(this.proposal.id); + expect(proposal.proposer).to.be.equal(proposer); + expect(proposal.eta).to.be.bignumber.equal(await this.mock.proposalEta(this.proposal.id)); + expect(proposal.startBlock).to.be.bignumber.equal(await this.mock.proposalSnapshot(this.proposal.id)); + expect(proposal.endBlock).to.be.bignumber.equal(await this.mock.proposalDeadline(this.proposal.id)); + expect(proposal.canceled).to.be.equal(false); + expect(proposal.executed).to.be.equal(true); + + const action = await this.mock.getActions(this.proposal.id); + expect(action.targets).to.be.deep.equal(this.proposal.targets); + // expect(action.values).to.be.deep.equal(this.proposal.values); + expect(action.signatures).to.be.deep.equal(this.proposal.signatures); + expect(action.calldatas).to.be.deep.equal(this.proposal.data); + + const voteReceipt1 = await this.mock.getReceipt(this.proposal.id, voter1); + expect(voteReceipt1.hasVoted).to.be.equal(true); + expect(voteReceipt1.support).to.be.bignumber.equal(Enums.VoteType.For); + expect(voteReceipt1.votes).to.be.bignumber.equal(web3.utils.toWei('10')); + + const voteReceipt2 = await this.mock.getReceipt(this.proposal.id, voter2); + expect(voteReceipt2.hasVoted).to.be.equal(true); + expect(voteReceipt2.support).to.be.bignumber.equal(Enums.VoteType.For); + expect(voteReceipt2.votes).to.be.bignumber.equal(web3.utils.toWei('7')); + + const voteReceipt3 = await this.mock.getReceipt(this.proposal.id, voter3); + expect(voteReceipt3.hasVoted).to.be.equal(true); + expect(voteReceipt3.support).to.be.bignumber.equal(Enums.VoteType.Against); + expect(voteReceipt3.votes).to.be.bignumber.equal(web3.utils.toWei('5')); + + const voteReceipt4 = await this.mock.getReceipt(this.proposal.id, voter4); + expect(voteReceipt4.hasVoted).to.be.equal(true); + expect(voteReceipt4.support).to.be.bignumber.equal(Enums.VoteType.Abstain); + expect(voteReceipt4.votes).to.be.bignumber.equal(web3.utils.toWei('2')); + + expectEvent(txPropose, 'ProposalCreated', { + proposalId: this.proposal.id, + proposer, + targets: this.proposal.targets, + // values: this.proposal.values, + signatures: this.proposal.signatures.map(() => ''), // this event doesn't contain the proposal detail + calldatas: this.proposal.fulldata, + voteStart: web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay), + voteEnd: web3.utils + .toBN(await clockFromReceipt[mode](txPropose.receipt)) + .add(votingDelay) + .add(votingPeriod), + description: this.proposal.description, + }); + expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + }); + + it('double voting is forbidden', async function () { + await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await expectRevert( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'GovernorCompatibilityBravo: vote already cast', + ); + }); + + it('with function selector and arguments', async function () { + const target = this.receiver.address; + this.helper.setProposal( + [ + { target, data: this.receiver.contract.methods.mockFunction().encodeABI() }, + { target, data: this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI() }, + { target, signature: 'mockFunctionNonPayable()' }, + { + target, + signature: 'mockFunctionWithArgs(uint256,uint256)', + data: web3.eth.abi.encodeParameters(['uint256', 'uint256'], [18, 43]), + }, + ], + '', + ); + + await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalledWithArgs', { + a: '17', + b: '42', + }); + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalledWithArgs', { + a: '18', + b: '43', + }); + }); + + 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', + ); + }); + }); + + describe('on vote', function () { + it('if vote type is invalide', async function () { + await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await expectRevert( + this.helper.vote({ support: 5 }, { from: voter1 }), + 'GovernorCompatibilityBravo: invalid vote type', + ); + }); + }); + }); + + describe('cancel', function () { + it('proposer can cancel', async function () { + await this.helper.propose({ from: proposer }); + await this.helper.cancel('external', { from: proposer }); + }); + + it('anyone can cancel if proposer drop below threshold', async function () { + await this.helper.propose({ from: proposer }); + await this.token.transfer(voter1, web3.utils.toWei('1'), { from: proposer }); + await this.helper.cancel('external'); + }); + + 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'); + }); + }); }); - }); + } }); diff --git a/test/governance/extensions/GovernorComp.test.js b/test/governance/extensions/GovernorComp.test.js index 8d48b4ddc..bfcd0af38 100644 --- a/test/governance/extensions/GovernorComp.test.js +++ b/test/governance/extensions/GovernorComp.test.js @@ -1,12 +1,15 @@ -const { BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const Enums = require('../../helpers/enums'); const { GovernorHelper } = require('../../helpers/governance'); -const Token = artifacts.require('$ERC20VotesComp'); const Governor = artifacts.require('$GovernorCompMock'); const CallReceiver = artifacts.require('CallReceiverMock'); +const TOKENS = [ + { Token: artifacts.require('$ERC20VotesComp'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesCompTimestampMock'), mode: 'timestamp' }, +]; + contract('GovernorComp', function (accounts) { const [owner, voter1, voter2, voter3, voter4] = accounts; @@ -15,67 +18,71 @@ contract('GovernorComp', function (accounts) { const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); - const votingDelay = new BN(4); - const votingPeriod = new BN(16); + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); const value = web3.utils.toWei('1'); - beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol, tokenName); - this.mock = await Governor.new(name, this.token.address); - this.receiver = await CallReceiver.new(); + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol, tokenName); + this.mock = await Governor.new(name, this.token.address); + this.receiver = await CallReceiver.new(); - this.helper = new GovernorHelper(this.mock); + this.helper = new GovernorHelper(this.mock, mode); - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + await this.token.$_mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - }, - ], - '', - ); - }); + // default proposal + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], + '', + ); + }); - it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - }); + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + }); - it('voting with comp token', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); + it('voting with comp token', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); - await this.mock.proposalVotes(this.proposal.id).then(results => { - expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17')); - expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5')); - expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2')); + await this.mock.proposalVotes(this.proposal.id).then(results => { + expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17')); + expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5')); + expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2')); + }); + }); }); - }); + } }); diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index b0babbd37..c1f424c29 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -1,12 +1,16 @@ -const { BN, expectEvent } = 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 Token = artifacts.require('$ERC721Votes'); const Governor = artifacts.require('$GovernorVoteMocks'); const CallReceiver = artifacts.require('CallReceiverMock'); +const TOKENS = [ + { Token: artifacts.require('$ERC721Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC721VotesTimestampMock'), mode: 'timestamp' }, +]; + contract('GovernorERC721', function (accounts) { const [owner, voter1, voter2, voter3, voter4] = accounts; @@ -14,94 +18,98 @@ contract('GovernorERC721', function (accounts) { // const version = '1'; const tokenName = 'MockNFToken'; const tokenSymbol = 'MTKN'; - const NFT0 = new BN(0); - const NFT1 = new BN(1); - const NFT2 = new BN(2); - const NFT3 = new BN(3); - const NFT4 = new BN(4); - const votingDelay = new BN(4); - const votingPeriod = new BN(16); + const NFT0 = web3.utils.toBN(0); + const NFT1 = web3.utils.toBN(1); + const NFT2 = web3.utils.toBN(2); + const NFT3 = web3.utils.toBN(3); + const NFT4 = web3.utils.toBN(4); + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); const value = web3.utils.toWei('1'); - beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol, tokenName, '1'); - this.mock = await Governor.new(name, this.token.address); - this.receiver = await CallReceiver.new(); + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol, tokenName, '1'); + this.mock = await Governor.new(name, this.token.address); + this.receiver = await CallReceiver.new(); - this.helper = new GovernorHelper(this.mock); + this.helper = new GovernorHelper(this.mock, mode); - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); - await Promise.all([NFT0, NFT1, NFT2, NFT3, NFT4].map(tokenId => this.token.$_mint(owner, tokenId))); - await this.helper.delegate({ token: this.token, to: voter1, tokenId: NFT0 }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT1 }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT2 }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, tokenId: NFT3 }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, tokenId: NFT4 }, { from: owner }); + await Promise.all([NFT0, NFT1, NFT2, NFT3, NFT4].map(tokenId => this.token.$_mint(owner, tokenId))); + await this.helper.delegate({ token: this.token, to: voter1, tokenId: NFT0 }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT1 }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT2 }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, tokenId: NFT3 }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, tokenId: NFT4 }, { from: owner }); - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - }, - ], - '', - ); - }); + // default proposal + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], + '', + ); + }); - it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - }); + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + }); - it('voting with ERC721 token', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); + it('voting with ERC721 token', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); - expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), 'VoteCast', { - voter: voter1, - support: Enums.VoteType.For, - weight: '1', + expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), 'VoteCast', { + voter: voter1, + support: Enums.VoteType.For, + weight: '1', + }); + + expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', { + voter: voter2, + support: Enums.VoteType.For, + weight: '2', + }); + + expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', { + voter: voter3, + support: Enums.VoteType.Against, + weight: '1', + }); + + expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', { + voter: voter4, + support: Enums.VoteType.Abstain, + weight: '1', + }); + + await this.helper.waitForDeadline(); + await this.helper.execute(); + + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); + + await this.mock.proposalVotes(this.proposal.id).then(results => { + expect(results.forVotes).to.be.bignumber.equal('3'); + expect(results.againstVotes).to.be.bignumber.equal('1'); + expect(results.abstainVotes).to.be.bignumber.equal('1'); + }); + }); }); - - expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', { - voter: voter2, - support: Enums.VoteType.For, - weight: '2', - }); - - expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', { - voter: voter3, - support: Enums.VoteType.Against, - weight: '1', - }); - - expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', { - voter: voter4, - support: Enums.VoteType.Abstain, - weight: '1', - }); - - await this.helper.waitForDeadline(); - await this.helper.execute(); - - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); - - await this.mock.proposalVotes(this.proposal.id).then(results => { - expect(results.forVotes).to.be.bignumber.equal('3'); - expect(results.againstVotes).to.be.bignumber.equal('1'); - expect(results.abstainVotes).to.be.bignumber.equal('1'); - }); - }); + } }); diff --git a/test/governance/extensions/GovernorPreventLateQuorum.test.js b/test/governance/extensions/GovernorPreventLateQuorum.test.js index ffec8ace6..f81bd5286 100644 --- a/test/governance/extensions/GovernorPreventLateQuorum.test.js +++ b/test/governance/extensions/GovernorPreventLateQuorum.test.js @@ -1,12 +1,17 @@ -const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const Enums = require('../../helpers/enums'); const { GovernorHelper } = require('../../helpers/governance'); +const { clockFromReceipt } = require('../../helpers/time'); -const Token = artifacts.require('$ERC20VotesComp'); const Governor = artifacts.require('$GovernorPreventLateQuorumMock'); const CallReceiver = artifacts.require('CallReceiverMock'); +const TOKENS = [ + { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, +]; + contract('GovernorPreventLateQuorum', function (accounts) { const [owner, proposer, voter1, voter2, voter3, voter4] = accounts; @@ -15,158 +20,170 @@ contract('GovernorPreventLateQuorum', function (accounts) { const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); - const votingDelay = new BN(4); - const votingPeriod = new BN(16); - const lateQuorumVoteExtension = new BN(8); + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); + const lateQuorumVoteExtension = web3.utils.toBN(8); const quorum = web3.utils.toWei('1'); const value = web3.utils.toWei('1'); - beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol, tokenName); - this.mock = await Governor.new( - name, - votingDelay, - votingPeriod, - 0, - this.token.address, - lateQuorumVoteExtension, - quorum, - ); - this.receiver = await CallReceiver.new(); + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol, tokenName); + this.mock = await Governor.new( + name, + votingDelay, + votingPeriod, + 0, + this.token.address, + lateQuorumVoteExtension, + quorum, + ); + this.receiver = await CallReceiver.new(); - this.helper = new GovernorHelper(this.mock); + this.helper = new GovernorHelper(this.mock, mode); - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + await this.token.$_mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - }, - ], - '', - ); - }); - - it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal(quorum); - expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal(lateQuorumVoteExtension); - }); - - it('nominal workflow unaffected', async function () { - const txPropose = await this.helper.propose({ from: proposer }); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); - - await this.mock.proposalVotes(this.proposal.id).then(results => { - expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17')); - expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5')); - expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2')); - }); - - const startBlock = new BN(txPropose.receipt.blockNumber).add(votingDelay); - const endBlock = new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod); - expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock); - expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(endBlock); - - expectEvent(txPropose, 'ProposalCreated', { - proposalId: this.proposal.id, - proposer, - targets: this.proposal.targets, - // values: this.proposal.values.map(value => new BN(value)), - signatures: this.proposal.signatures, - calldatas: this.proposal.data, - startBlock, - endBlock, - description: this.proposal.description, - }); - }); - - it('Delay is extended to prevent last minute take-over', async function () { - const txPropose = await this.helper.propose({ from: proposer }); - - // compute original schedule - const startBlock = new BN(txPropose.receipt.blockNumber).add(votingDelay); - const endBlock = new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod); - expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock); - expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(endBlock); - - // wait for the last minute to vote - await this.helper.waitForDeadline(-1); - const txVote = await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - - // cannot execute yet - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); - - // compute new extended schedule - const extendedDeadline = new BN(txVote.receipt.blockNumber).add(lateQuorumVoteExtension); - expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock); - expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(extendedDeadline); - - // still possible to vote - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 }); - - await this.helper.waitForDeadline(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); - await this.helper.waitForDeadline(+1); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated); - - // check extension event - expectEvent(txVote, 'ProposalExtended', { proposalId: this.proposal.id, extendedDeadline }); - }); - - describe('onlyGovernance updates', function () { - it('setLateQuorumVoteExtension is protected', async function () { - await expectRevert(this.mock.setLateQuorumVoteExtension(0), 'Governor: onlyGovernance'); - }); - - it('can setLateQuorumVoteExtension through governance', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.setLateQuorumVoteExtension('0').encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - expectEvent(await this.helper.execute(), 'LateQuorumVoteExtensionSet', { - oldVoteExtension: lateQuorumVoteExtension, - newVoteExtension: '0', + // default proposal + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], + '', + ); }); - expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal('0'); + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.be.bignumber.equal(quorum); + expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal(lateQuorumVoteExtension); + }); + + it('nominal workflow unaffected', async function () { + const txPropose = await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); + + await this.mock.proposalVotes(this.proposal.id).then(results => { + expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17')); + expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5')); + expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2')); + }); + + const voteStart = web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay); + const voteEnd = web3.utils + .toBN(await clockFromReceipt[mode](txPropose.receipt)) + .add(votingDelay) + .add(votingPeriod); + expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(voteStart); + expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(voteEnd); + + expectEvent(txPropose, 'ProposalCreated', { + proposalId: this.proposal.id, + proposer, + targets: this.proposal.targets, + // values: this.proposal.values.map(value => web3.utils.toBN(value)), + signatures: this.proposal.signatures, + calldatas: this.proposal.data, + voteStart, + voteEnd, + description: this.proposal.description, + }); + }); + + it('Delay is extended to prevent last minute take-over', async function () { + const txPropose = await this.helper.propose({ from: proposer }); + + // compute original schedule + const startBlock = web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay); + const endBlock = web3.utils + .toBN(await clockFromReceipt[mode](txPropose.receipt)) + .add(votingDelay) + .add(votingPeriod); + expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock); + expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(endBlock); + + // wait for the last minute to vote + await this.helper.waitForDeadline(-1); + const txVote = await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + + // cannot execute yet + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + + // compute new extended schedule + const extendedDeadline = web3.utils + .toBN(await clockFromReceipt[mode](txVote.receipt)) + .add(lateQuorumVoteExtension); + expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock); + expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(extendedDeadline); + + // still possible to vote + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 }); + + await this.helper.waitForDeadline(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + await this.helper.waitForDeadline(+1); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated); + + // check extension event + expectEvent(txVote, 'ProposalExtended', { proposalId: this.proposal.id, extendedDeadline }); + }); + + describe('onlyGovernance updates', function () { + it('setLateQuorumVoteExtension is protected', async function () { + await expectRevert(this.mock.setLateQuorumVoteExtension(0), 'Governor: onlyGovernance'); + }); + + it('can setLateQuorumVoteExtension through governance', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.setLateQuorumVoteExtension('0').encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + expectEvent(await this.helper.execute(), 'LateQuorumVoteExtensionSet', { + oldVoteExtension: lateQuorumVoteExtension, + newVoteExtension: '0', + }); + + expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal('0'); + }); + }); }); - }); + } }); diff --git a/test/governance/extensions/GovernorTimelockCompound.test.js b/test/governance/extensions/GovernorTimelockCompound.test.js index 446d74999..23d0adbd8 100644 --- a/test/governance/extensions/GovernorTimelockCompound.test.js +++ b/test/governance/extensions/GovernorTimelockCompound.test.js @@ -1,4 +1,4 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const RLP = require('rlp'); const Enums = require('../../helpers/enums'); @@ -6,7 +6,6 @@ const { GovernorHelper } = require('../../helpers/governance'); const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); -const Token = artifacts.require('$ERC20Votes'); const Timelock = artifacts.require('CompTimelock'); const Governor = artifacts.require('$GovernorTimelockCompoundMock'); const CallReceiver = artifacts.require('CallReceiverMock'); @@ -20,6 +19,11 @@ function makeContractAddress(creator, nonce) { ); } +const TOKENS = [ + { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, +]; + contract('GovernorTimelockCompound', function (accounts) { const [owner, voter1, voter2, voter3, voter4, other] = accounts; @@ -28,306 +32,321 @@ contract('GovernorTimelockCompound', function (accounts) { const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); - const votingDelay = new BN(4); - const votingPeriod = new BN(16); + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); const value = web3.utils.toWei('1'); - beforeEach(async function () { - const [deployer] = await web3.eth.getAccounts(); - - this.token = await Token.new(tokenName, tokenSymbol, tokenName); - - // Need to predict governance address to set it as timelock admin with a delayed transfer - const nonce = await web3.eth.getTransactionCount(deployer); - const predictGovernor = makeContractAddress(deployer, nonce + 1); - - this.timelock = await Timelock.new(predictGovernor, 2 * 86400); - this.mock = await Governor.new(name, votingDelay, votingPeriod, 0, this.timelock.address, this.token.address, 0); - this.receiver = await CallReceiver.new(); - - this.helper = new GovernorHelper(this.mock); - - await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); - - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - }, - ], - '', - ); - }); - - shouldSupportInterfaces(['ERC165', 'Governor', 'GovernorWithParams', 'GovernorTimelock']); - - it("doesn't accept ether transfers", async function () { - await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 })); - }); - - it('post deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - - expect(await this.mock.timelock()).to.be.equal(this.timelock.address); - expect(await this.timelock.admin()).to.be.equal(this.mock.address); - }); - - it('nominal', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); - await this.helper.waitForDeadline(); - const txQueue = await this.helper.queue(); - const eta = await this.mock.proposalEta(this.proposal.id); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txQueue.tx, this.timelock, 'QueueTransaction', { eta }); - - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txExecute.tx, this.timelock, 'ExecuteTransaction', { eta }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); - }); - - describe('should revert', function () { - describe('on queue', function () { - it('if already queued', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); - }); - - it('if proposal contains duplicate calls', async function () { - const action = { - target: this.token.address, - data: this.token.contract.methods.approve(this.receiver.address, constants.MAX_UINT256).encodeABI(), - }; - this.helper.setProposal([action, action], ''); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await expectRevert(this.helper.queue(), 'GovernorTimelockCompound: identical proposal action already queued'); - await expectRevert(this.helper.execute(), 'GovernorTimelockCompound: proposal not yet queued'); - }); - }); - - describe('on execute', function () { - it('if not queued', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(+1); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); - - await expectRevert(this.helper.execute(), 'GovernorTimelockCompound: proposal not yet queued'); - }); - - it('if too early', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); - - await expectRevert( - this.helper.execute(), - "Timelock::executeTransaction: Transaction hasn't surpassed time lock", - ); - }); - - it('if too late', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(+30 * 86400); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Expired); - - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - - it('if already executed', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - await this.helper.execute(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - }); - }); - - describe('cancel', function () { - it('cancel before queue prevents scheduling', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); - }); - - it('cancel after queue prevents executing', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - - expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - }); - - describe('onlyGovernance', function () { - describe('relay', function () { + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { beforeEach(async function () { - await this.token.$_mint(this.mock.address, 1); - }); + const [deployer] = await web3.eth.getAccounts(); - it('is protected', async function () { - await expectRevert( - this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()), - 'Governor: onlyGovernance', + this.token = await Token.new(tokenName, tokenSymbol, tokenName); + + // Need to predict governance address to set it as timelock admin with a delayed transfer + const nonce = await web3.eth.getTransactionCount(deployer); + const predictGovernor = makeContractAddress(deployer, nonce + 1); + + this.timelock = await Timelock.new(predictGovernor, 2 * 86400); + this.mock = await Governor.new( + name, + votingDelay, + votingPeriod, + 0, + this.timelock.address, + this.token.address, + 0, ); - }); + this.receiver = await CallReceiver.new(); - it('can be executed through governance', async function () { - this.helper.setProposal( + this.helper = new GovernorHelper(this.mock, mode); + + await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); + + await this.token.$_mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + + // default proposal + this.proposal = this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods - .relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()) - .encodeABI(), + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), }, ], '', ); + }); - expect(await this.token.balanceOf(this.mock.address), 1); - expect(await this.token.balanceOf(other), 0); + shouldSupportInterfaces(['ERC165', 'Governor', 'GovernorWithParams', 'GovernorTimelock']); + it("doesn't accept ether transfers", async function () { + await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 })); + }); + + it('post deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + + expect(await this.mock.timelock()).to.be.equal(this.timelock.address); + expect(await this.timelock.admin()).to.be.equal(this.mock.address); + }); + + it('nominal', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); await this.helper.waitForDeadline(); - await this.helper.queue(); + const txQueue = await this.helper.queue(); + const eta = await this.mock.proposalEta(this.proposal.id); await this.helper.waitForEta(); const txExecute = await this.helper.execute(); - expect(await this.token.balanceOf(this.mock.address), 0); - expect(await this.token.balanceOf(other), 1); + expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); + await expectEvent.inTransaction(txQueue.tx, this.timelock, 'QueueTransaction', { eta }); - await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { - from: this.mock.address, - to: other, - value: '1', + expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); + await expectEvent.inTransaction(txExecute.tx, this.timelock, 'ExecuteTransaction', { eta }); + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + }); + + describe('should revert', function () { + describe('on queue', function () { + it('if already queued', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); + }); + + it('if proposal contains duplicate calls', async function () { + const action = { + target: this.token.address, + data: this.token.contract.methods.approve(this.receiver.address, constants.MAX_UINT256).encodeABI(), + }; + this.helper.setProposal([action, action], ''); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await expectRevert( + this.helper.queue(), + 'GovernorTimelockCompound: identical proposal action already queued', + ); + await expectRevert(this.helper.execute(), 'GovernorTimelockCompound: proposal not yet queued'); + }); + }); + + describe('on execute', function () { + it('if not queued', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(+1); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + + await expectRevert(this.helper.execute(), 'GovernorTimelockCompound: proposal not yet queued'); + }); + + it('if too early', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + + await expectRevert( + this.helper.execute(), + "Timelock::executeTransaction: Transaction hasn't surpassed time lock", + ); + }); + + it('if too late', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(+30 * 86400); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Expired); + + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + + it('if already executed', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + await this.helper.execute(); + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + }); + }); + + describe('cancel', function () { + it('cancel before queue prevents scheduling', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); + }); + + it('cancel after queue prevents executing', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + + expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + }); + + describe('onlyGovernance', function () { + describe('relay', function () { + beforeEach(async function () { + await this.token.$_mint(this.mock.address, 1); + }); + + it('is protected', async function () { + await expectRevert( + this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()), + 'Governor: onlyGovernance', + ); + }); + + it('can be executed through governance', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods + .relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()) + .encodeABI(), + }, + ], + '', + ); + + expect(await this.token.balanceOf(this.mock.address), 1); + expect(await this.token.balanceOf(other), 0); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + expect(await this.token.balanceOf(this.mock.address), 0); + expect(await this.token.balanceOf(other), 1); + + await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { + from: this.mock.address, + to: other, + value: '1', + }); + }); + }); + + describe('updateTimelock', function () { + beforeEach(async function () { + this.newTimelock = await Timelock.new(this.mock.address, 7 * 86400); + }); + + it('is protected', async function () { + await expectRevert(this.mock.updateTimelock(this.newTimelock.address), 'Governor: onlyGovernance'); + }); + + it('can be executed through governance to', async function () { + this.helper.setProposal( + [ + { + target: this.timelock.address, + data: this.timelock.contract.methods.setPendingAdmin(owner).encodeABI(), + }, + { + target: this.mock.address, + data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + expectEvent(txExecute, 'TimelockChange', { + oldTimelock: this.timelock.address, + newTimelock: this.newTimelock.address, + }); + + expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); + }); + }); + + it('can transfer timelock to new governor', async function () { + const newGovernor = await Governor.new(name, 8, 32, 0, this.timelock.address, this.token.address, 0); + this.helper.setProposal( + [ + { + target: this.timelock.address, + data: this.timelock.contract.methods.setPendingAdmin(newGovernor.address).encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + await expectEvent.inTransaction(txExecute.tx, this.timelock, 'NewPendingAdmin', { + newPendingAdmin: newGovernor.address, + }); + + await newGovernor.__acceptAdmin(); + expect(await this.timelock.admin()).to.be.bignumber.equal(newGovernor.address); }); }); }); - - describe('updateTimelock', function () { - beforeEach(async function () { - this.newTimelock = await Timelock.new(this.mock.address, 7 * 86400); - }); - - it('is protected', async function () { - await expectRevert(this.mock.updateTimelock(this.newTimelock.address), 'Governor: onlyGovernance'); - }); - - it('can be executed through governance to', async function () { - this.helper.setProposal( - [ - { - target: this.timelock.address, - data: this.timelock.contract.methods.setPendingAdmin(owner).encodeABI(), - }, - { - target: this.mock.address, - data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - expectEvent(txExecute, 'TimelockChange', { - oldTimelock: this.timelock.address, - newTimelock: this.newTimelock.address, - }); - - expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); - }); - }); - - it('can transfer timelock to new governor', async function () { - const newGovernor = await Governor.new(name, 8, 32, 0, this.timelock.address, this.token.address, 0); - this.helper.setProposal( - [ - { - target: this.timelock.address, - data: this.timelock.contract.methods.setPendingAdmin(newGovernor.address).encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - await expectEvent.inTransaction(txExecute.tx, this.timelock, 'NewPendingAdmin', { - newPendingAdmin: newGovernor.address, - }); - - await newGovernor.__acceptAdmin(); - expect(await this.timelock.admin()).to.be.bignumber.equal(newGovernor.address); - }); - }); + } }); diff --git a/test/governance/extensions/GovernorTimelockControl.test.js b/test/governance/extensions/GovernorTimelockControl.test.js index 2a55787cb..ef50dc708 100644 --- a/test/governance/extensions/GovernorTimelockControl.test.js +++ b/test/governance/extensions/GovernorTimelockControl.test.js @@ -1,15 +1,19 @@ -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const Enums = require('../../helpers/enums'); const { GovernorHelper } = require('../../helpers/governance'); const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); -const Token = artifacts.require('$ERC20Votes'); const Timelock = artifacts.require('TimelockController'); const Governor = artifacts.require('$GovernorTimelockControlMock'); const CallReceiver = artifacts.require('CallReceiverMock'); +const TOKENS = [ + { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, +]; + contract('GovernorTimelockControl', function (accounts) { const [owner, voter1, voter2, voter3, voter4, other] = accounts; @@ -23,372 +27,419 @@ contract('GovernorTimelockControl', function (accounts) { const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); - const votingDelay = new BN(4); - const votingPeriod = new BN(16); + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); const value = web3.utils.toWei('1'); - beforeEach(async function () { - const [deployer] = await web3.eth.getAccounts(); + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + const [deployer] = await web3.eth.getAccounts(); - this.token = await Token.new(tokenName, tokenSymbol, tokenName); - this.timelock = await Timelock.new(3600, [], [], deployer); - this.mock = await Governor.new(name, votingDelay, votingPeriod, 0, this.timelock.address, this.token.address, 0); - this.receiver = await CallReceiver.new(); + this.token = await Token.new(tokenName, tokenSymbol, tokenName); + this.timelock = await Timelock.new(3600, [], [], deployer); + this.mock = await Governor.new( + name, + votingDelay, + votingPeriod, + 0, + this.timelock.address, + this.token.address, + 0, + ); + this.receiver = await CallReceiver.new(); - this.helper = new GovernorHelper(this.mock); + this.helper = new GovernorHelper(this.mock, mode); - this.TIMELOCK_ADMIN_ROLE = await this.timelock.TIMELOCK_ADMIN_ROLE(); - this.PROPOSER_ROLE = await this.timelock.PROPOSER_ROLE(); - this.EXECUTOR_ROLE = await this.timelock.EXECUTOR_ROLE(); - this.CANCELLER_ROLE = await this.timelock.CANCELLER_ROLE(); + this.TIMELOCK_ADMIN_ROLE = await this.timelock.TIMELOCK_ADMIN_ROLE(); + this.PROPOSER_ROLE = await this.timelock.PROPOSER_ROLE(); + this.EXECUTOR_ROLE = await this.timelock.EXECUTOR_ROLE(); + this.CANCELLER_ROLE = await this.timelock.CANCELLER_ROLE(); - await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); + await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); - // normal setup: governor is proposer, everyone is executor, timelock is its own admin - await this.timelock.grantRole(PROPOSER_ROLE, this.mock.address); - await this.timelock.grantRole(PROPOSER_ROLE, owner); - await this.timelock.grantRole(CANCELLER_ROLE, this.mock.address); - await this.timelock.grantRole(CANCELLER_ROLE, owner); - await this.timelock.grantRole(EXECUTOR_ROLE, constants.ZERO_ADDRESS); - await this.timelock.revokeRole(TIMELOCK_ADMIN_ROLE, deployer); + // normal setup: governor is proposer, everyone is executor, timelock is its own admin + await this.timelock.grantRole(PROPOSER_ROLE, this.mock.address); + await this.timelock.grantRole(PROPOSER_ROLE, owner); + await this.timelock.grantRole(CANCELLER_ROLE, this.mock.address); + await this.timelock.grantRole(CANCELLER_ROLE, owner); + await this.timelock.grantRole(EXECUTOR_ROLE, constants.ZERO_ADDRESS); + await this.timelock.revokeRole(TIMELOCK_ADMIN_ROLE, deployer); - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + await this.token.$_mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - }, - ], - '', - ); - this.proposal.timelockid = await this.timelock.hashOperationBatch( - ...this.proposal.shortProposal.slice(0, 3), - '0x0', - this.proposal.shortProposal[3], - ); - }); - - shouldSupportInterfaces(['ERC165', 'Governor', 'GovernorWithParams', 'GovernorTimelock']); - - it("doesn't accept ether transfers", async function () { - await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 })); - }); - - it('post deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - - expect(await this.mock.timelock()).to.be.equal(this.timelock.address); - }); - - it('nominal', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); - await this.helper.waitForDeadline(); - const txQueue = await this.helper.queue(); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { id: this.proposal.timelockid }); - await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallSalt', { - id: this.proposal.timelockid, - }); - - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txExecute.tx, this.timelock, 'CallExecuted', { id: this.proposal.timelockid }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); - }); - - describe('should revert', function () { - describe('on queue', function () { - it('if already queued', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); - }); - }); - - describe('on execute', function () { - it('if not queued', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(+1); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); - - await expectRevert(this.helper.execute(), 'TimelockController: operation is not ready'); - }); - - it('if too early', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); - - await expectRevert(this.helper.execute(), 'TimelockController: operation is not ready'); - }); - - it('if already executed', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - await this.helper.execute(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - - it('if already executed by another proposer', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - - await this.timelock.executeBatch( + // default proposal + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], + '', + ); + this.proposal.timelockid = await this.timelock.hashOperationBatch( ...this.proposal.shortProposal.slice(0, 3), '0x0', this.proposal.shortProposal[3], ); - - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - }); - }); - - describe('cancel', function () { - it('cancel before queue prevents scheduling', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); - }); - - it('cancel after queue prevents executing', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - - expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - - it('cancel on timelock is reflected on governor', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); - - expectEvent(await this.timelock.cancel(this.proposal.timelockid, { from: owner }), 'Cancelled', { - id: this.proposal.timelockid, }); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - }); - }); + shouldSupportInterfaces(['ERC165', 'Governor', 'GovernorWithParams', 'GovernorTimelock']); - describe('onlyGovernance', function () { - describe('relay', function () { - beforeEach(async function () { - await this.token.$_mint(this.mock.address, 1); + it("doesn't accept ether transfers", async function () { + await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 })); }); - it('is protected', async function () { - await expectRevert( - this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()), - 'Governor: onlyGovernance', - ); + it('post deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + + expect(await this.mock.timelock()).to.be.equal(this.timelock.address); }); - it('can be executed through governance', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods - .relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()) - .encodeABI(), - }, - ], - '', - ); - - expect(await this.token.balanceOf(this.mock.address), 1); - expect(await this.token.balanceOf(other), 0); - + it('nominal', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); await this.helper.waitForDeadline(); - await this.helper.queue(); + const txQueue = await this.helper.queue(); await this.helper.waitForEta(); const txExecute = await this.helper.execute(); - expect(await this.token.balanceOf(this.mock.address), 0); - expect(await this.token.balanceOf(other), 1); - - await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { - from: this.mock.address, - to: other, - value: '1', - }); - }); - - it('is payable and can transfer eth to EOA', async function () { - const t2g = web3.utils.toBN(128); // timelock to governor - const g2o = web3.utils.toBN(100); // governor to eoa (other) - - this.helper.setProposal( - [ - { - target: this.mock.address, - value: t2g, - data: this.mock.contract.methods.relay(other, g2o, '0x').encodeABI(), - }, - ], - '', - ); - - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - const timelockBalance = await web3.eth.getBalance(this.timelock.address).then(web3.utils.toBN); - const otherBalance = await web3.eth.getBalance(other).then(web3.utils.toBN); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - await this.helper.execute(); - - expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(timelockBalance.sub(t2g)); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(t2g.sub(g2o)); - expect(await web3.eth.getBalance(other)).to.be.bignumber.equal(otherBalance.add(g2o)); - }); - - it('protected against other proposers', async function () { - await this.timelock.schedule( - this.mock.address, - web3.utils.toWei('0'), - this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(), - constants.ZERO_BYTES32, - constants.ZERO_BYTES32, - 3600, - { from: owner }, - ); - - await time.increase(3600); - - await expectRevert( - this.timelock.execute( - this.mock.address, - web3.utils.toWei('0'), - this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(), - constants.ZERO_BYTES32, - constants.ZERO_BYTES32, - { from: owner }, - ), - 'TimelockController: underlying transaction reverted', - ); - }); - }); - - describe('updateTimelock', function () { - beforeEach(async function () { - this.newTimelock = await Timelock.new(3600, [this.mock.address], [this.mock.address], constants.ZERO_ADDRESS); - }); - - it('is protected', async function () { - await expectRevert(this.mock.updateTimelock(this.newTimelock.address), 'Governor: onlyGovernance'); - }); - - it('can be executed through governance to', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - expectEvent(txExecute, 'TimelockChange', { - oldTimelock: this.timelock.address, - newTimelock: this.newTimelock.address, + expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); + await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { id: this.proposal.timelockid }); + await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallSalt', { + id: this.proposal.timelockid, }); - expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); + expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); + await expectEvent.inTransaction(txExecute.tx, this.timelock, 'CallExecuted', { id: this.proposal.timelockid }); + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + }); + + describe('should revert', function () { + describe('on queue', function () { + it('if already queued', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.waitForDeadline(); + const txQueue = await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); + await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { + id: this.proposal.timelockid, + }); + + expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); + await expectEvent.inTransaction(txExecute.tx, this.timelock, 'CallExecuted', { + id: this.proposal.timelockid, + }); + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + }); + + describe('should revert', function () { + describe('on queue', function () { + it('if already queued', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); + }); + }); + + describe('on execute', function () { + it('if not queued', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(+1); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + + await expectRevert(this.helper.execute(), 'TimelockController: operation is not ready'); + }); + + it('if too early', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + + await expectRevert(this.helper.execute(), 'TimelockController: operation is not ready'); + }); + + it('if already executed', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + await this.helper.execute(); + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + + it('if already executed by another proposer', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + + await this.timelock.executeBatch( + ...this.proposal.shortProposal.slice(0, 3), + '0x0', + this.proposal.shortProposal[3], + ); + + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + }); + }); + + describe('cancel', function () { + it('cancel before queue prevents scheduling', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); + }); + + it('cancel after queue prevents executing', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + + expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); + + it('cancel on timelock is reflected on governor', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + + expectEvent(await this.timelock.cancel(this.proposal.timelockid, { from: owner }), 'Cancelled', { + id: this.proposal.timelockid, + }); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + }); + }); + + describe('onlyGovernance', function () { + describe('relay', function () { + beforeEach(async function () { + await this.token.$_mint(this.mock.address, 1); + }); + + it('is protected', async function () { + await expectRevert( + this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()), + 'Governor: onlyGovernance', + ); + }); + + it('can be executed through governance', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods + .relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()) + .encodeABI(), + }, + ], + '', + ); + + expect(await this.token.balanceOf(this.mock.address), 1); + expect(await this.token.balanceOf(other), 0); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + expect(await this.token.balanceOf(this.mock.address), 0); + expect(await this.token.balanceOf(other), 1); + + await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { + from: this.mock.address, + to: other, + value: '1', + }); + }); + + it('is payable and can transfer eth to EOA', async function () { + const t2g = web3.utils.toBN(128); // timelock to governor + const g2o = web3.utils.toBN(100); // governor to eoa (other) + + this.helper.setProposal( + [ + { + target: this.mock.address, + value: t2g, + data: this.mock.contract.methods.relay(other, g2o, '0x').encodeABI(), + }, + ], + '', + ); + + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); + const timelockBalance = await web3.eth.getBalance(this.timelock.address).then(web3.utils.toBN); + const otherBalance = await web3.eth.getBalance(other).then(web3.utils.toBN); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + await this.helper.execute(); + + expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal( + timelockBalance.sub(t2g), + ); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(t2g.sub(g2o)); + expect(await web3.eth.getBalance(other)).to.be.bignumber.equal(otherBalance.add(g2o)); + }); + + it('protected against other proposers', async function () { + await this.timelock.schedule( + this.mock.address, + web3.utils.toWei('0'), + this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(), + constants.ZERO_BYTES32, + constants.ZERO_BYTES32, + 3600, + { from: owner }, + ); + + await time.increase(3600); + + await expectRevert( + this.timelock.execute( + this.mock.address, + web3.utils.toWei('0'), + this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(), + constants.ZERO_BYTES32, + constants.ZERO_BYTES32, + { from: owner }, + ), + 'TimelockController: underlying transaction reverted', + ); + }); + }); + + describe('updateTimelock', function () { + beforeEach(async function () { + this.newTimelock = await Timelock.new( + 3600, + [this.mock.address], + [this.mock.address], + constants.ZERO_ADDRESS, + ); + }); + + it('is protected', async function () { + await expectRevert(this.mock.updateTimelock(this.newTimelock.address), 'Governor: onlyGovernance'); + }); + + it('can be executed through governance to', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + expectEvent(txExecute, 'TimelockChange', { + oldTimelock: this.timelock.address, + newTimelock: this.newTimelock.address, + }); + + expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); + }); + }); + }); + + it('clear queue of pending governor calls', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.nonGovernanceFunction().encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + await this.helper.execute(); + + // This path clears _governanceCall as part of the afterExecute call, + // but we have not way to check that the cleanup actually happened other + // then coverage reports. + }); + }); }); }); - }); - - it('clear queue of pending governor calls', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.nonGovernanceFunction().encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - await this.helper.execute(); - - // This path clears _governanceCall as part of the afterExecute call, - // but we have not way to check that the cleanup actually happened other - // then coverage reports. - }); + } }); diff --git a/test/governance/extensions/GovernorVotesQuorumFraction.test.js b/test/governance/extensions/GovernorVotesQuorumFraction.test.js index 84d36f926..ca915806f 100644 --- a/test/governance/extensions/GovernorVotesQuorumFraction.test.js +++ b/test/governance/extensions/GovernorVotesQuorumFraction.test.js @@ -1,12 +1,17 @@ -const { BN, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const Enums = require('../../helpers/enums'); const { GovernorHelper } = require('../../helpers/governance'); +const { clock } = require('../../helpers/time'); -const Token = artifacts.require('$ERC20Votes'); const Governor = artifacts.require('$GovernorMock'); const CallReceiver = artifacts.require('CallReceiverMock'); +const TOKENS = [ + { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, +]; + contract('GovernorVotesQuorumFraction', function (accounts) { const [owner, voter1, voter2, voter3, voter4] = accounts; @@ -14,129 +19,136 @@ contract('GovernorVotesQuorumFraction', function (accounts) { // const version = '1'; const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; - const tokenSupply = new BN(web3.utils.toWei('100')); - const ratio = new BN(8); // percents - const newRatio = new BN(6); // percents - const votingDelay = new BN(4); - const votingPeriod = new BN(16); + const tokenSupply = web3.utils.toBN(web3.utils.toWei('100')); + const ratio = web3.utils.toBN(8); // percents + const newRatio = web3.utils.toBN(6); // percents + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); const value = web3.utils.toWei('1'); - beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol, tokenName); - this.mock = await Governor.new(name, votingDelay, votingPeriod, 0, this.token.address, ratio); - this.receiver = await CallReceiver.new(); + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol, tokenName); + this.mock = await Governor.new(name, votingDelay, votingPeriod, 0, this.token.address, ratio); + this.receiver = await CallReceiver.new(); - this.helper = new GovernorHelper(this.mock); + this.helper = new GovernorHelper(this.mock, mode); - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + await this.token.$_mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - }, - ], - '', - ); - }); - - it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(ratio); - expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100'); - expect(await time.latestBlock().then(blockNumber => this.mock.quorum(blockNumber.subn(1)))).to.be.bignumber.equal( - tokenSupply.mul(ratio).divn(100), - ); - }); - - it('quroum reached', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - }); - - it('quroum not reached', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.waitForDeadline(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - - describe('onlyGovernance updates', function () { - it('updateQuorumNumerator is protected', async function () { - await expectRevert(this.mock.updateQuorumNumerator(newRatio), 'Governor: onlyGovernance'); - }); - - it('can updateQuorumNumerator through governance', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.updateQuorumNumerator(newRatio).encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - expectEvent(await this.helper.execute(), 'QuorumNumeratorUpdated', { - oldQuorumNumerator: ratio, - newQuorumNumerator: newRatio, + // default proposal + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], + '', + ); }); - expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(newRatio); - expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100'); + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(ratio); + expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100'); + expect(await clock[mode]().then(timepoint => this.mock.quorum(timepoint - 1))).to.be.bignumber.equal( + tokenSupply.mul(ratio).divn(100), + ); + }); - // it takes one block for the new quorum to take effect - expect(await time.latestBlock().then(blockNumber => this.mock.quorum(blockNumber.subn(1)))).to.be.bignumber.equal( - tokenSupply.mul(ratio).divn(100), - ); + it('quroum reached', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + }); - await time.advanceBlock(); + it('quroum not reached', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.waitForDeadline(); + await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + }); - expect(await time.latestBlock().then(blockNumber => this.mock.quorum(blockNumber.subn(1)))).to.be.bignumber.equal( - tokenSupply.mul(newRatio).divn(100), - ); + describe('onlyGovernance updates', function () { + it('updateQuorumNumerator is protected', async function () { + await expectRevert(this.mock.updateQuorumNumerator(newRatio), 'Governor: onlyGovernance'); + }); + + it('can updateQuorumNumerator through governance', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.updateQuorumNumerator(newRatio).encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + expectEvent(await this.helper.execute(), 'QuorumNumeratorUpdated', { + oldQuorumNumerator: ratio, + newQuorumNumerator: newRatio, + }); + + expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(newRatio); + expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100'); + + // it takes one block for the new quorum to take effect + expect(await clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1))).to.be.bignumber.equal( + tokenSupply.mul(ratio).divn(100), + ); + + await time.advanceBlock(); + + expect(await clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1))).to.be.bignumber.equal( + tokenSupply.mul(newRatio).divn(100), + ); + }); + + it('cannot updateQuorumNumerator over the maximum', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.updateQuorumNumerator('101').encodeABI(), + }, + ], + '', + ); + + 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.execute(), + 'GovernorVotesQuorumFraction: quorumNumerator over quorumDenominator', + ); + }); + }); }); - - it('cannot updateQuorumNumerator over the maximum', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.updateQuorumNumerator('101').encodeABI(), - }, - ], - '', - ); - - 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.execute(), 'GovernorVotesQuorumFraction: quorumNumerator over quorumDenominator'); - }); - }); + } }); diff --git a/test/governance/extensions/GovernorWithParams.test.js b/test/governance/extensions/GovernorWithParams.test.js index 86ebacc08..4e146947c 100644 --- a/test/governance/extensions/GovernorWithParams.test.js +++ b/test/governance/extensions/GovernorWithParams.test.js @@ -1,4 +1,4 @@ -const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; @@ -7,17 +7,21 @@ const Enums = require('../../helpers/enums'); const { getDomain, domainType } = require('../../helpers/eip712'); const { GovernorHelper } = require('../../helpers/governance'); -const Token = artifacts.require('$ERC20VotesComp'); const Governor = artifacts.require('$GovernorWithParamsMock'); const CallReceiver = artifacts.require('CallReceiverMock'); const rawParams = { - uintParam: new BN('42'), + uintParam: web3.utils.toBN('42'), strParam: 'These are my params', }; const encodedParams = web3.eth.abi.encodeParameters(['uint256', 'string'], Object.values(rawParams)); +const TOKENS = [ + { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, +]; + contract('GovernorWithParams', function (accounts) { const [owner, proposer, voter1, voter2, voter3, voter4] = accounts; @@ -25,141 +29,145 @@ contract('GovernorWithParams', function (accounts) { const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); - const votingDelay = new BN(4); - const votingPeriod = new BN(16); + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); const value = web3.utils.toWei('1'); - beforeEach(async function () { - this.chainId = await web3.eth.getChainId(); - this.token = await Token.new(tokenName, tokenSymbol, tokenName); - this.mock = await Governor.new(name, this.token.address); - this.receiver = await CallReceiver.new(); + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + this.chainId = await web3.eth.getChainId(); + this.token = await Token.new(tokenName, tokenSymbol, tokenName); + this.mock = await Governor.new(name, this.token.address); + this.receiver = await CallReceiver.new(); - this.helper = new GovernorHelper(this.mock); + this.helper = new GovernorHelper(this.mock, mode); - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + await this.token.$_mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - }, - ], - '', - ); - }); + // default proposal + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], + '', + ); + }); - it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - }); + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + }); - it('nominal is unaffected', async function () { - await this.helper.propose({ from: proposer }); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); + it('nominal is unaffected', async function () { + await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); - expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value); - }); + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value); + }); - it('Voting with params is properly supported', async function () { - await this.helper.propose({ from: proposer }); - await this.helper.waitForSnapshot(); + it('Voting with params is properly supported', async function () { + await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); - const weight = new BN(web3.utils.toWei('7')).sub(rawParams.uintParam); + const weight = web3.utils.toBN(web3.utils.toWei('7')).sub(rawParams.uintParam); - const tx = await this.helper.vote( - { - support: Enums.VoteType.For, - reason: 'no particular reason', - params: encodedParams, - }, - { from: voter2 }, - ); - - expectEvent(tx, 'CountParams', { ...rawParams }); - expectEvent(tx, 'VoteCastWithParams', { - voter: voter2, - proposalId: this.proposal.id, - support: Enums.VoteType.For, - weight, - reason: 'no particular reason', - params: encodedParams, - }); - - const votes = await this.mock.proposalVotes(this.proposal.id); - expect(votes.forVotes).to.be.bignumber.equal(weight); - }); - - it('Voting with params by signature is properly supported', async function () { - const voterBySig = Wallet.generate(); - const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString()); - - const signature = (contract, message) => - getDomain(contract) - .then(domain => ({ - primaryType: 'ExtendedBallot', - types: { - EIP712Domain: domainType(domain), - ExtendedBallot: [ - { name: 'proposalId', type: 'uint256' }, - { name: 'support', type: 'uint8' }, - { name: 'reason', type: 'string' }, - { name: 'params', type: 'bytes' }, - ], + const tx = await this.helper.vote( + { + support: Enums.VoteType.For, + reason: 'no particular reason', + params: encodedParams, }, - domain, - message, - })) - .then(data => ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { data })) - .then(fromRpcSig); + { from: voter2 }, + ); - await this.token.delegate(voterBySigAddress, { from: voter2 }); + expectEvent(tx, 'CountParams', { ...rawParams }); + expectEvent(tx, 'VoteCastWithParams', { + voter: voter2, + proposalId: this.proposal.id, + support: Enums.VoteType.For, + weight, + reason: 'no particular reason', + params: encodedParams, + }); - // Run proposal - await this.helper.propose(); - await this.helper.waitForSnapshot(); + const votes = await this.mock.proposalVotes(this.proposal.id); + expect(votes.forVotes).to.be.bignumber.equal(weight); + }); - const weight = new BN(web3.utils.toWei('7')).sub(rawParams.uintParam); + it('Voting with params by signature is properly supported', async function () { + const voterBySig = Wallet.generate(); + const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString()); - const tx = await this.helper.vote({ - support: Enums.VoteType.For, - reason: 'no particular reason', - params: encodedParams, - signature, + const signature = (contract, message) => + getDomain(contract) + .then(domain => ({ + primaryType: 'ExtendedBallot', + types: { + EIP712Domain: domainType(domain), + ExtendedBallot: [ + { name: 'proposalId', type: 'uint256' }, + { name: 'support', type: 'uint8' }, + { name: 'reason', type: 'string' }, + { name: 'params', type: 'bytes' }, + ], + }, + domain, + message, + })) + .then(data => ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { data })) + .then(fromRpcSig); + + await this.token.delegate(voterBySigAddress, { from: voter2 }); + + // Run proposal + await this.helper.propose(); + await this.helper.waitForSnapshot(); + + const weight = web3.utils.toBN(web3.utils.toWei('7')).sub(rawParams.uintParam); + + const tx = await this.helper.vote({ + support: Enums.VoteType.For, + reason: 'no particular reason', + params: encodedParams, + signature, + }); + + expectEvent(tx, 'CountParams', { ...rawParams }); + expectEvent(tx, 'VoteCastWithParams', { + voter: voterBySigAddress, + proposalId: this.proposal.id, + support: Enums.VoteType.For, + weight, + reason: 'no particular reason', + params: encodedParams, + }); + + const votes = await this.mock.proposalVotes(this.proposal.id); + expect(votes.forVotes).to.be.bignumber.equal(weight); + }); }); - - expectEvent(tx, 'CountParams', { ...rawParams }); - expectEvent(tx, 'VoteCastWithParams', { - voter: voterBySigAddress, - proposalId: this.proposal.id, - support: Enums.VoteType.For, - weight, - reason: 'no particular reason', - params: encodedParams, - }); - - const votes = await this.mock.proposalVotes(this.proposal.id); - expect(votes.forVotes).to.be.bignumber.equal(weight); - }); + } }); diff --git a/test/governance/utils/EIP6372.behavior.js b/test/governance/utils/EIP6372.behavior.js new file mode 100644 index 000000000..022ec3568 --- /dev/null +++ b/test/governance/utils/EIP6372.behavior.js @@ -0,0 +1,23 @@ +const { clock } = require('../../helpers/time'); + +function shouldBehaveLikeEIP6372(mode = 'blocknumber') { + describe('should implement EIP6372', function () { + beforeEach(async function () { + this.mock = this.mock ?? this.token ?? this.votes; + }); + + it('clock is correct', async function () { + expect(await this.mock.clock()).to.be.bignumber.equal(await clock[mode]().then(web3.utils.toBN)); + }); + + it('CLOCK_MODE is correct', async function () { + const params = new URLSearchParams(await this.mock.CLOCK_MODE()); + expect(params.get('mode')).to.be.equal(mode); + expect(params.get('from')).to.be.equal(mode == 'blocknumber' ? 'default' : null); + }); + }); +} + +module.exports = { + shouldBehaveLikeEIP6372, +}; diff --git a/test/governance/utils/Votes.behavior.js b/test/governance/utils/Votes.behavior.js index 8fe34dc47..864a2f976 100644 --- a/test/governance/utils/Votes.behavior.js +++ b/test/governance/utils/Votes.behavior.js @@ -6,7 +6,10 @@ const { fromRpcSig } = require('ethereumjs-util'); const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; +const { shouldBehaveLikeEIP6372 } = require('./EIP6372.behavior'); + const { getDomain, domainType, domainSeparator } = require('../../helpers/eip712'); +const { clockFromReceipt } = require('../../helpers/time'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -14,7 +17,9 @@ const Delegation = [ { name: 'expiry', type: 'uint256' }, ]; -function shouldBehaveLikeVotes() { +function shouldBehaveLikeVotes(mode = 'blocknumber') { + shouldBehaveLikeEIP6372(mode); + describe('run votes workflow', function () { it('initial nonce is 0', async function () { expect(await this.votes.nonces(this.account1)).to.be.bignumber.equal('0'); @@ -57,6 +62,8 @@ function shouldBehaveLikeVotes() { expect(await this.votes.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.votes.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + const timepoint = await clockFromReceipt[mode](receipt); + expectEvent(receipt, 'DelegateChanged', { delegator: delegatorAddress, fromDelegate: ZERO_ADDRESS, @@ -71,9 +78,9 @@ function shouldBehaveLikeVotes() { expect(await this.votes.delegates(delegatorAddress)).to.be.equal(delegatorAddress); expect(await this.votes.getVotes(delegatorAddress)).to.be.bignumber.equal('1'); - expect(await this.votes.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(delegatorAddress, timepoint - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); - expect(await this.votes.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastVotes(delegatorAddress, timepoint)).to.be.bignumber.equal('1'); }); it('rejects reused signature', async function () { @@ -157,6 +164,8 @@ function shouldBehaveLikeVotes() { expect(await this.votes.delegates(this.account1)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.votes.delegate(this.account1, { from: this.account1 }); + const timepoint = await clockFromReceipt[mode](receipt); + expectEvent(receipt, 'DelegateChanged', { delegator: this.account1, fromDelegate: ZERO_ADDRESS, @@ -171,9 +180,9 @@ function shouldBehaveLikeVotes() { expect(await this.votes.delegates(this.account1)).to.be.equal(this.account1); expect(await this.votes.getVotes(this.account1)).to.be.bignumber.equal('1'); - expect(await this.votes.getPastVotes(this.account1, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(this.account1, timepoint - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); - expect(await this.votes.getPastVotes(this.account1, receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastVotes(this.account1, timepoint)).to.be.bignumber.equal('1'); }); it('delegation without tokens', async function () { @@ -202,6 +211,8 @@ function shouldBehaveLikeVotes() { expect(await this.votes.delegates(this.account1)).to.be.equal(this.account1); const { receipt } = await this.votes.delegate(this.account1Delegatee, { from: this.account1 }); + const timepoint = await clockFromReceipt[mode](receipt); + expectEvent(receipt, 'DelegateChanged', { delegator: this.account1, fromDelegate: this.account1, @@ -217,16 +228,16 @@ function shouldBehaveLikeVotes() { previousBalance: '0', newBalance: '1', }); - const prevBlock = receipt.blockNumber - 1; + expect(await this.votes.delegates(this.account1)).to.be.equal(this.account1Delegatee); expect(await this.votes.getVotes(this.account1)).to.be.bignumber.equal('0'); expect(await this.votes.getVotes(this.account1Delegatee)).to.be.bignumber.equal('1'); - expect(await this.votes.getPastVotes(this.account1, receipt.blockNumber - 1)).to.be.bignumber.equal('1'); - expect(await this.votes.getPastVotes(this.account1Delegatee, prevBlock)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(this.account1, timepoint - 1)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastVotes(this.account1Delegatee, timepoint - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); - expect(await this.votes.getPastVotes(this.account1, receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastVotes(this.account1Delegatee, receipt.blockNumber)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastVotes(this.account1, timepoint)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(this.account1Delegatee, timepoint)).to.be.bignumber.equal('1'); }); }); @@ -236,7 +247,7 @@ function shouldBehaveLikeVotes() { }); it('reverts if block number >= current block', async function () { - await expectRevert(this.votes.getPastTotalSupply(5e10), 'block not yet mined'); + await expectRevert(this.votes.getPastTotalSupply(5e10), 'future lookup'); }); it('returns 0 if there are no checkpoints', async function () { @@ -244,22 +255,24 @@ function shouldBehaveLikeVotes() { }); it('returns the latest block if >= last checkpoint block', async function () { - const t1 = await this.votes.$_mint(this.account1, this.NFT0); + const { receipt } = await this.votes.$_mint(this.account1, this.NFT0); + const timepoint = await clockFromReceipt[mode](receipt); await time.advanceBlock(); await time.advanceBlock(); - expect(await this.votes.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal('1'); }); it('returns zero if < first checkpoint block', async function () { await time.advanceBlock(); - const t2 = await this.votes.$_mint(this.account1, this.NFT1); + const { receipt } = await this.votes.$_mint(this.account1, this.NFT1); + const timepoint = await clockFromReceipt[mode](receipt); await time.advanceBlock(); await time.advanceBlock(); - expect(await this.votes.getPastTotalSupply(t2.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal('1'); }); it('generally returns the voting balance at the appropriate checkpoint', async function () { @@ -279,17 +292,23 @@ function shouldBehaveLikeVotes() { await time.advanceBlock(); await time.advanceBlock(); - expect(await this.votes.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.votes.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.votes.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.votes.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - expect(await this.votes.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastTotalSupply(t5.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await this.votes.getPastTotalSupply(t5.receipt.blockNumber + 1)).to.be.bignumber.equal('1'); + t1.timepoint = await clockFromReceipt[mode](t1.receipt); + t2.timepoint = await clockFromReceipt[mode](t2.receipt); + t3.timepoint = await clockFromReceipt[mode](t3.receipt); + t4.timepoint = await clockFromReceipt[mode](t4.receipt); + t5.timepoint = await clockFromReceipt[mode](t5.receipt); + + expect(await this.votes.getPastTotalSupply(t1.timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(t5.timepoint)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastTotalSupply(t5.timepoint + 1)).to.be.bignumber.equal('1'); }); }); @@ -305,7 +324,7 @@ function shouldBehaveLikeVotes() { describe('getPastVotes', function () { it('reverts if block number >= current block', async function () { - await expectRevert(this.votes.getPastVotes(this.account2, 5e10), 'block not yet mined'); + await expectRevert(this.votes.getPastVotes(this.account2, 5e10), 'future lookup'); }); it('returns 0 if there are no checkpoints', async function () { @@ -313,22 +332,24 @@ function shouldBehaveLikeVotes() { }); it('returns the latest block if >= last checkpoint block', async function () { - const t1 = await this.votes.delegate(this.account2, { from: this.account1 }); + const { receipt } = await this.votes.delegate(this.account2, { from: this.account1 }); + const timepoint = await clockFromReceipt[mode](receipt); await time.advanceBlock(); await time.advanceBlock(); + const latest = await this.votes.getVotes(this.account2); - const nextBlock = t1.receipt.blockNumber + 1; - expect(await this.votes.getPastVotes(this.account2, t1.receipt.blockNumber)).to.be.bignumber.equal(latest); - expect(await this.votes.getPastVotes(this.account2, nextBlock)).to.be.bignumber.equal(latest); + expect(await this.votes.getPastVotes(this.account2, timepoint)).to.be.bignumber.equal(latest); + expect(await this.votes.getPastVotes(this.account2, timepoint + 1)).to.be.bignumber.equal(latest); }); it('returns zero if < first checkpoint block', async function () { await time.advanceBlock(); - const t1 = await this.votes.delegate(this.account2, { from: this.account1 }); + const { receipt } = await this.votes.delegate(this.account2, { from: this.account1 }); + const timepoint = await clockFromReceipt[mode](receipt); await time.advanceBlock(); await time.advanceBlock(); - expect(await this.votes.getPastVotes(this.account2, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(this.account2, timepoint - 1)).to.be.bignumber.equal('0'); }); }); }); diff --git a/test/governance/utils/Votes.test.js b/test/governance/utils/Votes.test.js index 7cf998b2d..62b1ab2cd 100644 --- a/test/governance/utils/Votes.test.js +++ b/test/governance/utils/Votes.test.js @@ -3,56 +3,69 @@ const { expectRevert, BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { getChainId } = require('../../helpers/chainid'); +const { clockFromReceipt } = require('../../helpers/time'); const { shouldBehaveLikeVotes } = require('./Votes.behavior'); -const Votes = artifacts.require('$VotesMock'); +const MODES = { + blocknumber: artifacts.require('$VotesMock'), + timestamp: artifacts.require('$VotesTimestampMock'), +}; contract('Votes', function (accounts) { const [account1, account2, account3] = accounts; - beforeEach(async function () { - this.name = 'My Vote'; - this.votes = await Votes.new(this.name, '1'); - }); - it('starts with zero votes', async function () { - expect(await this.votes.getTotalSupply()).to.be.bignumber.equal('0'); - }); + for (const [mode, artifact] of Object.entries(MODES)) { + describe(`vote with ${mode}`, function () { + beforeEach(async function () { + this.name = 'My Vote'; + this.votes = await artifact.new(this.name, '1'); + }); - describe('performs voting operations', function () { - beforeEach(async function () { - this.tx1 = await this.votes.$_mint(account1, 1); - this.tx2 = await this.votes.$_mint(account2, 1); - this.tx3 = await this.votes.$_mint(account3, 1); + it('starts with zero votes', async function () { + expect(await this.votes.getTotalSupply()).to.be.bignumber.equal('0'); + }); + + describe('performs voting operations', function () { + beforeEach(async function () { + this.tx1 = await this.votes.$_mint(account1, 1); + this.tx2 = await this.votes.$_mint(account2, 1); + this.tx3 = await this.votes.$_mint(account3, 1); + this.tx1.timepoint = await clockFromReceipt[mode](this.tx1.receipt); + this.tx2.timepoint = await clockFromReceipt[mode](this.tx2.receipt); + this.tx3.timepoint = await clockFromReceipt[mode](this.tx3.receipt); + }); + + it('reverts if block number >= current block', async function () { + await expectRevert(this.votes.getPastTotalSupply(this.tx3.timepoint + 1), 'Votes: future lookup'); + }); + + it('delegates', async function () { + await this.votes.delegate(account3, account2); + + expect(await this.votes.delegates(account3)).to.be.equal(account2); + }); + + it('returns total amount of votes', async function () { + expect(await this.votes.getTotalSupply()).to.be.bignumber.equal('3'); + }); + }); + + describe('performs voting workflow', function () { + beforeEach(async function () { + this.chainId = await getChainId(); + this.account1 = account1; + this.account2 = account2; + this.account1Delegatee = account2; + this.NFT0 = new BN('10000000000000000000000000'); + this.NFT1 = new BN('10'); + this.NFT2 = new BN('20'); + this.NFT3 = new BN('30'); + }); + + // includes EIP6372 behavior check + shouldBehaveLikeVotes(mode); + }); }); - - it('reverts if block number >= current block', async function () { - await expectRevert(this.votes.getPastTotalSupply(this.tx3.receipt.blockNumber + 1), 'Votes: block not yet mined'); - }); - - it('delegates', async function () { - await this.votes.delegate(account3, account2); - - expect(await this.votes.delegates(account3)).to.be.equal(account2); - }); - - it('returns total amount of votes', async function () { - expect(await this.votes.getTotalSupply()).to.be.bignumber.equal('3'); - }); - }); - - describe('performs voting workflow', function () { - beforeEach(async function () { - this.chainId = await getChainId(); - this.account1 = account1; - this.account2 = account2; - this.account1Delegatee = account2; - this.NFT0 = new BN('10000000000000000000000000'); - this.NFT1 = new BN('10'); - this.NFT2 = new BN('20'); - this.NFT3 = new BN('30'); - }); - - shouldBehaveLikeVotes(); - }); + } }); diff --git a/test/helpers/governance.js b/test/helpers/governance.js index b945a16e0..ae88e151a 100644 --- a/test/helpers/governance.js +++ b/test/helpers/governance.js @@ -1,4 +1,4 @@ -const { time } = require('@openzeppelin/test-helpers'); +const { forward } = require('../helpers/time'); function zip(...args) { return Array(Math.max(...args.map(array => array.length))) @@ -15,8 +15,9 @@ function concatOpts(args, opts = null) { } class GovernorHelper { - constructor(governor) { + constructor(governor, mode = 'blocknumber') { this.governor = governor; + this.mode = mode; } delegate(delegation = {}, opts = null) { @@ -116,21 +117,17 @@ class GovernorHelper { waitForSnapshot(offset = 0) { const proposal = this.currentProposal; - return this.governor - .proposalSnapshot(proposal.id) - .then(blockNumber => time.advanceBlockTo(blockNumber.addn(offset))); + return this.governor.proposalSnapshot(proposal.id).then(timepoint => forward[this.mode](timepoint.addn(offset))); } waitForDeadline(offset = 0) { const proposal = this.currentProposal; - return this.governor - .proposalDeadline(proposal.id) - .then(blockNumber => time.advanceBlockTo(blockNumber.addn(offset))); + return this.governor.proposalDeadline(proposal.id).then(timepoint => forward[this.mode](timepoint.addn(offset))); } waitForEta(offset = 0) { const proposal = this.currentProposal; - return this.governor.proposalEta(proposal.id).then(timestamp => time.increaseTo(timestamp.addn(offset))); + return this.governor.proposalEta(proposal.id).then(timestamp => forward.timestamp(timestamp.addn(offset))); } /** diff --git a/test/helpers/time.js b/test/helpers/time.js new file mode 100644 index 000000000..2e5f6d85a --- /dev/null +++ b/test/helpers/time.js @@ -0,0 +1,16 @@ +const { time } = require('@openzeppelin/test-helpers'); + +module.exports = { + clock: { + blocknumber: () => web3.eth.getBlock('latest').then(block => block.number), + timestamp: () => web3.eth.getBlock('latest').then(block => block.timestamp), + }, + clockFromReceipt: { + blocknumber: receipt => Promise.resolve(receipt.blockNumber), + timestamp: receipt => web3.eth.getBlock(receipt.blockNumber).then(block => block.timestamp), + }, + forward: { + blocknumber: time.advanceBlockTo, + timestamp: time.increaseTo, + }, +}; diff --git a/test/token/ERC20/extensions/ERC20Votes.test.js b/test/token/ERC20/extensions/ERC20Votes.test.js index 9759c1162..e722ed1a1 100644 --- a/test/token/ERC20/extensions/ERC20Votes.test.js +++ b/test/token/ERC20/extensions/ERC20Votes.test.js @@ -8,11 +8,11 @@ const { fromRpcSig } = require('ethereumjs-util'); const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; -const ERC20Votes = artifacts.require('$ERC20Votes'); - const { batchInBlock } = require('../../../helpers/txpool'); const { getDomain, domainType, domainSeparator } = require('../../../helpers/eip712'); -const { getChainId } = require('../../../helpers/chainid'); +const { clock, clockFromReceipt } = require('../../../helpers/time'); + +const { shouldBehaveLikeEIP6372 } = require('../../../governance/utils/EIP6372.behavior'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -20,541 +20,559 @@ const Delegation = [ { name: 'expiry', type: 'uint256' }, ]; +const MODES = { + blocknumber: artifacts.require('$ERC20Votes'), + timestamp: artifacts.require('$ERC20VotesTimestampMock'), +}; + contract('ERC20Votes', function (accounts) { const [holder, recipient, holderDelegatee, other1, other2] = accounts; const name = 'My Token'; const symbol = 'MTKN'; - const version = '1'; const supply = new BN('10000000000000000000000000'); - beforeEach(async function () { - this.chainId = await getChainId(); - this.token = await ERC20Votes.new(name, symbol, name); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator)); - }); - - it('minting restriction', async function () { - const amount = new BN('2').pow(new BN('224')); - await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes'); - }); - - it('recent checkpoints', async function () { - await this.token.delegate(holder, { from: holder }); - for (let i = 0; i < 6; i++) { - await this.token.$_mint(holder, 1); - } - const block = await web3.eth.getBlockNumber(); - expect(await this.token.numCheckpoints(holder)).to.be.bignumber.equal('6'); - // recent - expect(await this.token.getPastVotes(holder, block - 1)).to.be.bignumber.equal('5'); - // non-recent - expect(await this.token.getPastVotes(holder, block - 6)).to.be.bignumber.equal('0'); - }); - - describe('set delegation', function () { - describe('call', function () { - it('delegation with balance', async function () { - await this.token.$_mint(holder, supply); - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: '0', - newBalance: supply, - }); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); - }); - - it('delegation without balance', async function () { - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - }); - }); - - describe('with signature', function () { - const delegator = Wallet.generate(); - const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); - const nonce = 0; - - const buildData = (contract, message) => - getDomain(contract).then(domain => ({ - primaryType: 'Delegation', - types: { EIP712Domain: domainType(domain), Delegation }, - domain, - message, - })); - + for (const [mode, artifact] of Object.entries(MODES)) { + describe(`vote with ${mode}`, function () { beforeEach(async function () { - await this.token.$_mint(delegatorAddress, supply); + this.token = await artifact.new(name, symbol, name); }); - it('accept signed delegation', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }) - .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) - .then(fromRpcSig); + shouldBehaveLikeEIP6372(mode); - expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); + it('initial nonce is 0', async function () { + expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); + }); - const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - expectEvent(receipt, 'DelegateChanged', { - delegator: delegatorAddress, - fromDelegate: ZERO_ADDRESS, - toDelegate: delegatorAddress, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: delegatorAddress, - previousBalance: '0', - newBalance: supply, + it('domain separator', async function () { + expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator)); + }); + + it('minting restriction', async function () { + const amount = new BN('2').pow(new BN('224')); + await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes'); + }); + + it('recent checkpoints', async function () { + await this.token.delegate(holder, { from: holder }); + for (let i = 0; i < 6; i++) { + await this.token.$_mint(holder, 1); + } + const block = await clock[mode](); + expect(await this.token.numCheckpoints(holder)).to.be.bignumber.equal('6'); + // recent + expect(await this.token.getPastVotes(holder, block - 1)).to.be.bignumber.equal('5'); + // non-recent + expect(await this.token.getPastVotes(holder, block - 6)).to.be.bignumber.equal('0'); + }); + + describe('set delegation', function () { + describe('call', function () { + it('delegation with balance', async function () { + await this.token.$_mint(holder, supply); + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + const timepoint = await clockFromReceipt[mode](receipt); + + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal(supply); + }); + + it('delegation without balance', async function () { + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + }); }); - expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); + describe('with signature', function () { + const delegator = Wallet.generate(); + const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); + const nonce = 0; - expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); + const buildData = (contract, message) => + getDomain(contract).then(domain => ({ + primaryType: 'Delegation', + types: { EIP712Domain: domainType(domain), Delegation }, + domain, + message, + })); + + beforeEach(async function () { + await this.token.$_mint(delegatorAddress, supply); + }); + + it('accept signed delegation', async function () { + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + const timepoint = await clockFromReceipt[mode](receipt); + + expectEvent(receipt, 'DelegateChanged', { + delegator: delegatorAddress, + fromDelegate: ZERO_ADDRESS, + toDelegate: delegatorAddress, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: delegatorAddress, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); + + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(delegatorAddress, timepoint - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(delegatorAddress, timepoint)).to.be.bignumber.equal(supply); + }); + + it('rejects reused signature', async function () { + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); + + await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), + 'ERC20Votes: invalid nonce', + ); + }); + + it('rejects bad delegatee', async function () { + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); + + const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); + const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged'); + expect(args.delegator).to.not.be.equal(delegatorAddress); + expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); + expect(args.toDelegate).to.be.equal(holderDelegatee); + }); + + it('rejects bad nonce', async function () { + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), + 'ERC20Votes: invalid nonce', + ); + }); + + it('rejects expired permit', async function () { + const expiry = (await time.latest()) - time.duration.weeks(1); + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry, + }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), + 'ERC20Votes: signature expired', + ); + }); + }); }); - it('rejects reused signature', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }) - .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) - .then(fromRpcSig); + describe('change delegation', function () { + beforeEach(async function () { + await this.token.$_mint(holder, supply); + await this.token.delegate(holder, { from: holder }); + }); - await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + it('call', async function () { + expect(await this.token.delegates(holder)).to.be.equal(holder); - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), - 'ERC20Votes: invalid nonce', - ); + const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); + const timepoint = await clockFromReceipt[mode](receipt); + + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: holder, + toDelegate: holderDelegatee, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: '0', + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holderDelegatee, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holderDelegatee, timepoint - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(holderDelegatee, timepoint)).to.be.bignumber.equal(supply); + }); }); - it('rejects bad delegatee', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }) - .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) - .then(fromRpcSig); + describe('transfers', function () { + beforeEach(async function () { + await this.token.$_mint(holder, supply); + }); - const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); - const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged'); - expect(args.delegator).to.not.be.equal(delegatorAddress); - expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); - expect(args.toDelegate).to.be.equal(holderDelegatee); + it('no delegation', async function () { + const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + + this.holderVotes = '0'; + this.recipientVotes = '0'; + }); + + it('sender delegation', async function () { + await this.token.delegate(holder, { from: holder }); + + const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: supply.subn(1), + }); + + const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); + expect( + receipt.logs + .filter(({ event }) => event == 'DelegateVotesChanged') + .every(({ logIndex }) => transferLogIndex < logIndex), + ).to.be.equal(true); + + this.holderVotes = supply.subn(1); + this.recipientVotes = '0'; + }); + + it('receiver delegation', async function () { + await this.token.delegate(recipient, { from: recipient }); + + const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); + + const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); + expect( + receipt.logs + .filter(({ event }) => event == 'DelegateVotesChanged') + .every(({ logIndex }) => transferLogIndex < logIndex), + ).to.be.equal(true); + + this.holderVotes = '0'; + this.recipientVotes = '1'; + }); + + it('full delegation', async function () { + await this.token.delegate(holder, { from: holder }); + await this.token.delegate(recipient, { from: recipient }); + + const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: supply.subn(1), + }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); + + const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); + expect( + receipt.logs + .filter(({ event }) => event == 'DelegateVotesChanged') + .every(({ logIndex }) => transferLogIndex < logIndex), + ).to.be.equal(true); + + this.holderVotes = supply.subn(1); + this.recipientVotes = '1'; + }); + + afterEach(async function () { + expect(await this.token.getVotes(holder)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); + + // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" + const timepoint = await clock[mode](); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getPastVotes(recipient, timepoint)).to.be.bignumber.equal(this.recipientVotes); + }); }); - it('rejects bad nonce', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }) - .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) - .then(fromRpcSig); + // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. + describe('Compound test suite', function () { + beforeEach(async function () { + await this.token.$_mint(holder, supply); + }); - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), - 'ERC20Votes: invalid nonce', - ); + describe('balanceOf', function () { + it('grants to initial account', async function () { + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); + + describe('numCheckpoints', function () { + it('returns the number of checkpoints for a delegate', async function () { + await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const t1 = await this.token.delegate(other1, { from: recipient }); + t1.timepoint = await clockFromReceipt[mode](t1.receipt); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + + const t2 = await this.token.transfer(other2, 10, { from: recipient }); + t2.timepoint = await clockFromReceipt[mode](t2.receipt); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + + const t3 = await this.token.transfer(other2, 10, { from: recipient }); + t3.timepoint = await clockFromReceipt[mode](t3.receipt); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + t4.timepoint = await clockFromReceipt[mode](t4.receipt); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); + + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '100']); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.timepoint.toString(), '90']); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.timepoint.toString(), '80']); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.timepoint.toString(), '100']); + + await time.advanceBlock(); + expect(await this.token.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal('100'); + expect(await this.token.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal('90'); + expect(await this.token.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal('80'); + expect(await this.token.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal('100'); + }); + + it('does not add more than one checkpoint in a block', async function () { + await this.token.transfer(recipient, '100', { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const [t1, t2, t3] = await batchInBlock([ + () => this.token.delegate(other1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + ]); + t1.timepoint = await clockFromReceipt[mode](t1.receipt); + t2.timepoint = await clockFromReceipt[mode](t2.receipt); + t3.timepoint = await clockFromReceipt[mode](t3.receipt); + + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '80']); + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + t4.timepoint = await clockFromReceipt[mode](t4.receipt); + + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.timepoint.toString(), '100']); + }); + }); + + describe('getPastVotes', function () { + it('reverts if block number >= current block', async function () { + await expectRevert(this.token.getPastVotes(other1, 5e10), 'ERC20Votes: future lookup'); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + const { receipt } = await this.token.delegate(other1, { from: holder }); + const timepoint = await clockFromReceipt[mode](receipt); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, timepoint)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + expect(await this.token.getPastVotes(other1, timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const { receipt } = await this.token.delegate(other1, { from: holder }); + const timepoint = await clockFromReceipt[mode](receipt); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.transfer(holder, 20, { from: other2 }); + await time.advanceBlock(); + await time.advanceBlock(); + + t1.timepoint = await clockFromReceipt[mode](t1.receipt); + t2.timepoint = await clockFromReceipt[mode](t2.receipt); + t3.timepoint = await clockFromReceipt[mode](t3.receipt); + t4.timepoint = await clockFromReceipt[mode](t4.receipt); + + expect(await this.token.getPastVotes(other1, t1.timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + expect(await this.token.getPastVotes(other1, t1.timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + expect(await this.token.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal( + '9999999999999999999999990', + ); + expect(await this.token.getPastVotes(other1, t2.timepoint + 1)).to.be.bignumber.equal( + '9999999999999999999999990', + ); + expect(await this.token.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal( + '9999999999999999999999980', + ); + expect(await this.token.getPastVotes(other1, t3.timepoint + 1)).to.be.bignumber.equal( + '9999999999999999999999980', + ); + expect(await this.token.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + expect(await this.token.getPastVotes(other1, t4.timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + }); + }); }); - it('rejects expired permit', async function () { - const expiry = (await time.latest()) - time.duration.weeks(1); + describe('getPastTotalSupply', function () { + beforeEach(async function () { + await this.token.delegate(holder, { from: holder }); + }); - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry, - }) - .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) - .then(fromRpcSig); + it('reverts if block number >= current block', async function () { + await expectRevert(this.token.getPastTotalSupply(5e10), 'ERC20Votes: future lookup'); + }); - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), - 'ERC20Votes: signature expired', - ); + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + const { receipt } = await this.token.$_mint(holder, supply); + const timepoint = await clockFromReceipt[mode](receipt); + + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(timepoint)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(supply); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const { receipt } = await this.token.$_mint(holder, supply); + const timepoint = await clockFromReceipt[mode](receipt); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.$_mint(holder, supply); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.$_burn(holder, 10); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.$_burn(holder, 10); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.$_mint(holder, 20); + await time.advanceBlock(); + await time.advanceBlock(); + + t1.timepoint = await clockFromReceipt[mode](t1.receipt); + t2.timepoint = await clockFromReceipt[mode](t2.receipt); + t3.timepoint = await clockFromReceipt[mode](t3.receipt); + t4.timepoint = await clockFromReceipt[mode](t4.receipt); + + expect(await this.token.getPastTotalSupply(t1.timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + expect(await this.token.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal( + '9999999999999999999999990', + ); + expect(await this.token.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal( + '9999999999999999999999980', + ); + expect(await this.token.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + }); }); }); - }); - - describe('change delegation', function () { - beforeEach(async function () { - await this.token.$_mint(holder, supply); - await this.token.delegate(holder, { from: holder }); - }); - - it('call', async function () { - expect(await this.token.delegates(holder)).to.be.equal(holder); - - const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: holder, - toDelegate: holderDelegatee, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: '0', - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holderDelegatee, - previousBalance: '0', - newBalance: supply, - }); - - expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); - - expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); - expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); - }); - }); - - describe('transfers', function () { - beforeEach(async function () { - await this.token.$_mint(holder, supply); - }); - - it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); - - this.holderVotes = '0'; - this.recipientVotes = '0'; - }); - - it('sender delegation', async function () { - await this.token.delegate(holder, { from: holder }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: supply.subn(1), - }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.holderVotes = supply.subn(1); - this.recipientVotes = '0'; - }); - - it('receiver delegation', async function () { - await this.token.delegate(recipient, { from: recipient }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.holderVotes = '0'; - this.recipientVotes = '1'; - }); - - it('full delegation', async function () { - await this.token.delegate(holder, { from: holder }); - await this.token.delegate(recipient, { from: recipient }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: supply.subn(1), - }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.holderVotes = supply.subn(1); - this.recipientVotes = '1'; - }); - - afterEach(async function () { - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); - - // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" - const blockNumber = await time.latestBlock(); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getPastVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes); - }); - }); - - // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. - describe('Compound test suite', function () { - beforeEach(async function () { - await this.token.$_mint(holder, supply); - }); - - describe('balanceOf', function () { - it('grants to initial account', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); - }); - }); - - describe('numCheckpoints', function () { - it('returns the number of checkpoints for a delegate', async function () { - await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const t1 = await this.token.delegate(other1, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - - const t2 = await this.token.transfer(other2, 10, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transfer(other2, 10, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.receipt.blockNumber.toString(), '100']); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.receipt.blockNumber.toString(), '90']); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.receipt.blockNumber.toString(), '80']); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.receipt.blockNumber.toString(), '100']); - - await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); - }); - - it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, '100', { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const [t1, t2, t3] = await batchInBlock([ - () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - ]); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.receipt.blockNumber.toString(), '80']); - // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.receipt.blockNumber.toString(), '100']); - }); - }); - - describe('getPastVotes', function () { - it('reverts if block number >= current block', async function () { - await expectRevert(this.token.getPastVotes(other1, 5e10), 'ERC20Votes: block not yet mined'); - }); - - it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0'); - }); - - it('returns the latest block if >= last checkpoint block', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - - it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 20, { from: other2 }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - }); - }); - - describe('getPastTotalSupply', function () { - beforeEach(async function () { - await this.token.delegate(holder, { from: holder }); - }); - - it('reverts if block number >= current block', async function () { - await expectRevert(this.token.getPastTotalSupply(5e10), 'ERC20Votes: block not yet mined'); - }); - - it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); - }); - - it('returns the latest block if >= last checkpoint block', async function () { - t1 = await this.token.$_mint(holder, supply); - - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); - }); - - it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const t1 = await this.token.$_mint(holder, supply); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.$_mint(holder, supply); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.$_burn(holder, 10); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.$_burn(holder, 10); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.$_mint(holder, 20); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - }); + } }); diff --git a/test/token/ERC20/extensions/ERC20VotesComp.test.js b/test/token/ERC20/extensions/ERC20VotesComp.test.js index 660ed2124..5f95722ad 100644 --- a/test/token/ERC20/extensions/ERC20VotesComp.test.js +++ b/test/token/ERC20/extensions/ERC20VotesComp.test.js @@ -8,11 +8,11 @@ const { fromRpcSig } = require('ethereumjs-util'); const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; -const ERC20VotesComp = artifacts.require('$ERC20VotesComp'); - const { batchInBlock } = require('../../../helpers/txpool'); const { getDomain, domainType, domainSeparator } = require('../../../helpers/eip712'); -const { getChainId } = require('../../../helpers/chainid'); +const { clock, clockFromReceipt } = require('../../../helpers/time'); + +const { shouldBehaveLikeEIP6372 } = require('../../../governance/utils/EIP6372.behavior'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -20,507 +20,524 @@ const Delegation = [ { name: 'expiry', type: 'uint256' }, ]; +const MODES = { + blocknumber: artifacts.require('$ERC20VotesComp'), + // no timestamp mode for ERC20VotesComp yet +}; + contract('ERC20VotesComp', function (accounts) { const [holder, recipient, holderDelegatee, other1, other2] = accounts; const name = 'My Token'; const symbol = 'MTKN'; - const version = '1'; const supply = new BN('10000000000000000000000000'); - beforeEach(async function () { - this.chainId = await getChainId(); - this.token = await ERC20VotesComp.new(name, symbol, name); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator)); - }); - - it('minting restriction', async function () { - const amount = new BN('2').pow(new BN('96')); - await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes'); - }); - - describe('set delegation', function () { - describe('call', function () { - it('delegation with balance', async function () { - await this.token.$_mint(holder, supply); - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: '0', - newBalance: supply, - }); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - - expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(supply); - expect(await this.token.getPriorVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPriorVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); - }); - - it('delegation without balance', async function () { - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - }); - }); - - describe('with signature', function () { - const delegator = Wallet.generate(); - const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); - const nonce = 0; - - const buildData = (contract, message) => - getDomain(contract).then(domain => ({ - primaryType: 'Delegation', - types: { EIP712Domain: domainType(domain), Delegation }, - domain, - message, - })); - + for (const [mode, artifact] of Object.entries(MODES)) { + describe(`vote with ${mode}`, function () { beforeEach(async function () { - await this.token.$_mint(delegatorAddress, supply); + this.token = await artifact.new(name, symbol, name); }); - it('accept signed delegation', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }) - .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) - .then(fromRpcSig); + shouldBehaveLikeEIP6372(mode); - expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); + it('initial nonce is 0', async function () { + expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); + }); - const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - expectEvent(receipt, 'DelegateChanged', { - delegator: delegatorAddress, - fromDelegate: ZERO_ADDRESS, - toDelegate: delegatorAddress, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: delegatorAddress, - previousBalance: '0', - newBalance: supply, + it('domain separator', async function () { + expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator)); + }); + + it('minting restriction', async function () { + const amount = new BN('2').pow(new BN('96')); + await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes'); + }); + + describe('set delegation', function () { + describe('call', function () { + it('delegation with balance', async function () { + await this.token.$_mint(holder, supply); + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + const timepoint = await clockFromReceipt[mode](receipt); + + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + + expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(supply); + expect(await this.token.getPriorVotes(holder, timepoint - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal(supply); + }); + + it('delegation without balance', async function () { + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + }); }); - expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); + describe('with signature', function () { + const delegator = Wallet.generate(); + const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); + const nonce = 0; - expect(await this.token.getCurrentVotes(delegatorAddress)).to.be.bignumber.equal(supply); - expect(await this.token.getPriorVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPriorVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); + const buildData = (contract, message) => + getDomain(contract).then(domain => ({ + primaryType: 'Delegation', + types: { EIP712Domain: domainType(domain), Delegation }, + domain, + message, + })); + + beforeEach(async function () { + await this.token.$_mint(delegatorAddress, supply); + }); + + it('accept signed delegation', async function () { + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + const timepoint = await clockFromReceipt[mode](receipt); + + expectEvent(receipt, 'DelegateChanged', { + delegator: delegatorAddress, + fromDelegate: ZERO_ADDRESS, + toDelegate: delegatorAddress, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: delegatorAddress, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); + + expect(await this.token.getCurrentVotes(delegatorAddress)).to.be.bignumber.equal(supply); + expect(await this.token.getPriorVotes(delegatorAddress, timepoint - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPriorVotes(delegatorAddress, timepoint)).to.be.bignumber.equal(supply); + }); + + it('rejects reused signature', async function () { + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); + + await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), + 'ERC20Votes: invalid nonce', + ); + }); + + it('rejects bad delegatee', async function () { + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); + + const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); + const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged'); + expect(args.delegator).to.not.be.equal(delegatorAddress); + expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); + expect(args.toDelegate).to.be.equal(holderDelegatee); + }); + + it('rejects bad nonce', async function () { + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), + 'ERC20Votes: invalid nonce', + ); + }); + + it('rejects expired permit', async function () { + const expiry = (await time.latest()) - time.duration.weeks(1); + const { v, r, s } = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry, + }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); + + await expectRevert( + this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), + 'ERC20Votes: signature expired', + ); + }); + }); }); - it('rejects reused signature', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }) - .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) - .then(fromRpcSig); + describe('change delegation', function () { + beforeEach(async function () { + await this.token.$_mint(holder, supply); + await this.token.delegate(holder, { from: holder }); + }); - await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + it('call', async function () { + expect(await this.token.delegates(holder)).to.be.equal(holder); - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), - 'ERC20Votes: invalid nonce', - ); + const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); + const timepoint = await clockFromReceipt[mode](receipt); + + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: holder, + toDelegate: holderDelegatee, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: '0', + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holderDelegatee, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); + + expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal('0'); + expect(await this.token.getCurrentVotes(holderDelegatee)).to.be.bignumber.equal(supply); + expect(await this.token.getPriorVotes(holder, timepoint - 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPriorVotes(holderDelegatee, timepoint - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal('0'); + expect(await this.token.getPriorVotes(holderDelegatee, timepoint)).to.be.bignumber.equal(supply); + }); }); - it('rejects bad delegatee', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }) - .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) - .then(fromRpcSig); + describe('transfers', function () { + beforeEach(async function () { + await this.token.$_mint(holder, supply); + }); - const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); - const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged'); - expect(args.delegator).to.not.be.equal(delegatorAddress); - expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); - expect(args.toDelegate).to.be.equal(holderDelegatee); + it('no delegation', async function () { + const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + + this.holderVotes = '0'; + this.recipientVotes = '0'; + }); + + it('sender delegation', async function () { + await this.token.delegate(holder, { from: holder }); + + const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: supply.subn(1), + }); + + this.holderVotes = supply.subn(1); + this.recipientVotes = '0'; + }); + + it('receiver delegation', async function () { + await this.token.delegate(recipient, { from: recipient }); + + const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); + + this.holderVotes = '0'; + this.recipientVotes = '1'; + }); + + it('full delegation', async function () { + await this.token.delegate(holder, { from: holder }); + await this.token.delegate(recipient, { from: recipient }); + + const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: supply.subn(1), + }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); + + this.holderVotes = supply.subn(1); + this.recipientVotes = '1'; + }); + + afterEach(async function () { + expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getCurrentVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); + + // need to advance 2 blocks to see the effect of a transfer on "getPriorVotes" + const timepoint = await clock[mode](); + await time.advanceBlock(); + expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getPriorVotes(recipient, timepoint)).to.be.bignumber.equal(this.recipientVotes); + }); }); - it('rejects bad nonce', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }) - .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) - .then(fromRpcSig); + // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. + describe('Compound test suite', function () { + beforeEach(async function () { + await this.token.$_mint(holder, supply); + }); - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), - 'ERC20Votes: invalid nonce', - ); + describe('balanceOf', function () { + it('grants to initial account', async function () { + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); + + describe('numCheckpoints', function () { + it('returns the number of checkpoints for a delegate', async function () { + await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const t1 = await this.token.delegate(other1, { from: recipient }); + t1.timepoint = await clockFromReceipt[mode](t1.receipt); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + + const t2 = await this.token.transfer(other2, 10, { from: recipient }); + t2.timepoint = await clockFromReceipt[mode](t2.receipt); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + + const t3 = await this.token.transfer(other2, 10, { from: recipient }); + t3.timepoint = await clockFromReceipt[mode](t3.receipt); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + t4.timepoint = await clockFromReceipt[mode](t4.receipt); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); + + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '100']); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.timepoint.toString(), '90']); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.timepoint.toString(), '80']); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.timepoint.toString(), '100']); + + await time.advanceBlock(); + expect(await this.token.getPriorVotes(other1, t1.timepoint)).to.be.bignumber.equal('100'); + expect(await this.token.getPriorVotes(other1, t2.timepoint)).to.be.bignumber.equal('90'); + expect(await this.token.getPriorVotes(other1, t3.timepoint)).to.be.bignumber.equal('80'); + expect(await this.token.getPriorVotes(other1, t4.timepoint)).to.be.bignumber.equal('100'); + }); + + it('does not add more than one checkpoint in a block', async function () { + await this.token.transfer(recipient, '100', { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const [t1, t2, t3] = await batchInBlock([ + () => this.token.delegate(other1, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), + ]); + t1.timepoint = await clockFromReceipt[mode](t1.receipt); + t2.timepoint = await clockFromReceipt[mode](t2.receipt); + t3.timepoint = await clockFromReceipt[mode](t3.receipt); + + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '80']); + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + t4.timepoint = await clockFromReceipt[mode](t4.receipt); + + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.timepoint.toString(), '100']); + }); + }); + + describe('getPriorVotes', function () { + it('reverts if block number >= current block', async function () { + await expectRevert(this.token.getPriorVotes(other1, 5e10), 'ERC20Votes: future lookup'); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPriorVotes(other1, 0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + const { receipt } = await this.token.delegate(other1, { from: holder }); + const timepoint = await clockFromReceipt[mode](receipt); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPriorVotes(other1, timepoint)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + expect(await this.token.getPriorVotes(other1, timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const { receipt } = await this.token.delegate(other1, { from: holder }); + const timepoint = await clockFromReceipt[mode](receipt); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPriorVotes(other1, timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPriorVotes(other1, timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.transfer(holder, 20, { from: other2 }); + await time.advanceBlock(); + await time.advanceBlock(); + + t1.timepoint = await clockFromReceipt[mode](t1.receipt); + t2.timepoint = await clockFromReceipt[mode](t2.receipt); + t3.timepoint = await clockFromReceipt[mode](t3.receipt); + t4.timepoint = await clockFromReceipt[mode](t4.receipt); + + expect(await this.token.getPriorVotes(other1, t1.timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPriorVotes(other1, t1.timepoint)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + expect(await this.token.getPriorVotes(other1, t1.timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + expect(await this.token.getPriorVotes(other1, t2.timepoint)).to.be.bignumber.equal( + '9999999999999999999999990', + ); + expect(await this.token.getPriorVotes(other1, t2.timepoint + 1)).to.be.bignumber.equal( + '9999999999999999999999990', + ); + expect(await this.token.getPriorVotes(other1, t3.timepoint)).to.be.bignumber.equal( + '9999999999999999999999980', + ); + expect(await this.token.getPriorVotes(other1, t3.timepoint + 1)).to.be.bignumber.equal( + '9999999999999999999999980', + ); + expect(await this.token.getPriorVotes(other1, t4.timepoint)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + expect(await this.token.getPriorVotes(other1, t4.timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + }); + }); }); - it('rejects expired permit', async function () { - const expiry = (await time.latest()) - time.duration.weeks(1); + describe('getPastTotalSupply', function () { + beforeEach(async function () { + await this.token.delegate(holder, { from: holder }); + }); - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry, - }) - .then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })) - .then(fromRpcSig); + it('reverts if block number >= current block', async function () { + await expectRevert(this.token.getPastTotalSupply(5e10), 'ERC20Votes: future lookup'); + }); - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), - 'ERC20Votes: signature expired', - ); + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + const { receipt } = await this.token.$_mint(holder, supply); + const timepoint = await clockFromReceipt[mode](receipt); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(timepoint)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(supply); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const { receipt } = await this.token.$_mint(holder, supply); + const timepoint = await clockFromReceipt[mode](receipt); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.$_mint(holder, supply); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.$_burn(holder, 10); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.$_burn(holder, 10); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.$_mint(holder, 20); + await time.advanceBlock(); + await time.advanceBlock(); + + t1.timepoint = await clockFromReceipt[mode](t1.receipt); + t2.timepoint = await clockFromReceipt[mode](t2.receipt); + t3.timepoint = await clockFromReceipt[mode](t3.receipt); + t4.timepoint = await clockFromReceipt[mode](t4.receipt); + + expect(await this.token.getPastTotalSupply(t1.timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + expect(await this.token.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal( + '9999999999999999999999990', + ); + expect(await this.token.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal( + '9999999999999999999999980', + ); + expect(await this.token.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + }); }); }); - }); - - describe('change delegation', function () { - beforeEach(async function () { - await this.token.$_mint(holder, supply); - await this.token.delegate(holder, { from: holder }); - }); - - it('call', async function () { - expect(await this.token.delegates(holder)).to.be.equal(holder); - - const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: holder, - toDelegate: holderDelegatee, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: '0', - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holderDelegatee, - previousBalance: '0', - newBalance: supply, - }); - - expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); - - expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal('0'); - expect(await this.token.getCurrentVotes(holderDelegatee)).to.be.bignumber.equal(supply); - expect(await this.token.getPriorVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); - expect(await this.token.getPriorVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPriorVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPriorVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); - }); - }); - - describe('transfers', function () { - beforeEach(async function () { - await this.token.$_mint(holder, supply); - }); - - it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); - - this.holderVotes = '0'; - this.recipientVotes = '0'; - }); - - it('sender delegation', async function () { - await this.token.delegate(holder, { from: holder }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: supply.subn(1), - }); - - this.holderVotes = supply.subn(1); - this.recipientVotes = '0'; - }); - - it('receiver delegation', async function () { - await this.token.delegate(recipient, { from: recipient }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); - - this.holderVotes = '0'; - this.recipientVotes = '1'; - }); - - it('full delegation', async function () { - await this.token.delegate(holder, { from: holder }); - await this.token.delegate(recipient, { from: recipient }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: supply.subn(1), - }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); - - this.holderVotes = supply.subn(1); - this.recipientVotes = '1'; - }); - - afterEach(async function () { - expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getCurrentVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); - - // need to advance 2 blocks to see the effect of a transfer on "getPriorVotes" - const blockNumber = await time.latestBlock(); - await time.advanceBlock(); - expect(await this.token.getPriorVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getPriorVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes); - }); - }); - - // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. - describe('Compound test suite', function () { - beforeEach(async function () { - await this.token.$_mint(holder, supply); - }); - - describe('balanceOf', function () { - it('grants to initial account', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); - }); - }); - - describe('numCheckpoints', function () { - it('returns the number of checkpoints for a delegate', async function () { - await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const t1 = await this.token.delegate(other1, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - - const t2 = await this.token.transfer(other2, 10, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transfer(other2, 10, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.receipt.blockNumber.toString(), '100']); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.receipt.blockNumber.toString(), '90']); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.receipt.blockNumber.toString(), '80']); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.receipt.blockNumber.toString(), '100']); - - await time.advanceBlock(); - expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); - expect(await this.token.getPriorVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); - expect(await this.token.getPriorVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); - expect(await this.token.getPriorVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); - }); - - it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, '100', { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const [t1, t2, t3] = await batchInBlock([ - () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - ]); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.receipt.blockNumber.toString(), '80']); - // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.receipt.blockNumber.toString(), '100']); - }); - }); - - describe('getPriorVotes', function () { - it('reverts if block number >= current block', async function () { - await expectRevert(this.token.getPriorVotes(other1, 5e10), 'ERC20Votes: block not yet mined'); - }); - - it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPriorVotes(other1, 0)).to.be.bignumber.equal('0'); - }); - - it('returns the latest block if >= last checkpoint block', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - - it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 20, { from: other2 }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPriorVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPriorVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPriorVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPriorVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPriorVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPriorVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - }); - }); - - describe('getPastTotalSupply', function () { - beforeEach(async function () { - await this.token.delegate(holder, { from: holder }); - }); - - it('reverts if block number >= current block', async function () { - await expectRevert(this.token.getPastTotalSupply(5e10), 'ERC20Votes: block not yet mined'); - }); - - it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); - }); - - it('returns the latest block if >= last checkpoint block', async function () { - t1 = await this.token.$_mint(holder, supply); - - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); - }); - - it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const t1 = await this.token.$_mint(holder, supply); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.$_mint(holder, supply); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.$_burn(holder, 10); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.$_burn(holder, 10); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.$_mint(holder, 20); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - }); + } }); diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index f7baef4c1..92032db87 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -178,6 +178,7 @@ contract('ERC721Votes', function (accounts) { this.name = 'My Vote'; }); + // includes EIP6372 behavior check shouldBehaveLikeVotes(); }); }); diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js index d43d469cc..48a00e31a 100644 --- a/test/utils/Checkpoints.test.js +++ b/test/utils/Checkpoints.test.js @@ -117,8 +117,10 @@ contract('Checkpoints', function () { const latestCheckpoint = (self, ...args) => self.methods[`$latestCheckpoint_Checkpoints_Trace${length}(uint256)`](0, ...args); const push = (self, ...args) => self.methods[`$push(uint256,uint${256 - length},uint${length})`](0, ...args); - const upperLookup = (self, ...args) => self.methods[`$upperLookup(uint256,uint${256 - length})`](0, ...args); const lowerLookup = (self, ...args) => self.methods[`$lowerLookup(uint256,uint${256 - length})`](0, ...args); + const upperLookup = (self, ...args) => self.methods[`$upperLookup(uint256,uint${256 - length})`](0, ...args); + const upperLookupRecent = (self, ...args) => + self.methods[`$upperLookupRecent(uint256,uint${256 - length})`](0, ...args); const getLength = (self, ...args) => self.methods[`$length_Checkpoints_Trace${length}(uint256)`](0, ...args); describe('without checkpoints', function () { @@ -134,6 +136,7 @@ contract('Checkpoints', function () { it('lookup returns 0', async function () { expect(await lowerLookup(this.mock, 0)).to.be.bignumber.equal('0'); expect(await upperLookup(this.mock, 0)).to.be.bignumber.equal('0'); + expect(await upperLookupRecent(this.mock, 0)).to.be.bignumber.equal('0'); }); }); @@ -190,11 +193,33 @@ contract('Checkpoints', function () { } }); - it('upper lookup', async function () { + it('upper lookup & upperLookupRecent', async function () { for (let i = 0; i < 14; ++i) { const value = last(this.checkpoints.filter(x => i >= x.key))?.value || '0'; expect(await upperLookup(this.mock, i)).to.be.bignumber.equal(value); + expect(await upperLookupRecent(this.mock, i)).to.be.bignumber.equal(value); + } + }); + + it('upperLookupRecent with more than 5 checkpoints', async function () { + const moreCheckpoints = [ + { key: '12', value: '22' }, + { key: '13', value: '131' }, + { key: '17', value: '45' }, + { key: '19', value: '31452' }, + { key: '21', value: '0' }, + ]; + const allCheckpoints = [].concat(this.checkpoints, moreCheckpoints); + + for (const { key, value } of moreCheckpoints) { + await push(this.mock, key, value); + } + + for (let i = 0; i < 25; ++i) { + const value = last(allCheckpoints.filter(x => i >= x.key))?.value || '0'; + expect(await upperLookup(this.mock, i)).to.be.bignumber.equal(value); + expect(await upperLookupRecent(this.mock, i)).to.be.bignumber.equal(value); } }); }); From 6259e13a0c92547646f1dd1764583e86bb997441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 9 Feb 2023 15:36:14 -0600 Subject: [PATCH 030/197] Improve ERC721 Wrapper tests (#4039) --- test/token/ERC721/extensions/ERC721Wrapper.test.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/token/ERC721/extensions/ERC721Wrapper.test.js b/test/token/ERC721/extensions/ERC721Wrapper.test.js index 5c83d73cc..def6ca7ee 100644 --- a/test/token/ERC721/extensions/ERC721Wrapper.test.js +++ b/test/token/ERC721/extensions/ERC721Wrapper.test.js @@ -196,11 +196,21 @@ contract('ERC721Wrapper', function (accounts) { to: initialHolder, tokenId: firstTokenId, }); + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: this.token.address, + to: initialHolder, + tokenId: secondTokenId, + }); await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: initialHolder, to: constants.ZERO_ADDRESS, tokenId: firstTokenId, }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: initialHolder, + to: constants.ZERO_ADDRESS, + tokenId: secondTokenId, + }); }); it('works to another account', async function () { From dfcc1d16c5efd0fd2a7abac56680810c861a9cd3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Feb 2023 18:42:08 -0300 Subject: [PATCH 031/197] Bump http-cache-semantics from 4.1.0 to 4.1.1 (#4024) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index a663055fd..4f808cf30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8426,9 +8426,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, "node_modules/http-errors": { @@ -22946,9 +22946,9 @@ } }, "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, "http-errors": { From 5a00628ed3d6ce3154cee4d2cc93fad920e8ea30 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Feb 2023 15:21:45 -0300 Subject: [PATCH 032/197] Update getsentry/action-github-app-token action to v2 (#4036) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/upgradeable.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/upgradeable.yml b/.github/workflows/upgradeable.yml index a94f78c98..a7ed8da88 100644 --- a/.github/workflows/upgradeable.yml +++ b/.github/workflows/upgradeable.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - id: app - uses: getsentry/action-github-app-token@v1 + uses: getsentry/action-github-app-token@v2 with: app_id: ${{ secrets.UPGRADEABLE_APP_ID }} private_key: ${{ secrets.UPGRADEABLE_APP_PK }} From 2c711d0b0513d283b27a3b28df06d558a19c3aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 14 Feb 2023 14:47:07 -0600 Subject: [PATCH 033/197] Restrict ERC721Wrapper wrap by direct transfer (#4043) Co-authored-by: Hadrien Croubois --- .../token/ERC721/extensions/ERC721Wrapper.sol | 17 +++----- .../ERC721/extensions/ERC721Wrapper.test.js | 39 ++----------------- 2 files changed, 9 insertions(+), 47 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Wrapper.sol b/contracts/token/ERC721/extensions/ERC721Wrapper.sol index 87729188a..f10d55564 100644 --- a/contracts/token/ERC721/extensions/ERC721Wrapper.sol +++ b/contracts/token/ERC721/extensions/ERC721Wrapper.sol @@ -17,10 +17,6 @@ import "../utils/ERC721Holder.sol"; abstract contract ERC721Wrapper is ERC721, ERC721Holder { IERC721 private immutable _underlying; - // Kept as bytes12 so it can be packed with an address - // Equal to 0xb125e89df18e2ceac5fd2fa8 - bytes12 public constant WRAPPER_ACCEPT_MAGIC = bytes12(keccak256("WRAPPER_ACCEPT_MAGIC")); - constructor(IERC721 underlyingToken) { _underlying = underlyingToken; } @@ -29,7 +25,7 @@ abstract contract ERC721Wrapper is ERC721, ERC721Holder { * @dev Allow a user to deposit underlying tokens and mint the corresponding tokenIds. */ function depositFor(address account, uint256[] memory tokenIds) public virtual returns (bool) { - bytes memory data = abi.encodePacked(WRAPPER_ACCEPT_MAGIC, account); + bytes memory data = abi.encodePacked(account); uint256 length = tokenIds.length; for (uint256 i = 0; i < length; ++i) { @@ -61,23 +57,22 @@ abstract contract ERC721Wrapper is ERC721, ERC721Holder { * @dev Overrides {IERC721Receiver-onERC721Received} to allow minting on direct ERC721 transfers to * this contract. * - * In case there's data attached, it validates that the sender is aware of this contract's existence and behavior - * by checking a magic value (`WRAPPER_ACCEPT_MAGIC`) in the first 12 bytes. If it also matches, the rest 20 - * bytes are used as an address to send the tokens to. + * In case there's data attached, it validates that the operator is this contract, so only trusted data + * is accepted from {depositFor}. * * WARNING: Doesn't work with unsafe transfers (eg. {IERC721-transferFrom}). Use {ERC721Wrapper-_recover} * for recovering in that scenario. */ function onERC721Received( - address, + address operator, address from, uint256 tokenId, bytes memory data ) public override returns (bytes4) { require(address(underlying()) == _msgSender(), "ERC721Wrapper: caller is not underlying"); if (data.length > 0) { - require(data.length == 32 && WRAPPER_ACCEPT_MAGIC == bytes12(data), "ERC721Wrapper: Invalid data format"); - from = address(bytes20(bytes32(data) << 96)); + require(data.length == 20 && operator == address(this), "ERC721Wrapper: Invalid data format"); + from = address(bytes20(data)); } _safeMint(from, tokenId); return IERC721Receiver.onERC721Received.selector; diff --git a/test/token/ERC721/extensions/ERC721Wrapper.test.js b/test/token/ERC721/extensions/ERC721Wrapper.test.js index def6ca7ee..0558dfa37 100644 --- a/test/token/ERC721/extensions/ERC721Wrapper.test.js +++ b/test/token/ERC721/extensions/ERC721Wrapper.test.js @@ -1,6 +1,5 @@ const { BN, expectEvent, constants, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); -const { keccakFromString, bufferToHex } = require('ethereumjs-util'); const { shouldBehaveLikeERC721 } = require('../ERC721.behavior'); @@ -230,27 +229,13 @@ contract('ERC721Wrapper', function (accounts) { }); describe('onERC721Received', function () { - const WRAPPER_ACCEPT_MAGIC = bufferToHex(keccakFromString('WRAPPER_ACCEPT_MAGIC')).slice(0, 26); // Include 0x - - const magicWithAddresss = address => - web3.utils.encodePacked( - { - value: WRAPPER_ACCEPT_MAGIC, - type: 'bytes12', - }, - { - value: address, - type: 'address', - }, - ); - it('only allows calls from underlying', async function () { await expectRevert( this.token.onERC721Received( initialHolder, this.token.address, firstTokenId, - magicWithAddresss(anotherAccount), // Correct data + anotherAccount, // Correct data { from: anotherAccount }, ), 'ERC721Wrapper: caller is not underlying', @@ -273,13 +258,13 @@ contract('ERC721Wrapper', function (accounts) { ); }); - it('reverts with the magic value and data length different to 32', async function () { + it('reverts with correct data from an untrusted operator', async function () { await expectRevert( this.underlying.methods['safeTransferFrom(address,address,uint256,bytes)']( initialHolder, this.token.address, firstTokenId, - WRAPPER_ACCEPT_MAGIC, // Reverts for any non-32 bytes value + anotherAccount, { from: initialHolder, }, @@ -287,24 +272,6 @@ contract('ERC721Wrapper', function (accounts) { 'ERC721Wrapper: Invalid data format', ); }); - - it('mints token to specific holder with address after magic value', async function () { - const { tx } = await this.underlying.methods['safeTransferFrom(address,address,uint256,bytes)']( - initialHolder, - this.token.address, - firstTokenId, - magicWithAddresss(anotherAccount), - { - from: initialHolder, - }, - ); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: anotherAccount, - tokenId: firstTokenId, - }); - }); }); it('mints a token to from if no data is specified', async function () { From 6d18435098c797bc8c3cfbf51fe10d450a2f6595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 14 Feb 2023 17:03:37 -0600 Subject: [PATCH 034/197] Add `RELEASING.md` docs (#3981) Co-authored-by: Francisco Giordano --- .github/workflows/release-cycle.yml | 12 +++++++ CONTRIBUTING.md | 2 ++ RELEASING.md | 55 +++++++++++++++++------------ 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/.github/workflows/release-cycle.yml b/.github/workflows/release-cycle.yml index 4fe09a4eb..1831bd519 100644 --- a/.github/workflows/release-cycle.yml +++ b/.github/workflows/release-cycle.yml @@ -1,3 +1,15 @@ +# D: Manual Dispatch +# M: Merge release PR +# C: Commit +# ┌───────────┐ ┌─────────────┐ ┌────────────────┐ +# │Development├──D──►RC-Unreleased│ ┌──►Final-Unreleased│ +# └───────────┘ └─┬─────────▲─┘ │ └─┬────────────▲─┘ +# │ │ │ │ │ +# M C D M C +# │ │ │ │ │ +# ┌▼─────────┴┐ │ ┌▼────────────┴┐ +# │RC-Released├───┘ │Final-Released│ +# └───────────┘ └──────────────┘ name: Release Cycle on: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ecbc2d400..1a44cc27d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,8 @@ Any non-trivial code contribution must be first discussed with the maintainers i Make sure to read and follow the [engineering guidelines](./GUIDELINES.md). Run linter and tests to make sure your pull request is good before submitting it. +Changelog entries should be added to each pull request by using [Changesets](https://github.com/changesets/changesets/). + When opening the pull request you will be presented with a template and a series of instructions. Read through it carefully and follow all the steps. Expect a review and feedback from the maintainers afterwards. If you're looking for a good place to start, look for issues labelled ["good first issue"](https://github.com/OpenZeppelin/openzeppelin-contracts/labels/good%20first%20issue)! diff --git a/RELEASING.md b/RELEASING.md index f356ab2e4..0318f6338 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,36 +1,47 @@ # Releasing -> Visit the documentation for [details about release schedule]. +> Visit the documentation for [details about release schedule](https://docs.openzeppelin.com/contracts/releases-stability). -Start on an up-to-date `master` branch. +OpenZeppelin Contracts uses a fully automated release process that takes care of compiling, packaging, and publishing the library, all of which is carried out in a clean CI environment (GitHub Actions), implemented in the ([`release-cycle`](.github/workflows/release-cycle.yml)) workflow. This helps to reduce the potential for human error and inconsistencies, and ensures that the release process is ongoing and reliable. -Create the release branch with `npm run release start minor`. +## Changesets -Publish a release candidate with `npm run release rc`. +[Changesets](https://github.com/changesets/changesets/) is used as part of our release process for `CHANGELOG.md` management. Each change that is relevant for the codebase is expected to include a changeset. -Publish the final release with `npm run release final`. +## Branching model -Follow the general [OpenZeppelin Contracts release checklist]. +The release cycle happens on release branches called `release-vX.Y`. Each of these branches starts as a release candidate (rc) and is eventually promoted to final. -[details about release schedule]: https://docs.openzeppelin.com/contracts/releases-stability -[OpenZeppelin Contracts release checklist]: https://github.com/OpenZeppelin/code-style/blob/master/RELEASE_CHECKLIST.md +A release branch can be updated with cherry-picked patches from `master`, or may sometimes be committed to directly in the case of old releases. These commits will lead to a new release candidate or a patch increment depending on the state of the release branch. +```mermaid + %%{init: {'gitGraph': {'mainBranchName': 'master'}} }%% + gitGraph + commit id: "Feature A" + commit id: "Feature B" + branch release-vX.Y + commit id: "Start release" + commit id: "Release vX.Y.0-rc.0" -## Merging the release branch + checkout master + commit id: "Feature C" + commit id: "Fix A" -After the final release, the release branch should be merged back into `master`. This merge must not be squashed because it would lose the tagged release commit. Since the GitHub repo is set up to only allow squashed merges, the merge should be done locally and pushed. + checkout release-vX.Y + cherry-pick id: "Fix A" tag: "" + commit id: "Release vX.Y.0-rc.1" + commit id: "Release vX.Y.0" -Make sure to have the latest changes from `upstream` in your local release branch. + checkout master + merge release-vX.Y + commit id: "Feature D" + commit id: "Patch B" + checkout release-vX.Y + cherry-pick id: "Patch B" tag: "" + commit id: "Release vX.Y.1" + + checkout master + merge release-vX.Y + commit id: "Feature E" ``` -git checkout release-vX.Y.Z -git pull upstream -``` - -``` -git checkout master -git merge --no-ff release-vX.Y.Z -git push upstream master -``` - -The release branch can then be deleted on GitHub. From 4e8aa43a900c6fa715c6f64f7c5b6d4d0c041c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 15 Feb 2023 15:21:29 -0600 Subject: [PATCH 035/197] Add publishing integrity check after releasing (#4045) Co-authored-by: Francisco --- .github/workflows/release-cycle.yml | 25 +++++++++++++++++++++ scripts/release/workflow/integrity-check.sh | 20 +++++++++++++++++ scripts/release/workflow/pack.sh | 1 + scripts/release/workflow/publish.sh | 2 +- 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 scripts/release/workflow/integrity-check.sh diff --git a/.github/workflows/release-cycle.yml b/.github/workflows/release-cycle.yml index 1831bd519..2fd66458d 100644 --- a/.github/workflows/release-cycle.yml +++ b/.github/workflows/release-cycle.yml @@ -142,6 +142,11 @@ jobs: run: bash scripts/release/workflow/pack.sh env: PRERELEASE: ${{ needs.state.outputs.is_prerelease }} + - name: Upload tarball artifact + uses: actions/upload-artifact@v3 + with: + name: ${{ github.ref_name }} + path: ${{ steps.pack.outputs.tarball }} - name: Tag run: npx changeset tag - name: Publish @@ -158,6 +163,26 @@ jobs: PRERELEASE: ${{ needs.state.outputs.is_prerelease }} with: script: await require('./scripts/release/workflow/github-release.js')({ github, context }) + outputs: + tarball_name: ${{ steps.pack.outputs.tarball_name }} + + integrity_check: + needs: publish + name: Tarball Integrity Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Download tarball artifact + id: artifact + # Replace with actions/upload-artifact@v3 when + # https://github.com/actions/download-artifact/pull/194 gets released + uses: actions/download-artifact@e9ef242655d12993efdcda9058dee2db83a2cb9b + with: + name: ${{ github.ref_name }} + - name: Check integrity + run: bash scripts/release/workflow/integrity-check.sh + env: + TARBALL: ${{ steps.artifact.outputs.download-path }}/${{ needs.publish.outputs.tarball_name }} merge: needs: state diff --git a/scripts/release/workflow/integrity-check.sh b/scripts/release/workflow/integrity-check.sh new file mode 100644 index 000000000..86e99f929 --- /dev/null +++ b/scripts/release/workflow/integrity-check.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -euo pipefail + +CHECKSUMS="$RUNNER_TEMP/checksums.txt" + +# Extract tarball content into a tmp directory +tar xf "$TARBALL" -C "$RUNNER_TEMP" + +# Move to extracted directory +cd "$RUNNER_TEMP/package" + +# Checksum all Solidity files +find . -type f -name "*.sol" | xargs shasum > "$CHECKSUMS" + +# Back to directory with git contents +cd "$GITHUB_WORKSPACE/contracts" + +# Check against tarball contents +shasum -c "$CHECKSUMS" diff --git a/scripts/release/workflow/pack.sh b/scripts/release/workflow/pack.sh index 798417d3d..ce30712f8 100644 --- a/scripts/release/workflow/pack.sh +++ b/scripts/release/workflow/pack.sh @@ -20,6 +20,7 @@ dist_tag() { cd contracts TARBALL="$(npm pack | tee /dev/stderr | tail -1)" +echo "tarball_name=$TARBALL" >> $GITHUB_OUTPUT echo "tarball=$(pwd)/$TARBALL" >> $GITHUB_OUTPUT echo "tag=$(dist_tag)" >> $GITHUB_OUTPUT cd .. diff --git a/scripts/release/workflow/publish.sh b/scripts/release/workflow/publish.sh index f9e2802d9..41a9975cb 100644 --- a/scripts/release/workflow/publish.sh +++ b/scripts/release/workflow/publish.sh @@ -15,6 +15,6 @@ delete_tag() { if [ "$TAG" = tmp ]; then delete_tag "$TAG" -elif ["$TAG" = latest ]; then +elif [ "$TAG" = latest ]; then delete_tag next fi From 4ff538af58455987a45243a5ecefca76544969cd Mon Sep 17 00:00:00 2001 From: Francisco Date: Wed, 15 Feb 2023 19:16:22 -0300 Subject: [PATCH 036/197] Fix flaky timestamp tests (#4046) --- .github/workflows/checks.yml | 1 + test/helpers/governance.js | 15 +++++++++------ test/helpers/time.js | 11 ++++++----- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 02664e85c..8bb32dcc3 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -26,6 +26,7 @@ jobs: runs-on: ubuntu-latest env: FORCE_COLOR: 1 + NODE_OPTIONS: --max_old_space_size=4096 GAS: true steps: - uses: actions/checkout@v3 diff --git a/test/helpers/governance.js b/test/helpers/governance.js index ae88e151a..1ffa086cb 100644 --- a/test/helpers/governance.js +++ b/test/helpers/governance.js @@ -115,19 +115,22 @@ class GovernorHelper { : this.governor.castVote(...concatOpts([proposal.id, vote.support], opts)); } - waitForSnapshot(offset = 0) { + async waitForSnapshot(offset = 0) { const proposal = this.currentProposal; - return this.governor.proposalSnapshot(proposal.id).then(timepoint => forward[this.mode](timepoint.addn(offset))); + const timepoint = await this.governor.proposalSnapshot(proposal.id); + return forward[this.mode](timepoint.addn(offset)); } - waitForDeadline(offset = 0) { + async waitForDeadline(offset = 0) { const proposal = this.currentProposal; - return this.governor.proposalDeadline(proposal.id).then(timepoint => forward[this.mode](timepoint.addn(offset))); + const timepoint = await this.governor.proposalDeadline(proposal.id); + return forward[this.mode](timepoint.addn(offset)); } - waitForEta(offset = 0) { + async waitForEta(offset = 0) { const proposal = this.currentProposal; - return this.governor.proposalEta(proposal.id).then(timestamp => forward.timestamp(timestamp.addn(offset))); + const timestamp = await this.governor.proposalEta(proposal.id); + return forward.timestamp(timestamp.addn(offset)); } /** diff --git a/test/helpers/time.js b/test/helpers/time.js index 2e5f6d85a..30df8dc32 100644 --- a/test/helpers/time.js +++ b/test/helpers/time.js @@ -1,16 +1,17 @@ -const { time } = require('@openzeppelin/test-helpers'); +const ozHelpers = require('@openzeppelin/test-helpers'); +const helpers = require('@nomicfoundation/hardhat-network-helpers'); module.exports = { clock: { - blocknumber: () => web3.eth.getBlock('latest').then(block => block.number), - timestamp: () => web3.eth.getBlock('latest').then(block => block.timestamp), + blocknumber: () => helpers.time.latestBlock(), + timestamp: () => helpers.time.latest(), }, clockFromReceipt: { blocknumber: receipt => Promise.resolve(receipt.blockNumber), timestamp: receipt => web3.eth.getBlock(receipt.blockNumber).then(block => block.timestamp), }, forward: { - blocknumber: time.advanceBlockTo, - timestamp: time.increaseTo, + blocknumber: ozHelpers.time.advanceBlockTo, + timestamp: helpers.time.increaseTo, }, }; From 5e76b2622546a2e42e5c19e4ce1a96ee2691c7fe Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 16 Feb 2023 14:33:56 -0300 Subject: [PATCH 037/197] Add Subgraphs to docs sidebar --- docs/modules/ROOT/nav.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 6604c2de5..fd206aacd 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -19,3 +19,5 @@ * xref:crosschain.adoc[Crosschain] * xref:utilities.adoc[Utilities] + +* xref:subgraphs::index.adoc[Subgraphs] From d5d9d4bd3db23444a06bd1a5e2f33fc81e143795 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 17 Feb 2023 03:35:43 +0100 Subject: [PATCH 038/197] Simplify ERC721Wrapper.depositFor to save gas (#4048) --- .../token/ERC721/extensions/ERC721Wrapper.sol | 23 ++++++------- .../ERC721/extensions/ERC721Wrapper.test.js | 34 +------------------ 2 files changed, 12 insertions(+), 45 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721Wrapper.sol b/contracts/token/ERC721/extensions/ERC721Wrapper.sol index f10d55564..83e59ce88 100644 --- a/contracts/token/ERC721/extensions/ERC721Wrapper.sol +++ b/contracts/token/ERC721/extensions/ERC721Wrapper.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; import "../ERC721.sol"; -import "../utils/ERC721Holder.sol"; /** * @dev Extension of the ERC721 token contract to support token wrapping. @@ -14,7 +13,7 @@ import "../utils/ERC721Holder.sol"; * * _Available since v4.9.0_ */ -abstract contract ERC721Wrapper is ERC721, ERC721Holder { +abstract contract ERC721Wrapper is ERC721, IERC721Receiver { IERC721 private immutable _underlying; constructor(IERC721 underlyingToken) { @@ -25,11 +24,15 @@ abstract contract ERC721Wrapper is ERC721, ERC721Holder { * @dev Allow a user to deposit underlying tokens and mint the corresponding tokenIds. */ function depositFor(address account, uint256[] memory tokenIds) public virtual returns (bool) { - bytes memory data = abi.encodePacked(account); - uint256 length = tokenIds.length; for (uint256 i = 0; i < length; ++i) { - underlying().safeTransferFrom(_msgSender(), address(this), tokenIds[i], data); + uint256 tokenId = tokenIds[i]; + + // This is an "unsafe" transfer that doesn't call any hook on the receiver. With underlying() being trusted + // (by design of this contract) and no other contracts expected to be called from there, we are safe. + // slither-disable-next-line reentrancy-no-eth + underlying().transferFrom(_msgSender(), address(this), tokenId); + _safeMint(account, tokenId); } return true; @@ -64,16 +67,12 @@ abstract contract ERC721Wrapper is ERC721, ERC721Holder { * for recovering in that scenario. */ function onERC721Received( - address operator, + address, address from, uint256 tokenId, - bytes memory data - ) public override returns (bytes4) { + bytes memory + ) public virtual override returns (bytes4) { require(address(underlying()) == _msgSender(), "ERC721Wrapper: caller is not underlying"); - if (data.length > 0) { - require(data.length == 20 && operator == address(this), "ERC721Wrapper: Invalid data format"); - from = address(bytes20(data)); - } _safeMint(from, tokenId); return IERC721Receiver.onERC721Received.selector; } diff --git a/test/token/ERC721/extensions/ERC721Wrapper.test.js b/test/token/ERC721/extensions/ERC721Wrapper.test.js index 0558dfa37..6e46d2e5a 100644 --- a/test/token/ERC721/extensions/ERC721Wrapper.test.js +++ b/test/token/ERC721/extensions/ERC721Wrapper.test.js @@ -242,39 +242,7 @@ contract('ERC721Wrapper', function (accounts) { ); }); - describe('when data length is > 0', function () { - it('reverts with arbitrary data', async function () { - await expectRevert( - this.underlying.methods['safeTransferFrom(address,address,uint256,bytes)']( - initialHolder, - this.token.address, - firstTokenId, - '0x0123', - { - from: initialHolder, - }, - ), - 'ERC721Wrapper: Invalid data format', - ); - }); - - it('reverts with correct data from an untrusted operator', async function () { - await expectRevert( - this.underlying.methods['safeTransferFrom(address,address,uint256,bytes)']( - initialHolder, - this.token.address, - firstTokenId, - anotherAccount, - { - from: initialHolder, - }, - ), - 'ERC721Wrapper: Invalid data format', - ); - }); - }); - - it('mints a token to from if no data is specified', async function () { + it('mints a token to from', async function () { const { tx } = await this.underlying.safeTransferFrom(initialHolder, this.token.address, firstTokenId, { from: initialHolder, }); From d64d7aa5d1dac7dd7bf60dae917e400120340002 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 17 Feb 2023 10:08:31 +0100 Subject: [PATCH 039/197] ERC4626 inflation attack mitigation (#3979) Co-authored-by: Francisco --- .changeset/perfect-insects-listen.md | 5 + contracts/mocks/token/ERC4626DecimalsMock.sol | 33 - contracts/mocks/token/ERC4626OffsetMock.sol | 17 + contracts/token/ERC20/extensions/ERC4626.sol | 75 +- .../modules/ROOT/images/erc4626-attack-3a.png | Bin 0 -> 60433 bytes .../modules/ROOT/images/erc4626-attack-3b.png | Bin 0 -> 66184 bytes docs/modules/ROOT/images/erc4626-attack-6.png | Bin 0 -> 56290 bytes docs/modules/ROOT/images/erc4626-attack.png | Bin 0 -> 58886 bytes docs/modules/ROOT/images/erc4626-deposit.png | Bin 0 -> 115497 bytes docs/modules/ROOT/images/erc4626-mint.png | Bin 0 -> 112787 bytes .../ROOT/images/erc4626-rate-linear.png | Bin 0 -> 50813 bytes .../ROOT/images/erc4626-rate-loglog.png | Bin 0 -> 72818 bytes .../ROOT/images/erc4626-rate-loglogext.png | Bin 0 -> 109923 bytes docs/modules/ROOT/nav.adoc | 1 + docs/modules/ROOT/pages/erc4626.adoc | 193 +++++ test/token/ERC20/extensions/ERC4626.t.sol | 18 - test/token/ERC20/extensions/ERC4626.test.js | 772 +++++++++--------- test/utils/math/Math.t.sol | 2 +- 18 files changed, 628 insertions(+), 488 deletions(-) create mode 100644 .changeset/perfect-insects-listen.md delete mode 100644 contracts/mocks/token/ERC4626DecimalsMock.sol create mode 100644 contracts/mocks/token/ERC4626OffsetMock.sol create mode 100644 docs/modules/ROOT/images/erc4626-attack-3a.png create mode 100644 docs/modules/ROOT/images/erc4626-attack-3b.png create mode 100644 docs/modules/ROOT/images/erc4626-attack-6.png create mode 100644 docs/modules/ROOT/images/erc4626-attack.png create mode 100644 docs/modules/ROOT/images/erc4626-deposit.png create mode 100644 docs/modules/ROOT/images/erc4626-mint.png create mode 100644 docs/modules/ROOT/images/erc4626-rate-linear.png create mode 100644 docs/modules/ROOT/images/erc4626-rate-loglog.png create mode 100644 docs/modules/ROOT/images/erc4626-rate-loglogext.png create mode 100644 docs/modules/ROOT/pages/erc4626.adoc diff --git a/.changeset/perfect-insects-listen.md b/.changeset/perfect-insects-listen.md new file mode 100644 index 000000000..9e60120ed --- /dev/null +++ b/.changeset/perfect-insects-listen.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC4626`: Add mitigation to the inflation attack through virtual shares and assets. diff --git a/contracts/mocks/token/ERC4626DecimalsMock.sol b/contracts/mocks/token/ERC4626DecimalsMock.sol deleted file mode 100644 index 9cce5139c..000000000 --- a/contracts/mocks/token/ERC4626DecimalsMock.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../../token/ERC20/extensions/ERC4626.sol"; - -abstract contract ERC4626DecimalsMock is ERC4626 { - using Math for uint256; - - uint8 private immutable _decimals; - - constructor(uint8 decimals_) { - _decimals = decimals_; - } - - function decimals() public view virtual override returns (uint8) { - return _decimals; - } - - function _initialConvertToShares( - uint256 assets, - Math.Rounding rounding - ) internal view virtual override returns (uint256 shares) { - return assets.mulDiv(10 ** decimals(), 10 ** super.decimals(), rounding); - } - - function _initialConvertToAssets( - uint256 shares, - Math.Rounding rounding - ) internal view virtual override returns (uint256 assets) { - return shares.mulDiv(10 ** super.decimals(), 10 ** decimals(), rounding); - } -} diff --git a/contracts/mocks/token/ERC4626OffsetMock.sol b/contracts/mocks/token/ERC4626OffsetMock.sol new file mode 100644 index 000000000..6e270ca73 --- /dev/null +++ b/contracts/mocks/token/ERC4626OffsetMock.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../../token/ERC20/extensions/ERC4626.sol"; + +abstract contract ERC4626OffsetMock is ERC4626 { + uint8 private immutable _offset; + + constructor(uint8 offset_) { + _offset = offset_; + } + + function _decimalsOffset() internal view virtual override returns (uint8) { + return _offset; + } +} diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index 663377dad..16656bd4f 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -17,28 +17,48 @@ import "../../../utils/math/Math.sol"; * the ERC20 standard. Any additional extensions included along it would affect the "shares" token represented by this * contract and not the "assets" token which is an independent contract. * - * CAUTION: When the vault is empty or nearly empty, deposits are at high risk of being stolen through frontrunning with - * a "donation" to the vault that inflates the price of a share. This is variously known as a donation or inflation + * [CAUTION] + * ==== + * In empty (or nearly empty) ERC-4626 vaults, deposits are at high risk of being stolen through frontrunning + * with a "donation" to the vault that inflates the price of a share. This is variously known as a donation or inflation * attack and is essentially a problem of slippage. Vault deployers can protect against this attack by making an initial * deposit of a non-trivial amount of the asset, such that price manipulation becomes infeasible. Withdrawals may - * similarly be affected by slippage. Users can protect against this attack as well unexpected slippage in general by + * similarly be affected by slippage. Users can protect against this attack as well as unexpected slippage in general by * verifying the amount received is as expected, using a wrapper that performs these checks such as * https://github.com/fei-protocol/ERC4626#erc4626router-and-base[ERC4626Router]. * + * Since v4.9, this implementation uses virtual assets and shares to mitigate that risk. The `_decimalsOffset()` + * corresponds to an offset in the decimal representation between the underlying asset's decimals and the vault + * decimals. This offset also determines the rate of virtual shares to virtual assets in the vault, which itself + * determines the initial exchange rate. While not fully preventing the attack, analysis shows that the default offset + * (0) makes it non-profitable, as a result of the value being captured by the virtual shares (out of the attacker's + * donation) matching the attacker's expected gains. With a larger offset, the attack becomes orders of magnitude more + * expensive than it is profitable. More details about the underlying math can be found + * xref:erc4626.adoc#inflation-attack[here]. + * + * The drawback of this approach is that the virtual shares do capture (a very small) part of the value being accrued + * to the vault. Also, if the vault experiences losses, the users try to exit the vault, the virtual shares and assets + * will cause the first user to exit to experience reduced losses in detriment to the last users that will experience + * bigger losses. Developers willing to revert back to the pre-v4.9 behavior just need to override the + * `_convertToShares` and `_convertToAssets` functions. + * + * To learn more, check out our xref:ROOT:erc4626.adoc[ERC-4626 guide]. + * ==== + * * _Available since v4.7._ */ abstract contract ERC4626 is ERC20, IERC4626 { using Math for uint256; IERC20 private immutable _asset; - uint8 private immutable _decimals; + uint8 private immutable _underlyingDecimals; /** * @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC20 or ERC777). */ constructor(IERC20 asset_) { (bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_); - _decimals = success ? assetDecimals : super.decimals(); + _underlyingDecimals = success ? assetDecimals : 18; _asset = asset_; } @@ -65,7 +85,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { * See {IERC20Metadata-decimals}. */ function decimals() public view virtual override(IERC20Metadata, ERC20) returns (uint8) { - return _decimals; + return _underlyingDecimals + _decimalsOffset(); } /** @dev See {IERC4626-asset}. */ @@ -90,7 +110,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { /** @dev See {IERC4626-maxDeposit}. */ function maxDeposit(address) public view virtual override returns (uint256) { - return _isVaultHealthy() ? type(uint256).max : 0; + return type(uint256).max; } /** @dev See {IERC4626-maxMint}. */ @@ -179,44 +199,14 @@ abstract contract ERC4626 is ERC20, IERC4626 { * would represent an infinite amount of shares. */ function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) { - uint256 supply = totalSupply(); - return - (assets == 0 || supply == 0) - ? _initialConvertToShares(assets, rounding) - : assets.mulDiv(supply, totalAssets(), rounding); - } - - /** - * @dev Internal conversion function (from assets to shares) to apply when the vault is empty. - * - * NOTE: Make sure to keep this function consistent with {_initialConvertToAssets} when overriding it. - */ - function _initialConvertToShares( - uint256 assets, - Math.Rounding /*rounding*/ - ) internal view virtual returns (uint256 shares) { - return assets; + return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding); } /** * @dev Internal conversion function (from shares to assets) with support for rounding direction. */ function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256) { - uint256 supply = totalSupply(); - return - (supply == 0) ? _initialConvertToAssets(shares, rounding) : shares.mulDiv(totalAssets(), supply, rounding); - } - - /** - * @dev Internal conversion function (from shares to assets) to apply when the vault is empty. - * - * NOTE: Make sure to keep this function consistent with {_initialConvertToShares} when overriding it. - */ - function _initialConvertToAssets( - uint256 shares, - Math.Rounding /*rounding*/ - ) internal view virtual returns (uint256) { - return shares; + return shares.mulDiv(totalAssets() + 1, totalSupply() + 10 ** _decimalsOffset(), rounding); } /** @@ -262,10 +252,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { emit Withdraw(caller, receiver, owner, assets, shares); } - /** - * @dev Checks if vault is "healthy" in the sense of having assets backing the circulating shares. - */ - function _isVaultHealthy() private view returns (bool) { - return totalAssets() > 0 || totalSupply() == 0; + function _decimalsOffset() internal view virtual returns (uint8) { + return 0; } } diff --git a/docs/modules/ROOT/images/erc4626-attack-3a.png b/docs/modules/ROOT/images/erc4626-attack-3a.png new file mode 100644 index 0000000000000000000000000000000000000000..4cb52237d635f634085d87337322ac5e0741ff01 GIT binary patch literal 60433 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¨> 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 W72fFO~av*&4wv-#`&9xxJ7DPz*Q3{XF6KYttW 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 ZvU7})GO|xKx>5WWyCiin@%)Vk{~s{kR%ZYJ literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/images/erc4626-attack-6.png b/docs/modules/ROOT/images/erc4626-attack-6.png new file mode 100644 index 0000000000000000000000000000000000000000..1587fb5c1360e59243297ebca9f097ed116dcdc4 GIT binary patch literal 56290 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 O?BA=lCt1P7>wf@z+Op{Y literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/images/erc4626-attack.png b/docs/modules/ROOT/images/erc4626-attack.png new file mode 100644 index 0000000000000000000000000000000000000000..dc059b228bf86b49a55f9afac01baf1188b51904 GIT binary patch literal 58886 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

g*$r>t!O)t#tQ{bjOl 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 literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/images/erc4626-mint.png b/docs/modules/ROOT/images/erc4626-mint.png new file mode 100644 index 0000000000000000000000000000000000000000..f89ab90070952a0409570d00e2fb6e9ea0ed8c0f GIT binary patch literal 112787 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�O9c{+=>)TzHlOu>v`YGckW9-|?1gc7Nv~VY;rTZ`mYt zSyBzEn&M^m{)s}!P+L)ZfYHBGMJv}&A@d}4g&!Cd!81uVQTS7;Y7yYZf9FoS)Zx6k zwKWpg+2`0so^}q=!Yzs_Ljl^K>+7uEz7Lq^w?cuR2b!>ZqHOZ2N~+2|Ei+`x!6UB) z8v}R=x@cajfyxZ?Ms$EOlT4g$4ZKrSDeVas4nDdTlribQx6<>@d=*5D?0I>;*(5??mawz<4pTYMIdfLXK`}FW((94|f%#Cbi zkW(cAFGvWSumXj(%(A6I)?^_8+61(?`5oJx;yFHc?1O4(1cIOBIbM|k1NU>aZ`k6p z2)b2HFBnKlDC^cpgOI!(+{FBFfF*{;<9qYVYu>u@g=mvUMS|eW{(NiCM@tBE1653Z zV&-=?46~g(D|{4Xh!_h`zA4;2;Li-!YM!j(&2ZqBR3lxT*g7*hZ5QgxRXey7`O??y z;(3EzCSwAiVhhK;++5|?nQ^`sHO|V392>!)lGFVT?r4*s;eS!s#W6>R+?}P!S+S;m zz|R!Ovi$FeGsusG(tZzEyn%Le2GD2SXDjHT`%s2~;hFE&M{;>^xD$2PMbLpQ)KSLE z3tz*=jbZZi#2#?XG+u2ia@%-f4_}*8dy=Z)qosZpc5=~KwTPLrk}0@z00&CKt1|_h z2ZQ5)!F9_khU{O-p$U!%MXSMCOnyVv)H(sh{fSo+@ zjTvQa!vPaWffd*TAq2wkKscY1pSE6QD9*<~94RL1vtv5qwq_<95)p#M1p_KFS#<1; zn6dL+SU4<^^t8vFeDJnfJ2kadpLFzcU8kRX?m_|z#SX0H_xPQM+T)JZuHZ)^j@gGa zG^08cslXTMxTH*{#cIOdFL-ulVb~|@viAFaj$oGW4u)5O(JbFfzfDk zCb4JRPDPXE2sZjbZZYHvg)YULZWRY`wCCTrAklJy5)V7$IJ==IWkr?19 z1DKRmep)rccjgz1h$)E>caSE`;jV3;HJsN=W(VL$cH^V-|HEb?Hda{T`vI_zySsWq z_rk$w5VJs&gE$_h#$x>Qly0K%+r;!JjhR2?J2;0jnW?5K$fS;vK{$kn-T~IWuNi$y zCRO`C#eo#A`$puv(hc$`{2O8x2A%V_UlEg9qhYrZ)50;!tLj*8T=SS58yhwS0{v2z z#Z;{9LjLHiY|qjz@0Igfr>qf_1VthtVpRTcfF6Qgi>g1}c^48sWWy*doCIA!#74~H zl}}e7_p@1tbcJc(dr1-aqfeyc7R>)a8%m_{_|q`zhw6n^nq8E#eloGAl$KFqtZCC*d#9w?vs6CQn^I=?t(fqy|0ZeI+ldEodrQ#9;mmRE(--yHKh4t%Lrtp^V$`Nxkn=~Fi z1?R>8LlvHJ(?5}VE8u(M5K0vqnI)#B^#Pm;H_>7&0hESZ6b#_`Mu3+ogFQdwH{R%G z!rOzO_coT!ANZ1vPSK%1(M<43)1R4H4ws8hnuK4bJ7YDZQHcM?v4>w48S(L$KVHZvrLD zqt%QaahTC>!Fcj?bR5f|;(_0zRsweoEw##56x{`cT|OfnT30F{&5Ox2Z6=)RG;mW) z>_v*=?xr%|ySA`g_sUn;_jiA3nSLoIN~qHgCSVxJRRg<^yDQdO!?P^fM^4K=4~bIKyIABzR@MA zbX9+hIdTo$5tYC1ptm@yuW+v>!vC@o0Xf&@xJTN|8C>4k?}8#qlkZhTo(>I=p*}`% z^IU#XlR%SNzjW@X_c)c?x#dVICJ#$tyU?9V+FaxiyenkKVZ zrdxOg;O%OSr^H3YAuK>ag!bodvO*t?k}_3*2=n)$1oCBFovFt#f=Re3=0F z{UtmqWhbXOyl2?-B_$1yg@eTOtNucN80F~qe~@P5@vudrcw z#0_dA75VTaR|i|g`&Wg7VU;p>%{VzeFiy8c5HJTn)z%d^^3VelcHQLu`F29%n*_4a z@7IN%BOjbL(|w@TfRWf2jFqjjo9cpuTBkE6?Ib^SJGsgANEd{89qL}``>1?zOcSZ) zl$nFFWC9|$ooYOrx@L2p3oMCO>OO8Ab?e^#Z;^aO=>fUI|G-gtj|29NFyB75Akz0 z4fieGdA9gEKI1&fB6D&D(foCaDvGWw%%v(hayQYU@B)$SGB17x+;B}ON9Ic(A|W9W z;^1Xt%RyQM{WOuon)S0W`+agMUH!j&FG7TnU1j_a@u(A{NmITQTkV|Zo`OXa;D#L* zdyi5P5(hv%Q1g_njPBx(i8H)ttfo}GS7VH1k2&l0zg4fMW8q*gKR>T~MG%ux75TVf z)yD<>Ev^$9OyV?GK`j0dVU)i!#zJ(mBdX?(do>1z!NEO}j?v&+1oC^mhBA4k(?&u+ zuP2zStjjcEf1hdLYLeq~)tR<)8Iu#=TOxXEc((J#yTi>cj$znVpFe*cnub6x#TER= z#~06`ilDf+18R$g0Cb*OnMMKI2W`le^!4#*q?DmywYIj~*PAMHt_+PqkbH>`9=T5kV61!_V}RM8k+WZL!GLM zhK5FGl@a%SxM&r+zo&pg1%5LEdI_kmt z^{a?r`46`)bu!9bJ!3i~#`3kN_N_4DswIHqPLOo|`8ZXJ6@^mleFGc2k zm!JHUbJZsdT-#+sC88|cN90!0oj8lF8US^?D*v0L0(y`)<5p(`O>!(er{O>!9VEw3 zNTb5CP?5r1y(0)@YtiggQW6Q-7}?@)bc@Y_Vxv=5X^t`;oHd5~fBxJz(rBL-ccF-H zH|RuIdjB<)c_rX^)`O}9(m_3O1c_|bjv6nfQxp`_YRk*d2|CYd<>gC+B1n{Pl>$n; zEak!lLYZfg4*G#%BDy@mIWLbfw8j5o3}KRcYcF4QHV}+b_;YiTqQLKxyf(>O9+!>P z|35nK(+frMFty{~ukU;8Pz-n8zRk=fRdDBcW)pdhWaKqatvBXmb@bRiXns|x7d8Tg zvwy)Kb9dWPQ?Dk8)|*=MUQNTT|8jUWsX=|w=@FIv^wo}U`ma%Xc}ilFRwS$FMLtI) z;6c12%bi=Li&$&~;yB$TcrU!g5Sr_XlK67iU%bdrfi-8Wn9H^YC`c55W73D&HTtak z_0K_p7G@>)ua7!wL;Hkj--u}$LGpPw$Fx34w3{WXUOA`z!M@q{ND|@hCmA zyyq;#WKtRVn;8ltgUE?)MA7N8iL9BVp-1F}i$*HhFo;Qo$V0T^P{Oe$3K(C{3(616 z^DFiKz>y(nxF07>t5CwXMnmElG_fT`hZ}yLaJ%Nc$Nxoj|1Zo}BqZTgAB|c4pvYA9 zD!vRqX7Rw(9fZcSyAdAG(Tx39BmQ1Hb zKq#oCz8q7($<~tj!Hl?}u*O{mt3>l{M9GNQ0y3dy7p>0$bfe-Z4xx4rln0j*)5KTTN${e(QDCKE^A81B4=$6At=Rt`(hm z!6Tgas7Muf#*QL%bdNIdaZI*d2y_H3Q{{09E0nB?P_39 z3Hcyx6|L+&ySQmD#p&$;v$G8z;4oNAT+^>od?1N( z-e8>Oz;Tc0Ke^1t(wl)p&p+g_rXhGnDE_z_K6*yMkpcym3=|yn(A#0>Kiywk4i53P zlzMbns||KcH}(%5MS99BJX1Qx727zRxCbh zIK8t<|7EquMfxHa0b-j8jn*<;goWkc_)3})Mr4x~t#Wb)qBvJ3R2gQ@sn)y?NbM-F zks&y=o)M?bO_jzcznU~&av=mLxh^)Gc39d(Ob>~HwF?rm!^;AT{*QE@onYZMEV*os zm;I@ZN2H~&*(;|hl$Dg0g_|cDAT&9nnl$Xz!3Ok~+XgB7qY>OpMvTpspGYdubZ^}% z{_{u7%?9gS>*05$4b(8m_r`14=q={FgcfE6r}eZW7TSogdK|KX<&^V}xZQ`;GEKA_ zm*!WDU8;=Q9MIg0Kt>r>UGXQpH+0#ZPgvTSi9@VKK^(pNE8a(`>OB8Bz@xfo-SCoL zF8vttP>Wc3ep!)_h~u%+x9+ET6oIYB1nm5|Qlj~y%k2*&Kb*)hy?qrFtLa3~BNqL4 z`VmvEO>^y2-YpYMB`QjAguY)Tk5Y(_`u-?S@9s-yOzy6_B`P?8)bk>-MCh=pOkH@$ zKL6@iW1~O)_K6T}<={6Yi=pu9X9iH0XJ|MOx@WvZD(MI{bY*I=O&;YIjtt92Eq=?u zxBMpxr$Y^GzSrg(T`rgM=f3vbc40snx$K-FV1`wh73}M`f2AZ6Q1s!Nhs9q{$uT_> ziHsY~2HPC>;UJP5A}labB%~cz0TX`>rO51c#6ePFG(-&D<9?|9`|yt0I7g1d6}DVY zoMGyrf0PLvPnhCX)2&vua_DM0wy}pXukBgLavSkVv(EF+%JDs3IW@mIt=DXJ)7$xX z`|ps~hl2t-{ynY((w#L~4NLQL%@Qa$5+mN0G}7&lQxvht<(8pTEX^9nyTGK+z@G-xi;e!9>`p->B3 zHCT>s>66^=|H$4D@^yS6el!9lK9&WaCVVAO;5Ma|zQQ{Edy#7y(!lNUxZ}m3RsJWa z9r5tXkdjHY4_M`88D53Yq+yDGop_BAGx+}gLLZFTXK(mMOxwKUmnVAlRiJLI$(qM? zOTA{*8ROSY&O`6jAt@>meOp~0M3p((Wq?WD4Lkm}dLB_3>9f8s{Io?QNUSX!70?lT z+7^Ejcm1+28DsqQjAy^FeSbHv7ZkofSR&TN!mG7Pq5L8=urq#b{;l2RHst9Pu_tVe zm@S4q==!#dzZtWtdBP~rHS`nvok8vq-cBas;&2L@Aqt{?t?9_``BT<l+{90c zy!D}4_6%+8^V-po|Dhu!r`=JU@AFY#13hbeL^@e~XPTB?ECE%YO?-`Hk%gk+`)}pf zjAXf#=bCYnyC{|LlD{o}O;_0_?5sSBgNX{C!dfAsU>k;dd|QsxnF6>aj9_Dk2$veQWH6OD5M%w)t*4pmFL- z=rVBag0(xZ2P1jFK8u^j@%V(Y+Tb=3-nrt~IGxb?&wLD?$5gZY`0+j23LeenL(*?Q z4>*AxN9Pt76H`!3IA!quD|gdqnDcGPxJ+g5C$;LvjL+K}`DeJ~>xOa<(zF00pSA!8;=E(JY#2l!pAZ-K?UE80BO78udh)q^Y0D5%;`%+Q4GF^gqWV;E6uYZ z4OtH9Rg$FpU$bG3tZA_~TzX{NP&E9wcAdh({@Dqg+oeb%d0ok1Vb2}FC`)88B|n9X z_`Dcw)d~vq;Rx_QBc{wDdvh_qt@z1wmbqfJ%$51SbA1?Lz3lRDdL@i|fHmgJQ6s7T zV~3TCcnWUik;eyru6f`@m75;W7k3`#br|;!Col(J?oJ@QaWP5-R$QMm7xGc+@;qYG zPg@I|%NI%@X$}TZco}hc=5CO1E;O)?;@aN@Y%!LkVG~>)^XU`>~T|F_JkLE4W z;Wnp}n6$g5Nnw;(|xu$oe8$ zvhZZ{CX!>h{zP!-{qMK-uji7wB$0=YkV%sJ^z?>JRQS4;MINgiswOb5G!<6A4^T!G zD{|8pdk_=K?p=MJ&Y~pq-HPLV(ayVyE=UrPc@q*kVd!$nYb53?NDw5w$GWjkAM7pL z_nNrd8XPO8K)Sb&pm$*1V{e&~PEC0XNHX{%ecb90z@*df_Lv1sFn?M>UAM@~et z?He+6dCV=ILa;;Uyz*WYC)2RxI|@~sCCP?&Pq0AKL@15%yZc;^JPlT)Uqx$r`JA_NgkB>_I1a zq0XIG1VB+KG?>UecW1m)WaNht+fmT_=}Uk$M<+?k#d@~-+1w|CV4o|n6FSK=qZ|~U z4GLzbI%*OGe$OtM;+gn<%$|Dd!W7vV1Bn~{H15OsQK)EeRhsV%_*5me7;uMK`d8Q9 zyNAU@M1DI$Ayb>6jK_%^f5@}rKdwz2EVv#h$e%R4R|~oBE~-+lrRvKD@f7jzgd@_J zx_W#cu)1$w29j-y$5c~K>%zsm#aiaZjKD<0%n`!O*)V5mjf-)lL)r6znMX64Ue=z8fBFcEqCB zklN^+_BCk@YbF$SK8QQ6GvB%;?6tq*BWL(!`IvlHbfz3gnNTAlTrX^x9` zNe@X zfZ=@thj`SDkoz7XG^F>iUXxukhDGCR`P0ZrU!5I)p) zsV?DW>N*jLct|g6M-smN0Ccm{TVr_iDSW>(+L^aOMrQ3baMf5 zv@(S~%tyNlIiII4>%i}5dZKTE2GsV#s{Q&XT(g|=Bu+BeoUL~&MS1vQt9J&vlFU%R z%7ttv(1WPGeLwBmn;DOaV5K)ReKl)F7AWXvO@n8!cF?$`|2^`{V8$C_@II-5h^;HW zC(ciAe*L;AW*x&>|CWmG3Zy5g=A!b~_N2>`obSe(ooACE&X>1Ew{;>Ye}K#6rfN(Rv^ zvZbp+{%VGhYeRaIwzgCaLvvDCj4p=A96uZkX1Oks(loq5eI@#%8w3kD_eZ$lvW+vS zsh$y7h&s+ADGvBv5p?pm&A(p!0?g0Jst4(eoEyF-K3n$*PrlUEUZjN3yaHc)wqcv(;oH}dPt(ksf5k@_4f|MSWw|#V-FgvSVEpJ;CFJ9s zyS1VGbFY>267f9)kG98_o@8!S)RpWj1w6U#> zHS66Kv!Q^@zt#i7DX1`ZCY-vlG_3<8yQc`|8P|-K2r03MNXOt>PSiq;3{Ic$7MT$j z{<`zQ(pBj@o!z@3D~Y2!$$lRe77^kHunrv^^@ zOYZzdahW+ix#9xxzow6S7_hNyIh`f6J6+;@(Zp)&^c`+DdZ<6*n0~>v`i3h6c;mm~ zESYdE#O6};4Tdo9sVZ7nSf=^`}kAh zfS*M;%Mz}TVd~{VxX`1N%oV7C*(4iJ;*`W?=ykGhKvoT_*}%*nKuWn*emWb5q-#Sr z26c8%c-PF_?Tc&6zt*yuUlERNSGk{#P@zg6gqNkX0Z5VUl7G=M%8=%$k!?z{3#9q+ zQ?`qNqkJa!&N_*UKj(=%|%hVjyQ#0_?mm^=QI}kbZx6z5NHnCIv_p&}p}U z!dalVCUR?Uekec6)3x|k_LT0$d3UX=GKfy=<&2acyL9h?bl)6rCDUCaBKODA=cDGE z`&b=gN%v+9CCQ;6#rZHPa}Ajf+QhDyHr{v$zG|8Izitn>8V8wUo^8F%6J^kKtR23m zR}c8L9g6-R!rlU^s;+(aMv+jY1Vy@(knY|zNQs0(X*MC< zAl>XUx6kvQ_y2w8jBy;pF%V?Wxz?KNo^#&wx_=k4QYA*&0rKnTq3pj2=#Jc+lqidG zB!0=3S0}Gxvy@O7y6~(+1n&VX3`D~|E3ms|w(}zOp`ce?H?(KXbg-i4(`)PaG<@^_Co~j%Fl&Nw651{?;aD5g1U3gdvaf)ZFMYa>h;w?<;Ew!E#TiKLkEArwK;lXj zy+s1iYW$b!b4mxY&%X{#K&x4QTm4F!r1&S`y&8ZIx8tog9jYlgjux&K@yZ$a_VUI65Gh2 zyY+lE*ew|fvbr)D)6JI;4jRi7GpOtBr`qj65C!PSjt?m=XY2^WiiVR1aKXyh*0SCk zFhl9!&qS2fSUgQB%0CGK=lt>r5JNQ7OpEHuBR;$naiA#7qhKfZqR|^Q8ncm z9Pdd07*6B6-=%;8r9%;HDSGzjPQL;TTjb1bzwiK)_}TCNxit>#UK~ z{^s%thw7YBmh~s#LVhbN>b>WA@==xsVVePbW86e8c)^Sy(W=&ShyFx5{p31mTppbD zNi1W-cv60+ES4Bg=k3p3rX$*M1?>PBG*J46AD4*l^aLs{!LuX4A{Grujw0j)-W1g* zP^dftSG(#u=!0}}&&(d{6ld&?H{cL3V_V*;S<`Q)p$Vr zT|1Ymj!osoF^LzF-ftSyEF7CCLxe~Yjbsl`SFkNxPwauAqY3*Luar6`z9}6NG~X7g%+mS0a;Fn3cy9B5 zW9z3kyu9aRHMnwsQ60@zh{3bKhF_uG@;jQV&Zx0+n-0&teAJ6-DZa`Y@c6t^OxlqcErUfw*50-{0m84X8)ZpLLg& za-X-fC?ZC zP6}2VR|t>Jw38IM?yyMr`E{U7eaW z#Q$ZMt@yB#s+wx`<{W`a@fr*(TBj_WS-)uukINt6mcVh0kBX3NQT;ax6-BYZc8{|r zKIT|(@PY$lmVV|BeMAc1lE{_xOo)9`D1x9%)w7YsVmE^FxAqruZHllBC54<*j z!pDl&6Tl$@0n`%M7y|nRMsJ~L?K}$&uTW=Qo%X!L{5em!%fIVcsYQv@WIhp4^VwSc zo#f8L1(}-v^f%lsEEd+aO)-q1?mPa6x=$m52=*WR5(5yC@e?*DH@PxLT43}K+yIF0 zel*cQfW$8ne|Q%F|9S>Yd_vI!_K*WUI}l6;2*N0_VoEa2)yc0wvH&C3Eo%sx z1%OF3Tw^|YgQzeX^eqtT-gNZ73v5plKc}Z_Dh7ebLH~kY9Wa2=lgESD2 z0ayB&=#gqnYDKp3llkX&CN+V9=(4i1Na~F)Y`~Qai2}I7H_Vgu6w5GEa2E}p#z#Cf zoAXQ~0f2w!>tmPo1|e-He<1%?1m(3rl=cBY?pH-R-J;qMHL-ZPZt|Ap! zV3~s~qwldrDx|sa8&SSdjc2i~5U63LScs<7fNh~AwBG(|s&`YAMAx79O89kx` z{Dcnc$6mQyK(yZ#PAUg*K4~6jgolTRNEU!cnRc^Edv8A_D-%d}yTFr90_i1dV41+o zO+dDUXn!8T7*0tp=KI+ijOl=J0M=!1lD9KJ)x0$LKDv9;<#R;MNJm*V*o5<|0I_2T z+YLIvaz8!*NK(GFVZmd0Bsc21uhX?O0VvjmhPxePb56tRi1o%`^O)fIEu@$vzGH>s z3e=X6S}+T?+{vEL00VuqWn1qyf+?RK}5ENB|pZb&@pj>mK zlRi-6d#8ZCJZ&H`#PgLO=menM8KL!dx001y8`z{&xt(-r`Qk?sJ}s1*2zAf3zq zQkp59HH362fHfqG?g1DGGrnL)*`L$a4z+W@Gn97sYC=M)IgybNRej=aUj!B!pq|hR zENnf%K02uM000Vn5nuJ(l0_b4%FbrTyJ^#KMOQv*Uv%^UY{>5uINBn$5Lq${+izb6~OXSBa}S3JI1_R!|aBaTo&=){vAVI6Oc4u&f1 z4sw~u6hm z8k9hHPiPP$79nO&C0=(z4&IY3mloPy=%CFS~L#;Q$8iF(D{Y6S!YMfVvAd zIKF_z1hn|1IEg+_fN~Wecun~hsq9+Bw`@^lX9q-_W$b+PF1RRw@qylQN61Yl zzcji}&+W5tCaEpgm$H2)i2mHuzYr*8)~N@Ug-iB*fE_Lej1ZAWsAZ(HA}`|^K)w_d z!LcKT#1%B+vNlUj7jgC9-2ipibNTH#2swXW{b}dPga}H|LlMx#C6H-rgSw)uLAD#6 zX@uw#VKJJckOaV+2otsvuiFrWtsrpVWEh!`c0ige66{M`(Cqee1J&+fr@e9RZw}j{ zZndLFQ*;%*(;#Pixx z0Wdkj0u2E1g?#o?JAM~x*&-fD_(K(UTd-ZKSFgOrXJMKjt|FYjJRNUF#9u_e=$siq zzl;#I{dqj(^tirTxb)7HzkJ2#1H|XW16?U7*L{5U!$C!%OQ`f-G1%||B{jPs&;kqd zmXTynP0;m5gTldHl;8}^z=DE8t(tJ)g?$8An~A`=Q$Zw7RS^C5&b6yTVF zou_TX{2Xv}opXX&`s`0i_Rl_*zBq>MB(O52BzbXxg*GYUtYcJMtdYHku`^jBNXd{* zu9N=`nX{DNosD2nSq-Zs3x9vGWV*%;bm~BH4hI95F>fAoQlD-T7~e!4;@-Di#o(=b z^757_Wf>;ZDZ~c4?#MVFh`Bh`7tF7?*4Mow;!Z=qC*9&V z_Cx7ciTD+H|9d%p|0@^uZ=q0?}9S_Tl6M(M! z=<*F1()zEX<;-5fM!V2Ex29I=-}8%H?xPaR2<$lMEU9wkGJB z5JfC@A6Ogjxq_hL3LGOY0pGc&zdsY0gN=Gw_5$OY1R-~BP>w|x+y)H$hRhJIlvG9C!nuAnlA(vPAT5Xj5YaE|rB z7L?+_yaC2rl4}Z!2f^<^vXk-wiN}R|KvCbYT+^8)a`lJ* znR4c4bv3qLpX1X6Llbys;d$qa`zp{-u zTBJ9;s0(C0A;R%~%Al59GgZG^-d_v;CKdv-;HkR3YFI5G9^O+2^8tW_b0IuPTtPS> zssiSfFMtmL0@V-xqq|UA`_j)P8%~m`h|Ro$;DUl84}mb?0BlB7P;lkgp7r07MW?f} zf`Z+_SB97Guj4tXQ_|-7?^ANlinaY00NQ9)z3spR7lM9;6aPwaXWsgfW9C^fR(}rI zwIb`Y1!LE}zk=gqrXvz$d}rl3ycWFV4DKhG*S!<4m3kIM!X3X_+G^=$3B>1vdrm9H zMu@KY--Wrf_HjQO*B z10m_#4`p)(EP@FjG#T8I#_+n$J_yauY>j`D2Pm+&WJK3gVD8NKc z{T;{TBW3Hml!y8IB%AvVNDmH!vO64^bLm)8kCs7gY#uVjCRv=~#-?&Ofpd24E9MbW zc8J*5-RakjJFMN}-O5BUpfbYowLdmF5Y9Q6J{G>O95HCT0{Y6EG9krT(kz z-W^y01HF9rhsR?wvVNh6fu9Cuu$ccS*zW)y?=ZLF&0%k(xcm-To67qzc7z%og=(tY z!gW{hUB@c(?Q6zKgctI$ZVPjzHUkg&ieJ&!n9|36N1T6IwusZ{w7MKKqKPUPTR;q6 z;m5CYRkUlJ4Valpl=cc^Ht&jQRp})$NM)ufKj0s5{}r*@EwEEtB>j>_>an#*aBwig&Zw{x*2W52%9p=gdXaYtn_^0Y?z7;BmmkgMmI;TNFM+mK{Qud>z$V5WyM zwwr<@{>wftq}=H+zSZZHIh20tSDetAe-!ITRa&eJ-4qs_>m$vWW5xf-+o`AIX;854 z2p(1I$u%eb$zlr|GQmfv#TUX~7IptYUA?(R$&%-RS`6NIrV^ z>yGqa;_YR!D?{brel+&f#&&jxz|gk?my-t^-iEu1dBlO=*(Uhw85xttuk`C+tPU22C?8|!-q)_lFEzEA3mTzEjH zaq=69{<}j_nvPr!$_(f92E+d^&~~lSjubuSw{QIC@Jzld%DG;j{jXsC!O(!d&g;Z( z`wAfJA`fwiCh>G}RI7>I`?zWil2VD7M-{aczqL%Wf~}T<0%nSU`!klVt}b9iE0G5A zcrNI|3YPcQV^ami#p{2T+A+KN`1pWf5iS9Nw5;qSV313JaE$^kMuB7k7N z*Y#3i;Qj4zI9_AR+6$h?va5Ib$dB`ZHWFwttJTszg6lVSAnxc1T$MWZ49iMVvMU0k zklh~ZOI5%oAz$)qXP)*?K+oxpcn?%3tXlCMl5xgZKexH3a&#kD#Z*(Am?oV>pZUU{ zaATRm;J{M8 zkO?xd9VHHidAykna(-SOVl58Hqk`w2!Jn|lIP4zGw_nQ*LMi+J<_@?dw61@!lLZqw z(zVoQJ}mteimbR?`Cl)3eBIPD5`>k-`WlAArCo@(|)pzxg0BQ@Cwj=o@o^9m^%*siOt?}Ugi39#4NX7^eG~ab zPCzWsjJLX=);QPYxlE1snx-q?sOa@uZN9)#Th(a+#N<$AR{SmQiN}@K+j3&m0iTOj zNG=`-G1Sn2?S#)0lUmcd77o0p2(vzJ3w2P>S4}bT>qlU~7 zBvj|(hm=GE(r)9g+jMjUk`cXAD1Ml1fX2K>m$8ofAi+&9v$1{4>4Uw3sb3j#rz24t zi|E5Q$nM<;GVJ7f;)rTnxCiPtG$qu9&?wf6gs_xW?|cLEV^XrsWP)k*^4B<$q%fJCwD!y-4s|ZeWqH4t5=Wq>#64i z`NN2>I>G816hI#4&oc^C#q~!Au=3>#|Mj_y&A=72nXQUjo&~Vli0@kJ!ubi2qhsZ)kenK&A9TsuN61pjc%EQN{v1 zw)IBRLk#=hQHGZNs{;a79geD<>g1oXC`+=aFlNJ`YMF; zDo-}d%X#g%W-qta$N}4vkH6^Ong$S53Yqyj>Z7Q&QdRS zj0jlf_J9)hww_#THsWCfd7fO!6%bG&ZamW^gBhQ{E^-v`hXMQj9^kw}wpm{^U$!im$PfBfeD`uMHH_bTLOAJ1=Gg$sRP8CbfyWkZj?;F3 zd~NDqhyvnf2N-Y#Sc&?|l%60;5RvY`bRiF3<80%@S&B_lNCWEbM^xCv?KbovO>Alf zM#kstZS?9&@|EzYVJ3cZAU!T>|AYF1o<0cpmHx_AOcQa=o{tO}(eHgfbF;&e8W{n1 zjz0hL(}Y}i7h@ALo!sue6LC~AP&|+_*8)@{h^xl@-4NZNZtF%q#(!#P96>2OkhZT^ zpHhMVQZK)rc=iT{^_7e((dPYk-4L#@EbulL^;UtpR+3rnIjT6EvDX&w*gTosxPV&! zXTg?I;er}|_AA*XyXq@P7=L@b0`2-JxcYL0J7w~OQ> zj1fv|)zA^l@ja;N;+Obnm!Dqqa!y zpPE6w#+Ey}n((bwM@MtW33EEfcSLh9rhsFttx?}w_ClQ-%|KICgznrA2Xj4B@kG7e z|1_tuaSL%%{$j9ihPiyiVZmA?-Ji(9I%Ad{|5fo@fkDzX9lqQb5GX^}O8@r!I&XmM zz){Q|H};xsoJ5=u${;(3twIGV#){9SK?MO!E;r<%IMG*N#8*9f2;AEdD^WsT8w#-7 z2-}9hZ;p>=xMT#%F1*N&ZD%Hs<6YljUoK6y>hi4p$;Fhe^RQ5~BAXXyesJdVkFBZE zf~RpVmZ4Z&voZ%cIwkMgtOCIDrfrtJ_;Ma;pXLGThaWD4XszlaSZB#VqvM0~YP^sp z5Q+}!sG^K<3-Hcvp2|5gk0)RUWl`(DMphP9_?~KBTt_1!^zgBi20G&TKO6CYuX*0x z)>vwL4k8u9FTYBKS90(UGvd?!#vdwI ze{wpDAnB$eK-VCpDLbz1CLNYxu6`hOptYXpJopK4>SLi|@|nO4?LXRnt>TAIKTT3- z=jQ3ZSQ4ElMp%ctmf(Tz-l4p*%iB6sp)$DQ%nzD!@!fHPG>Z>2Tg&`Cp_%dqrW~9K z)PjN}-eB$aj7d`h*rEc<88vVBDQ@k7kdgPEUNMsJneNJdciXkoTZCaSILc=6r#Xxs zDE=pXRob|Tl+4alkrK0WEZ!zm#zr5-i=QNI*X z=JDozv7{dWcD;&d{|%$4k5l|d;Cyz2Wm(V&ptOjxf%YFYz~yJbWd#!nY^hZc#$Ui% z5pf0qYz4+qj6}BosVV?3;^}cRD|C*pm_T?NZ)_XI zK?+n=f?@$w&iIHQDghi6l@-kYGztjm-oD?6duzyZbO&6X6g4$6^PW2uhXV&3+rb*V z@lRyO`ct%O_W zE~~{)LhAgO^|$_Ft25Q+{j|U_8HLLEU-ITRo9fRDvdY+Z2Ku)Oiq}C|g=t z`4W)MrZVFK%iXQ5Ek@1CKs?$v2!AjRFt8BbnJkHpPY9kYGS2pQlUtaaVfxnYnE0n|ItjC zR&v`Xgggh!4={XQJ_{D2CKo-T^lir3*97uOV|TL0&B!mMAe`^)^!31TAi_LC_;yRt z^|udNJhvqga3)0&%%lWV3QU`WS+vYui6zD4R8qlajoLW4XRYrH&*SCUw2!GOpqhhu zl2njWVWpik_j0%xy8a<*XjnRn+byDK{vQl8pd^COqFkF+wANkAZTZ5GKX+$xKq~=Y z*OP{GyMV=bOg`-r`S2hGldz{{buWPx%oSyr-J` zGR_)oBNyjy_Fkb=XSa@KuB!CfBd<3zXCq8PBKtDs{a+%smPY{h?WJWBg6P}MB8CRG z3bZUN5d#AQ{#ExuLWl-9H+$al-z`iLv@@&sH}eg^A-%@yy9?S*3gmo8OURTDycTT} zBfyHKA9udi^B+@obV_yY+6c?F?m9+W3UR~LAq9}sw`r^LWj}pEFWMeh)uVgp0mzm= zwf)w69#pCPjWbC$)p{8*L{ckiPe87EC6gbxN-GXjNsgb~@O`Siu^_@k0Gc;(P*)9> z#DIsD!#V!%0yUur_HUZz znAYdcTj-pLi63FxYAjRG&u{CM$pue_q6z1;i(jqA^!-rdF)pIc+2NnXrEeXIYLt@dVq zTYdi9&cepE^gHQy6Ghur`Bu(p=R0ff?bF=Vk)kJ_vJt4 zCa*0|fvOC)y@hY6qG-!1dgI*-_ zEREBAwW|5y;1N-!>1%a3`nc14RC_{btT=1(m!4n7z6`pK*pFL&&ECq?Er_zYZ$*Q0 z70qKz7>gKz6tfq!IMu|fE(u$>G^XJodcL^ut!&w0ilJcB%8SKXOXjpjwHzD`{a2mG zv`&AlR+RSBs)E0*7#&tV&0*&ozk3sL@%$hu^5CmQgfeQzh1&8q70OVO&6XWf-pg00 zDJ8U>-HCn|IP7@XqVw34<8#!I2l|y%_a$nnZVqSMQm-e-_C3=5uOUk2d#qhbvWK@d z5r;p{6%To823Mk|-V8gxuI^S1_7W)bJx>fjWWy!b$cX99IIF3{=kHxUf=NI>ei7}N zmeuA``rSyF8;EVLN>cwJz(ww$C*UjYQ;fF`H*q+hWCA0W^DnVvaEj^JQRJRG-Tko; zYPmio+rO0%gZ8jz4!n?p5M7)Osod;Y9=nK0K^T=}4$D@Qk;#)Bg{H#dis*~p_O0q& z!#D#JEAno*yDlYGH8Wy-_GdyeW}}wHxqV9|I_Z{$0-b4q7zrx<(Os1h$x&D$q`JKv z)3mevxAybfKqOrMZ^D9F!!5V{AHb+!I)}WCKCl6|Etqxz(q#?Of7aG%-N;jc7o}~g z!7ZHohSuF05XiNgS}5M3{+E3DH=RyPJj{U?Jx{WqP?Ph{{^0{x@2kwV3B^S71UKfp z!M*VXa*?c*pSuBc^ijTqU&?&Yafl7#nriJ2Rbqp*#`G9wix`^aJsR`+-qy~)f~g6v@f9&BdJHWtme$iQ`CK)?T= zf!P2CriD93i>=QWqZ*~Pl-PHkzp5g>XvzgfFg-6Is-2I5 z1-^vt<$8xsUTY26fpGFHE=`kV;|tH&#%^G99#*|mxf~I@%pQ4^jnDF-Fn7T#+?{5; zG)vW!zEX@nT4gllVlYJBSywt=+_ST$OXPh2UK1(kS^U#Wte(3UxjKBCSs^=YoP6Z1 zkC@2POP9%x;EK%xvO!6{%*d6?s70NSIA9C z7o1tlqJD^|6dL%Ze3Moael)Pp0ri+rg?QY~RXUaHdfrz_5OjtL2qY;U{VaSpV5j40 zESUoV7oO1fTMq`Slo%?{+f_@BjB4DAv(8O6@@zU>?UBZSbPw`Y+L~R*Y04Q$W$e6T z7057U>QjoGsvz8rsi-Vrn0@H(g;a(#?nzo?Bi417J&9`KoNcZ!bdQiIN{HB?rm2?W zQV0%fYWxG^haBZ=S~b~!>bf(+KE7LCSu9B%PTZ1og|*Lx^U1B5yoxr7ur_&rECb=& zK#hI(CbvoG_7WrEZm&$)Jcpfk=RQ}#5MyY_&~y5?M%KQN>grciW;&kT4@xK{!}fuvVRTnJKXpU4DZa_M!s6Q zd=Wjqv3|0=a)%aom=VD==*(L!P2UM$fclv@r7a21m3c=@_PwE1i|<=a@OO6a=;nII zaYP!m$M&uDuA0XTGtE$@^HV`%c}nh6+0KVui!#9xS~lA&i2tHw{D5C~iv{8rn)e#* z(=Z-*k(U5=8{P0B;XPc`&f}JuevIL9TX=4q?eJ|A#?Y5>y6lVnb!x$ejmKUs#`n%A z3_VHY5gj6vS$2p`mPx_U(g4 zmbIk8Ea0Ob+A_RpdbLP7L(UPE?DQ1pF!H750JgLziZLTSL}K;PXLKA&^B848cv92N zo$k)%g-OYhFF7uVCCb5GZY!+AdruD|nf%!=rw|R=C4vTnW@Y(^JA*qae+*ViGM82^ zW4!_~Ml|{BA-Df00?qV0s)+s?a0<|aZjk<(44^TK>1SHRPwy?M}6#=An!TNCkd$fFDA$= zHGw4bV~{eN!-3V~2}z%0?&SJH-O`T7Djn4w=z)O|g>RR*rz>z32q~7uajq!uclb}Q zv8Fed-R$e0{mI=pJ3bu#(2lmR6*JKaQ9GA9rj*T5z8RjvK8D`%k?A&gV>2bhs0^{s zlNXD}?8b0-*P8taz;GnluJ7@+w>B{A9FOsMU36*?Z~I2d8HZI%UAZjLZg|ZxR$pm4kXOjX5xNaU31clVB~8qC`V{Y(KKXOmU{dyyxx|hh zG&dv;C{uhk2x@1huf%X+BZ0pEYzoPX0oems#-cGoHUd*1gd2M7J@_NAi~j;B^% zgci{hd}pP9s&KE0pLR(UVYbmOR?(p0Vqx~CVwSb*i#$9mi$ipo+*6Md(d8br`(bC| zF(9C@dQCg0wltaXpED*{?0_fzwW4`>Wz62nIM~XA)sXu(VqKGCAnPJf2Rw2jbk~5F zCJFlyCz>j+67k7(@cFI9r~2;g_!3rZc~wp(Zc{_(Gy-zsShW!k_r+A=qpBC30o1lb z0X2;#1>xg~BocHd(pXgZ%;NN&6)m%aX`VF^iqL&cwZ)fX#f7ZjmRnlfz6$RObbD#n z1PU0k|I@%X3ygt^V9j{F93dHL$%3DP+Q8X2lFB8~5% zybd$mlaGv0TU+SqA(NSW9}5znz8X*F{rJ}Tg;T>(-wBCmj~_Zt204f?w`*1(Zst89 zmtWTAPM1!`iv1qUFRw&g4;`mXGA;Xt(5wr*fEmWqwD4DdLAKnu*lkP#FPx8lSDv*8 zW(#<~z#@r9NoZNT8DDdh0b(F?s2_0}=p=>0l5qwlcX+Q1jad_TAzCT~R#2Ow^qnn` zrX*qEG<Y`MoAQaU^44xO4HuE~f&8-tW75lg255rb7^gYGs5?glVlzDEpoHAn{kgxW z-4fpbq8QaOwevCn0z5plrOi$~{M)HlVx`j1Ihti^j2K++Aq8*xWokLqblfUuYF-Sm zh_mWVpjiEk{*^6UIe767O2Be^hlK5EhO%=f+Y#{8KmVengyeU&FTus6j+}an$ojGt zE)S-s>j`OirS5P0V=E)k%Php?1V;Y5dIQUG)diV+E%2MSo!xu94wsU%kBl5%b_U2M z+b}P;KY3ak|NPSCY48P(pD5M+kB{|;XacY}?d~ivqI3K-O7n>3GiW=yg%4J8JNcn#L2;nzvEo7VcD$@JBF zUF5S}u6kf3d$m2{$JR!o34swlbVk3(%(9pk9!IiD7C$7Ag(0 z>7kAF#Xcx5d7Lf1L$Dz!EGRyxF~tf6lN2*-w%|0Te$DULr(>YPZ>2%WZ=?x_9f|X& zOvVltNn&7xYThP2`YzJbEF22-bkSBYlbgMbJNq6kgdOShsGgXM)Oem6OlAJw=wFov zu3>MEJA|8EHQcr2z8=6MYLG4l;@+2^As?4GnO3idWhw~X<<|GlvY>p|ek5_(!8IlU zeVpEW`dE?N^}5{2i_uT`nZe%bODc%64e05uWfF_|`*=EvPY+lccqW4qOdfXDjNf^^v2FKjZxQ8dGF|yp;;HwU z^%2fJ1L^zsY#Od>p5lv~_a;T6cdTsPdO+k+TCTRRNQ_AW%ueQ%y)y(@9cep1gbD2L z+V|*jJ}Og(H^<0!tl0SsqhU#Zf;BrOqJFS0v1FWtFNmal|K8E$Q0;NO53?3b4Qyu} z5zp{bI2U@oTH3JaA{bfY@WK*P<?YN5gu`l@oHl2oz9)B3UJa)86>QVjg zE!kWp*UdM8`k+|T9HYMM*38`7HpetL;=kmuOiz|!m0+9VMQkD;jQ z9QMoG-jrIgCfB##OHLUMq|%&SsT^o^_I*zr6ciV&od?Jizb`aAM8!K-UQ?K2sZofl z(8KBLbA#3Edev((yxO+@p?cN$^x4_nfeo(`!Pe{9S4O`^F0kALe?@bw+4>Y`NE(>t zEXSXby<5mPO-#M=Smaq~YdaYZ^r>1uerbmmbpDnz5n`P)#cXr4Tqax^gcMa*BPuss#+os?-ZwszH(uyZhA=Fo^p-*OV++NeQaytN5c@f7Gh??6S2Ql$Cv&0~H^I28@{&|Iv zr}kcnKndh}DR&~oet+K*?exb>?1Ev>3$xShP%c>?{xfUqle6nVYvcX%rgJw-_#rt5 ztoaTk1`j*%2ra{eZ!)INOoaRA4mgJC+F4c2&3~|aUc&~e6@?~<56I#1GiO1xF=mO| zw<=8A=DKiQp*=R8_wdWs?(s1A@yk=$y&_qks}`@*Qa8A&-1o)v1@KWkEi=U89v;T9 zmg|NOu$69(S^wq>Nu%ddZPjIL_AS^#<4GJX!WDbB=jUB|i3yg%f9@G2=J3yYir!vZ zwy5EjNYlSns*$^0ZGejkRbQq}EbTm_-QG?7vTb_(Wuf^;!LRcM1Id=>16yNbFlNp1 zMW@m4mDPv&D(?2iiGA8-R30tw-UYeIuMalvRi9L&pxhc>{6u5lU^1H`;x8c*jt`_% z+f_M$9}54dIk#3N<>GL&SQNaXo*?2+`{TNqF>t$n5xQA++tAW)3Trl!vtB57kvbXI z#2Q8)TPD;8%J@D`g{NVJ}qBIX6rshP_Zwwk)k1n3QuxT7uM}Aq4J6&#smUIJ&>KbF>wfv`{TlhfaO?xA$pouk2e|ZY`~gj4nf2 zAB$h2d2ZnN`-OT>dxsJ8l-sUIg+6XcbaHS=k7{YY{I1k!#?*RnZhr@b>t^HsVUZ*I zIJSEVbvhW5cBE9kY-fe)vJ|UFy^)>lOIz zgqPvqLXF#gO7nPEp#i3}32lNW^2303zi75iPvLAr3+hPp3`R>zB|IceX*46k-S4Ax ztyz}R(r5`opTi|HA4IVs5n8U4J=fXW(>|#=xoXh4xgm$g;`KlK^Ti9^5`TSnm+DRc z)5_z-Yw3x7^~Fge;^E`=^a{ntFLQ#Z!-~wFUtG@;Noi_r4ibu#>1UDNSCbtb%>}VN zoI$*0&%+|bpsdgWwmgV`uPo~Rw&$(0pRt+#2Epw>7~7 zoig^=(?s55l%+>r65@ue-GpxDFh*P2brcI&*9lS&=(|b9=gb)Hd} zxweK(w!`j$m?dfWSMnkGiQ`zuTQ_jCT~H1x#hORxuTSYk4L!ECQmsyJRD0_ zPHvQ4zIEG#M=$=JWTu05{kXez`tZe}X)nv0e2eSTz0>a`;T?|pl$xBu`phbU!!Qzrd4h&52ZO_0^Sp1%~%&>jN03Y*84KUbzt>hg=U6%$Ov`p!LCTsY)g>kB7Q zRZW>l4#u=v(k={u0Zdf<<$fBV1b$hcV7Fv*QZpD+VFgn?Z@A@T8Z9=9gXQ!m z6w}U@_Y!K~%__70_$c_2nfwl7>EEre7%|1}e6K>b>%2K!_139d`|Sw)^x3nVYPoMU zZDN6)h4!dHEJUNG>zZ=ENLAEak2P+q;~!7Fi-oe#HdoT6U6&txA#3txS9Cl;I~V!2 zW{_@~lk6%ObgomcX>NTmsPziwP_=%T?%Fi7R#@_O*t^)Vgh0-?;P=s@I~SDq)VAq6 zWa7G6V0|^tgQCi^zR|Jepu*KFr(&=A*afU07t?0OXMb+U5XFq%Tbl1>34CQV^dyJ@ z>Ajlr&_^TH^1Y$Ou#zdVoZ>_a+P4K@RQ~5;}UPNzh9y-v{ zAw$i&as-V`jVkcU@Ut#GAQKg|6C6}JAE31SqpQcU9+q__(2LE8!84Dd81Fgpte@wP^Bcrau&Uy4C)KW@cc~cXd z^0*J(+mdPzm~zEr_K-#Vw9ga`&XDe&e({ELKH%59(1!O(`qLL30T!(x+q^*q?oS@k zexHD%3~ovC%^`mHn_eyud*<_d9Mmwd;R{p_*1dBPD7jrjPlI2b&ea<`rA3c0xW5r2 za0?=)S-k0oELhZbc-{V1wo2njEtwb09hTW#V@y(JrY85;d`6zzpn37@^Tfa_!r{c; z5Iow9ni|%O!YGZGD@LoC*lZjF3+wL#xNhuOOv$WyEP>K$OW{{H!1r?9kDxb<)$J28a)k$MFZL4J9dI zkb!rzYO!ZV!ibaCTgGQGnh%-qBU|?G^}0KTEI>Si4sIrtt-$(c$!3#i>J5{LS{aQi zvB-Gf@%R!C-`GSO0m0l*VU)y>gMx{Z4A-}ufbu838oF1(5wm@#_;qePi6fjsWg~*C z*#p$BJ6)Dt3o;*uRRmn}ywWtE|Y;$zn(Xumi~*AhkK zk3Y*Dp{DbPXKs5wkW`6U&V$;E}W*Sq$jif)h=gGJ+zC-K21`r@9=4Gla=h%%NON3W?t%|J19>i^P^bYkHv4*AQmkyuFRdXHUM6$QGu*n=LrR?er2a2J6jT|{eh zKa0#aRzU-JJo0yAX*;ePE*y)OKZvYA8eaz7pbGIMJ_6IV5RCU673y}zlDdawg?6N0 zs345EhmisxPx|oWJ|e$082Usr^ zStct^KXSltQ;DonU+QKR1wLow?+3$t{!xVnW--D?QlAjY<77#o2Ij~w?pP+&i26ics!Bt2F-qajLkQXfZ6CO#<27~l@KgNOHCnY1q zjl-qxS%_7jg4q3hoAN*sx(b3oP7mX@*No8kw7K&R5+I=~#(S3U|#9 zU92mNKk7s7o5&y<_CKQS3`n{^WeJ9=NGA(W41uv4_GtzrpfN_DdA$LHdisGYZ8(lw zM>lb|>jg27?bsf;CU7by0P^5bAi*8n}1^nRYY3P`JiVg6)78(c`p=(FA)a2=Sg(pc1>tn9$!z@1U68th3~%AuEbkUu`y zZ`FA(RG;bImyCdH#-LvQknG&`zy6;A}xffqYco zidSRoXmG<2Pa)Eb2xH?@^(^!dv>nROS-yyz$j^7x0U2WBL|2h-(ET3;|AQw^DVy5Y z6@vLFky#H$NlB@?wiZ?0Co%B*NNZaJgH8G}-BNuBfprRZ>Pn;}n5gfsR@9Y4FUwyh zVv&?YGqzT4qf&-}KkCEgTexFWtdd(uZ%`4sBg>pZ-CJ1hADJ!*+1MW0cA$YNd}6g? zBI<$QD-%tl>gl>$Qdhr~?w-hQ_AEK~xC5C<)3^$3yn%qoP z%_I}o-(wP2lXb1#;OxNsk`kjo*Jtb;9A{M)R#qET8Gq;nLCypoJIE~Yc0I((((@En z=ZXLnkI^}>qW%=5FKo?T=AkjIHJU-AE|~RlW@V^a9LB?F1)mZ(9(qrhoG`F-a$jQg z)i$)CJAO|jo$f@Ob`XumT{4G+-4u-D>i4=Uh_;zXCgwYma(?cD?Y2racV6eMT*xIl zYznztjlew6mqR~hhu#-WOB*XeB%8qRfbSnUfvcC`C>=5gzl-8OfBsMO8-JjFN$4eZ z4=1o`W?5^)4G{%La`8`rt*npEtt+q7VcpS&&HT6ilw7^%J@PvEk~GrepvBo`2%Oub zoqrwVn2+RIEjuwk+mN_v;u4Y@)HN zQpnbMvl^eL0BMy_rXH5~Xh&V=k*?h@{P|>?Y5VwrU6OFQR`a2+e&tu3$B8k*TQbz+ zAwpgb@nJgJ<_z|z{Fjvd3SuG#a8f;H%;8Rde?i-PfAz*R$lDLUE?hA)AU1KD%%tIR zozO5XG23eWuc6V#yK`Akxnmr@`V0y@w7aWBcC*|<|05Pelz&2f?{ ze9O3*->P4Wj;5ipq9a%nv!YW=fsqceGTH_0}Cdd%$_Iy+NkGE&|WIQC{j{?^nR z7v+viN9Ub?_6C-_y6groy#}Vj6PvCPbh2S{D7}glb7x?oHH10x~A`K7qMNu|EpR%0^sNYjgRWeJhV} z)YI6wiPRC2*DcH6EZQ4foorm8>WPwrr%6spVR`Dev$Ny1|MTZhLLNs19|n!uBXB;F zyrSZGmDhSg+bsn+szzpJYb{D}7|4-l9riD7l6ahGGj3*`iW`cIRo7I&260Tt%*<@K zZP9Qo;XC6(%lrMAY9}3cq)^wrZqL*`)~oB!WC9-sYC7-2x^z!C2|>kFFo6)I*%{}< z)lBrOkSx`q_tph!dST%Gy3|b9~%bM(Zu-y>0 z8zVbc`mR=zB6KL5953G0n2hJfl9_kB(wXnQLngW{L;LxKBco%*cACtlGrCIpqH1XD z+ip4DRuZ|*YE;ENBubN;Tz=%rFL8JrfhoYGZItJz&N=43L0Mb@-FWL+b^hB^_w^sFSm zyJb0MyRp;x;o9DTxjQ3_ZdDp3M*T_5uA)AMZOvS4A3^`bm z1$u{7hf6Ex1BM9)+7P59JjQi@G|Ljn4u5sfGLWC7Dy%hNHn(oYWgq{`i<2AwvW^aa zQ)k$k%2f2Kzk?(E6l*NeBY*Y<7%EyN>d4)^Gjk`b_&hb(E~{h6*F*5NB8z*<7J=sE z&%H4RVLXO4{=t=;p@TjhryiCrf@~?yP4z5z>Rl-6cg-0Zo7%z}Mmht1qyU?&O8ag_H)uLZf@5sIH3Y)$J-(Lo`5Sk&L=p3YW%ADr@9_%hID$35y z72hmFO)T4?J~|rAEb)II4n5JZtNSJxB6M07GFDq$#O}Cx2f4YcvK?9#UZ-CBv9IPP z)VItCTNk#~X#}>0E<1(h9zL#aF~)}ON|G95i@hL8b8X}>2tKPZF?&^FdmhDnEY9a3 z7z#f$`Ox<~0*%#2bn}{LvL-A0l9<`S;jy>a)sT(-@v2xngGP@OQ;XwF2e5iV?0tH-MsjYSUlDWYRUeNhc2bmp{n6EbiFj6$ zlz3$TS$QpDlb7rUGxWwR*s}YVt)fN)YTW_3v_xxXB}bsS(8YlyJ(tV8uSc<=(C??P zi)1~nngMlS&u+*nEItA5`?nfd+|)|asRFx9f*G?fvoDAjIA7=L8c|}gU2Ycjnwmbe z=)meu7F0BcIYwv`s7m`3{roEb6>N{KJD3=o05y_a@+)rcXZG=78Pe_j3sg&r<*Bk)?MBpZ&MkqY zPsK8&uKhf%VSTH(0VyMTQAOCWM!(JWh>^w5}vzWu_yu|-9(Vlm94=wMk#(miSm|%EB zz6c_q!F2x1p8e=ra>I=N1&nAp#YfZBHg`QHi4@+e#g-}Nn%7Tuinl|9uJ+p*{3D+0 zXt~0dzBxDf5w6r>Oa-s8*j?bRs@#B@a6s|q<&xe^a{WyB!9svY(U)&-$fUfl)SmH+ zJ>qTWAVRVcpJYUQvQVzj)?-B6H9V(rYf1*5_?CfdZ9xSt=!|W6axOOr4=oIWnV-fN%^PcM>?wpB z&r)&>dVBY_pGzv3ha0+vrCBTqP0OhpF%lmJM4{8RPSDIZNGr#$ zFq3S9=(%t$!{~)c@X20VcB$(469;!)&EcMpj?RC-2L}z(C#lB&uwFh>)qcGd6>tek z0wGCu0vuO7#^EWktP;-9C=e;B(QWo?DM_*26)@&fd8fwN>N0$VFfjMCuqNv>^@}wk zLUju5KjOD{uVuqH_G6*3@ZO?Txq4aIc0}~5^ssK3FRpFj)S1Ja!ST142d=iCnM((F zqxRa*Eg_Kq6=6o^OzjN64YUHy5$5zKK{xzB&Iaf+@pf879YUejVIXE+P)!p9v4Yw?qFyBkFL?@Rtn=J{hT>>;fa z#JP5mK9%wBr`x>()qg7e{fKwq=I88>Q!2D_28bA>!i&EoBRx_rQI5O?>``KxD_v)i zyy?4aP@5w%rCS?K8L-oZXFK<=PzNUcr3K;D>sH2)3rZTp zvcI`uCU3?zj3|pnDt@!J9gz)2Q?s=W;b}mWy-Tt^v$_O)JG%c^!~;qGpXMPoKPpLZ zc4%biyZ-}T>T{iWn|3$a`-)bdQif;CQzo_9?3X(o9PeF`;A?z_WuX~@xn>g2D(6Kd zc1Bfja(@jM$H*64vRN7%mCDIgV{-6Pnfs4+fPyD-6rG1p4iZN6n!R8y| zo3@~SVO~Nv*$;9nu0&$eE~Q^9Ej4FTbIhyU7fVG;@Cu_pRlo%)N2s(Bx3yXH{oCs+ zOKY}S5L%HB8mO20z4Dy$c1^x+SgBf+w50twx3ay)PP`JTHnV__`}ZpZMaF9$nyFMh zMr>O|q|!uWuNEDAxh2l1Q&<$+zEEqG7cnxG=H1aJILAMSbrOEJ+shWz%SB<6Ujp<+ zv|pR_g5hJLWTADed|un^JEAuDBeVwgHnZ41zHsQkji6M07l%Z)1V7_c{I%RJRZV>J z6oUZY=@(sW^(Q0+HC?#;Bi*mQ{IaT6yZ9obVekul;wpV284SM=01ubarmZ|*x~yAB zwL&}Tk|@iZWk4P-c@yr@{N&pdPD#$7)@%2C{x+eR)L~U*r?G%P7@4lq# zU?6oUbpkg>t|^Swkef22#d`hOxw%uK_!6@yxT~S>7h|KBJ6RmD7izYQUnxy%9kMpG zJcgf{(;jx^G*Ot~bPXYIr?+s`gaB}|oYdYQ<~j`R1-~TC#IayU(iEbMxdOR1N3t`5rbAeMw(kQ5TRCabrvb<-U=*cverS9?gI$C|;7B6`xeL~&1DRZ4+ zuB~G^T5OwBxBAfcCtk?uy(N#NqKf}vaYk=3(oDnm&~wvII%#fbLAj^P6LGQi9?-TF zc8C+7Z0^Xsd+^GPhHf{{a2!u zxlV7KPHt)0Ss>GsUDkDY*sR~wJxv4cSB!B55Hc^?s}MF?HY*oOf1L3vTcG#^;x5l? zFmGj>#@NWtMNI%{S6oq4+(KL_#|#MZpx|l2IjgU`tV~OStEcUzZh_rN?DBATi^awx z1e}JW2ReOfqhA?)^JDCA?Fu5ky+p7^HSXVzNg*Du(3+TC;WyrXe6c~Vl(KLdZ!xUD zIpcphnz!sw?7O}w^UmVeelWm{U05zlhwA5)8qX`m7u^SzV$*Jz*Yx*9q3@hk*U;|KtT)Ge?dvR_OUn9`Mm{l-CL>vtR?8U zIkqFE5rC=;AKy_DdZM}v$R=JOR==GBO(BGC8H#|eH@%I}dnM;RDrE$8@#l+eKNREaf{S+~MrMJ0y z#t1}N;9GY3+ByO*5M=%^2Vff2(FvjdAb%L=Ob42`>W?vq-LDXcR=5Zb+v1br0urwJ zi8ue4Avqrvu^lQb|2~QfaFO3TQQsufy3svnu2USYJhO=6e7AIsFVGTBVN^86u~q(W zH;IrNs=I_~RaXI5p3tA+Y@SLThk zk7k&bI;f1($t*2<3x>XBGab6Mz9}&}Lf#LsC33ZBSBa_cE~r`W>s?uTy(X)AM5=yf zKhT;pXW^Q=Hzk~UFRd#Di#ly1sUcLW!;+~%M$~e}}vIM-&Zjv)Ln*V8S zPy>-ee191Ojew-qnktatG;Wsfc^6NqPjJ1Ro{p5t2$`xN+imSNFQLi#&>eJ$&%X4d z_=cpYCN~x_=ngK~Ih*17>FlM2!;X9L0Y67VS%B5R5L_T*YyT1M{!U;`yh7c>+pgjq8&Az?-E~( zkx56*6<+KyPAW1u8IYt*VW-p>$f~#vnUS-45IcHMHLvNLBR4>8K_02Y{e@-**IH-Y z%O#8%;+&^kmlmdrVYS;;rfUh1EC5}c^dy9FT~vy+BXG-k3_GOa_zseNIsk+^3bhrg zeI}Vjq?+gq?8drLme=cZpjx?ZQACMn_1Jav+|>V`aV4_XO{b6)a((50;b+60Y-867 z5~2M*er^7nAF9RBG9!whNQ*RF9^m+DT@<)LM*sUeWj}8BM`F)bS)GkSMm%ldpLh6)E)~6=dt~ zY>Bdf82M4P|A;*+kgROfWoK`_mrFW`m{bqLN7+vItFPvE{cU9SCui~lA!#)x89ZVo zY=((YmKFM+s5o^+V?xwh7~kn=k|KfSu#~Xfa8qE1Jwm;53lU#m@y)(-L=F6?Xjw# zrJsn0+@35rAh2CGii?*3Vc6Bj_4~Ys z#7l%)AyhI#s7d@6P|2Dn;->d$pUdnyb8vJfM@j!5YNTHtUm{3IKBPShN+_e(j4P=l&m|A03H%6+xu)$`3Q}SAdRvY(!J&D^y7b`H7yD=F6)Vhc+0?9*c0kIt#KGY zMth!ZD%BROTv2Y)9|B2@xSl^5uUoK?fqIYsfawlW)e49krd{vtk(6En8DP4@M`bUM zBPUuuF%PqL^DM1)D9e@HX)?Xkm?U|Kg%h_FO1YJ#{){COo#lLaa%;`559CLwb4V|V zMI(QQqP-yR56(Qle>zJr8t;KvqVLrz@(flFiwtDj1(SI?J^sw3bv=zCnQu$|o&#k~ z-G|}Suf{%375v6J-UtYR+Y&ZT?##Ohj+!$ZlE4uS@BGshbPJP^zD6CMuejh^cs7T#qa7rjN?ZrqkE$vHgkIcNL6BIb&QCid|)~?6b30)}l zzhe)Qt!mqsp>0uAd+$ZpNZID63dHp8cDu+&T=Af;2xykYfW9%%+I_2LbRW7TdJ(I0 zbj0ZrQV}1D=w$zlPU9JOR`G@uX&=8Z+_Y2y-*JhrwD_4A`s2H8Ecn^Qul>9cVA`l8 z(aQ6eitrs-A2DDH%7HpmH zZ$p4GfDa0HcVZFWV~ZvO#Z=B`HX-yj{sTUeEpL9{sN z@5|a+zw8VyGK$61-hS?qe?DX=G$UgQ7KAa)-jHMz@I+!sSmgv+ThPQE8kBh_!POxn zPX{GBZb@Mb!|gK3yQaMBkb~kuYM2q{Sg(6Vo?s-^GvjT*hKCRc5v#jL?0Kp+x+G9V z!~bj_Jej|K#uq7eh1z@ma?-=Hz1xkt8E%M;9D1$TXqh|d{;$bD&SYfP8;U;+sc>j7 zNSzb-haNEp3zwIiuhmQtRm))Q&lGEGpTX6k;}S`zR-=2tY?YbRnaX%I-RC;YJE`0A zJyc3u<)eTrkjzO_fs~7AoDz@kwF^=?ozOV6+MZ==s{~K@qw3)A_)JF?-eOl$(1B3F(_k6HUa>CosCOl9&>Ax2&fzRRC zp(QMt7S>qmlc4@x8rUh+26+iySsSkPL_7~{k0zJkayxkmP8jLDw$a=UKbn@30QAl9xPc0W-0r6SfI56%$1cb9V2?MIq#+;*ayT{Yn%nv-cTpjFfwFx;Y)!b&cBwBQND z#siNX^NTs2ItIphWtB`Q9&;S170GT4{AnyNDLz!keJdW($9V(T!2H;Rge_iw3@975 za&be$z>t%ZW2B0FqbU)LGIG$BN#Wd7IILGkmxDz_L^h^_9v)LmN~Pp!pv)e zfn#+lY@cdTO;=y9SJtKw-v@vKYAcrs&S|deztT6CNkVre&^O6EQjWEd9OfRNB$=6x zh*ejtAe|6I%*4sR!dE|zE!Od_ZRqQ^?wXh-)w$RGBQ8>cvT=si0|c&{j{Q-eMT9_7 z29(fibs|S<;b)HKW#?tn?wz*?3#B~#c6jF!S!Z(os&fx^4}v;vH}?J8LNbVE2Pd+S z1OyK`ebco3cHAH8dW_uouKe@dP0#syG9wwJsS^j9Kkxq9-hS@5WRN)t!a)*+Q*>ep z2IAh^^cS85H51fEzeA(4=0@ig-E+0xmgI_$9JW1i;Y$T6<&k6JV}X<8Z8BZl``X1E&VqN)%R095k)JCtTG|+kB#p<#f@>F>v?B3J+%-%c>uFhtd^HFA} zS$cu!OZuBsV?6xbFxVAOZ7jo+XbfMH&FuGwS|783kbE9>Y)e@afmkLq%59LYMaT=g z5-bNhpH~fl`jGw&0QUf>9$102K9G6veAZ6IUVLTL{V z%AGibp^b+XUN!?@s2-iGe-R+^- zn07W`wh1FE4PdCkAX$Xq`{$F5T6XxjRR{+O9LQBEt$zpjO zwW-uFe5CDH1A-W&O}08mP-#LI^;nk$Yfj)a9GJwiz$ML8#uy*G6l5RLR$$X?qn;fM z@{3^85C#+F7eNA){3yLV_->K_n*k0X5g;xQncxeZvSYCBO@)@~58b_D)UA()C=`Q> z<;Ngu-j$g~pb$I<+p(HOqeC!v_CcywxrO zMz-e3^igVXlqT1#U34C5%lG2X#~#+80e-J6+<36tGuj4c6CJWN*uHR(EPdEnhn82Q zB)UB&XN`qGHjV2#>Y&z7sjIFJprDw;{k4>6TZ3o-@ z3If8Clqxm5g5Ut!=~Pd|26bnk|DFQYbdW%~o`GdwG6{6rq{C0JLg@D5uzd(?qEfgz z-qyI-aMrE&V-_^H;65|Y!6_t18iRBxeI}cWK)j1Q?6{xTe%}f@3b@=wND|c%kMgSX zAe3d#Yr9kp+x*-;uAFacp+Aca6wagwRCFaOvgOT%@>yiaI&5KWMwZ)7!0j0mf1& z$PW!X!XV*xXtc-(jv|C^`t5NA7?L=@F-f{z0NEBue7y2n<9rGVf1hK6u*kUeIKt-W z_qZIIhBwNP79ViJ5CG670cv&H#xHW4s&dQE(oOxJ$J5RTFAfbqC`5pk-A;3M;-L}9 zg;K5ASW)d6zoX^oPG7`tPMp8%! z^iXt56o98ZuS*Bta9CdM!J3io&IZKN^P!61YYykTe`l#1V=;kI7($i>8tn`;*SAap zQ(*J@8^nI`@@x)%Dc@FPrJGKGB>)*{ay^mOw6hg(g3)Ta`+?q%uWwD0dytu$ZIgxo zr;)^B@I<>T0fISzyE5IgfibBxOIK&AwXl|l?s;C9ww55Vq^EFYD8Ro(3C5}|Pk9vZ zZDFYr1+=VW(tA0p4tbMf?y}yE#AK*86Uu7RLBq@o1#d{4LJQ*3Lx>zY5~s1i;Q+cB zvDVD9|8Hy_iUlEG@P#(~|q39|PLOjvXWUikzL3rJWV1pn|6B9fu zWM_s;?1{~&1@5Xp{0p3R=UR9c($p7jqd8Sz89-C9*}1ZoZ$nfMnY%!ZPu`?^wsO#P zxCwq_-S3;0{xG5m-ZBG6=B-DrC*P^uj=OQ)fNiuZI8edsNd+-}Y9wyYU8T7~lvV04yyFSONujFO*W4a4{W9ugmrFslQ)y z29%BQ#>%CMkyRQ z^w{F{xS3mOEpF*5xcor~-;1_S;@E5uz-COP5pgR0GgNI_>9wbp7Br(qXHjN#%hdDs zj(Z7J1VL%ZO=?PMtFQU&-vY)l5iXG*SUCCGp3L53S+0NqytU~7Jb!)d?@4gl*=gLF zDhL+ZkqtI4ye?n*tXrYK8GaJ9&hRRpN#}v!A$wi7ce%>UwKlb)VEdxjdF81eCPMnZW}v&qSN_hS0Y{}L5akx#VicxCM9rasa6oecoGjT4A2v&4R7;5O8f7q9TgKC~#p=X(K%GrEe^|wC8%e1?ojZr3 z_5r3Une@fYymNahhnPNFR@J2yuJ^pQKZU`<>RZhMD3a<32#D+_*G0brNno(+8*IMb zDN>vJz(>qSJYuw;6R=2kf8dXsOcgq-1?y%>Ei*fE%8k*Hk#b*WjI_o2@#Smc_avn)}RGU|()+tpK^ulLKkRVkI=y{P_H zM9n6R`|4eB!bcQ9pe})IGXDeul^0?E zWT61$^6lw}!(q$Cd3A|8aMYtyIqAZb`$spK5<`^DHH&ll~+c!TYF|A;j(ZP^KiLW?~v~ z0qZi}qhBH_D!9FoM7s?FcB`=N=X;r1S-#*9sN?PIsFP6z58mMDl8^ga`9PFvEM9^i-@S5s;5IjO8n zZ%A}JF|KxS5G&eVA=8*b@#v@j(dw?cV?%5ua{m=t(tc4(o9NOra`^#eM!M^mF;ap*MbpOg4LG^E!U)og%jg2 z^{V<>4ZsyO;b&x$Q`nH_O!|Y0<5#r9wtE3^BGW;`fi~$Kc&cOQ{5N=FS0BK{9uL`g z#X$7E{kN?>aqs;px)}e#BNum~la| zf+Es~-HxV{AyPcbCm`SiK~xJpL((8TC4HQk3J!42dBoUFkhrcs$h8H6Vs;p2aF((< z3;DpePmfQFN8fV+ydyo;St%Cm!lKAs(>vDMx{lsIOFT2Pb$tl+{X}zhQ6N*hEO#Vj zDXD>S-19d?0*gOAd`<8d4a~m36yUz6By1E3jAC0a`TEr#Q>%%Kfa}}Ksb`z-wb{r5 z=N*m-4%F#rXvi$BJLP3u&r%Q^KNuAX2LdGO?p$2*x64@UM_P3$i3EHW@a}B5EJ}7e z9)(8UBOw)LZqh9u^;bVn@{Nm2lWxa+nHNiUrE_IBE4p>fC=!s9*K}#@4h9L>Fk~}x9oW!Oic5Y3DHO5b9^y!k{$ycJvFd4su0LaYA9P+@pVOiG^KuW{(!g=TdzRl6p5zIp##V+lYi+Ef-tSKHcvYn%3QtmJiP#qLr(Jy zgS@1jKy60V$H#*VTHWg%4`xKvMqk{|ffwWumy_CnQ2^`X(FR16$Dvc1z``jXyKoJ5 z;tzT!FLAsV+z`!pDG0F*VgKKVnG=zT+p#|IpG+B&FxC!=?`b_;I?%EG&)ij@?d|6c zKh$Hl8Cz`luKhN=Vdw5)vUx>Vllwju03(D};QiQ{uH9_te_h`%oJ_{^8CH?+{~DG; zJsGA8`)@1ka6LK`^zHZ#o9mhSE{Lkg!|A?8ak2E=fk6q>-5_S~U!n{oGXP8dADFB$ zvg-DiKL3DZp1sSNKHgRNYntw>X@TSjOlfC^oPKV$r56$ZJas%I9-RYJt&dvtp^s%$ zuJ3oatx!gg{s8G8Izp{H8opzTr%R$RY=Ur5K>?(jp92B`q!xlBp6`Z@y0CK`&4=vM z)1~$IZY>0hOexNyG!qvgZ-XN`O}FRUua@<|(`PX&GlX$4I4HlFp7+gsw&me?wNRtLP2Q)d`JSq}nA>JQX&B}jEeQ7e%EARgEeqWip@&P-H z92vEo1HP_crG%KChhjgDFM!6T;T(AxiMARr=hQg7P>TjOmq(q{UMH-yzAKdQ0C|Ux zA#e_6Hgav~uFExAkSgJz#`tBo1OeQzhMlR>S9QL${-p!ACjA;?M+%(OHJ{Zp(@3yW zeqT$@09Ln+cNT?zo(E8Qp-`|rC<1LSq^CZqSG8t90m2*y_z225+XU?c(eSDXD%Kcc zG=6efv@lknfkt)dHE8ucG2mY3e^u+%z|Gl+fcjugaf*Ca2?- zBnKd`W*=Fa?y*Q0GZtCavX?9;V;4?aCsPlU$`mkClV!f++)leBtXm$XRotY1Ao04* zhqnEPWvGXQaEqwNkbnpH*U5KFidGW3OLCcxIPEkfIqzL^hTckgrvMmEVCo0pl#E&v zWQO0>08H=iTy62%@|&ube=AUddz;OVoK*fQCTNof<$EvyIh7@f>_Q7ljox$RZSa1@ zcnzMJ;CCrt7ArmA4c#5FzXX?16FS;GmOO>pWNn*Ib|qv9ohez!w&(Ge0ULK;=Gp)0 zcT!=absWSSS22fGS(ZE(BQ3=IL%yf)tsa;4cEIrXkQ2?-Kj-Ob{!DP(U1(U5D^?s(Di_-U z|EqbneRVfXjW4mQZ+ev(pF~xD*)yiZ8U!AXOSUfOQQLEfs*iLfd-fM*>L z0)E2yY4773(-shV1t2LL^G#fb0R5rK;Riq zjV%J`#P{uAFF%PqDUlbKSoL?%3|T03RD(M0BHB#`nKmE%RMrS2Y!d3ja5JApZ`u=$ z4$Jt7O3XVedm=e5WSv&-gC?sHKPdrlt5y^Hd`~*79z2{_%3Lj@*#HA(Ue^Z%C}(Nr zr6ltm&h{gZNYHR6pvwBtZuTX3MBf)m`5X(BVoev*%~XEf_IxNQ*z~kr-jc5fUjB-v zSjcT(S_Jvq&e`%!u`VS14^8C-73nGw7M_HkYn6i?bm2+TJd1U!bW)Jwluv)(+ezvbNJotU5eGXBYoh!@fQ3*8N=QgyI^0%k!MUva<;Fzuq$2pHau#=h9`9z3tcuM>+(jqtd%G2qnfqy3MVg+j7~Gi7CYb`CKp6BovJ_>TDG z4h)O>l}owkn9De~_y2^f7@I&00a-E5$znBJ0$iJpM!-6w{u4oHUf%pZt=5u(9Xy&M z@T~?J5V4Iq)?wbXbJLx)lpkjq1E|Cg)GT6(|0OW|%LMYvl32&T{@6e9#d#fDlhHw{ zB%T?+r3KSU313wbAmlv=ko3gt)sgwzRbQI2}Htcvsn zS9uB&y5jdR>|&6B91j3xFaRs$c6uvx=lJ23i?sTnmZHT8QRKb?c0F}4}$Nu!xNyV%l5 zV__ga<=1#Aurpeg`2j9-r|HP_L?aD6tnFVk5ki)<1?vP~2v9N@FRbU#cml^@ZE)f8 zeTLf}IGp$e@i-TC<*T<-0*U-UEmCE9+t?jM=8LBtdB8|K`IlCu_grNdJ)@OmqSV!t z2-)0GvrU%~1X$8UH<9h-E9MoR#2dRKm)u&w;KZg)s2?~#_>upBW}BJ7sVCvT9bVVg zvX?hpA-qxO_t}>Q@NW0*Y7W|6HZQtpOJA^W5Pwq81Y{QJe+->p0z>EPhCj`YwUIS33FVch zg%I;&KB<}%_l#n{4 zyOPJ)3@?%XF=VQ9{$`Aw-{2m4ni%zg1zB?|TW6KV$#ud{f6;-tFy6GKB9n{blNp{# z8MXe|?^32Va^82sx!&%@;M-~a9`D7IZTrYDxuAIcFW|%l@*vze~DKEb$RW5g{Fl_1wj!o zUjDD~0vI^?X8YQ78IxT#!JNPv#rQstH^`8?@xF|D=N9C93s?b=I>|-C0QMIcdEcCP zQA@ZUN|{ZuT!y1+z`l`nzaPoTiw=U{zNU8mW%pO$f#JDraQOltoegAiHlsx1}X+g zJg^(J%H_9@&cSw(f3ez$&mhg*io*Oik#C!ivpvrU)WH)Cbw_D8$kMGSR>Lh&8W+8k z)xPnwZpgb(nJrKUd90Wa0?Gres|FztgBg9{MgZcECf{HjeyoxNYflKz4JDy_dS5+( z?s*WfF8~G!n$}sZ?O;+|zxt_!U;i7~_ zNkLUg=E?=L5ywiX#*yl!WXZ+>IjH6d{+}MjS`Qw@#-|FkwVdD}vMWs;7CPwlE8?ZT z35`lRh{NLTdB{BnjJEJTisLgJO*jm;j8f!K>~rrbApIse8t8I(Hx}GvM%Rj5hED8_ z8I5SM{z*s(IHduK1*pfS5qOD2^J7+Eron8!`UCpb<2XaUhWzLF~)(Vfpe+FrlQ@WijF<*p3f zS@p!@NB?jWTE`Njnn1}I0G&6)caQwE!sBfk~1)we0 zJk@wE7hMJ$30C*nHIsomY&aHHT(@5*?JvHoKt)rWOyocEzyf;Q-F5BukpTN2b?dZx zusBT_aKleu>`cK$J<0qvWk1jbS2@}b(;2P+E`O)$0Llfa zMwBlEII}Yqmm9S}c_M$i0dSHpsT)B-1t&j-DO>gq&ZjH~bt&5&qv|9Q z(G)KeXoo0-{tjX^vSU}H@|=&)yF2H34d7nMBql&&rg`v?MdKU7q-=8&}i-gI0ysW zp#M{dcmdfO*~JD||ER&{NP@yG$bZ?P;US=9js=S>`zvT&;jjT2Zo#o}bXR1}aZuVL z6JX7Hub;BOR+c?<$2<$f1Hf_tz?`pP`<{yevu?S4Lr{h-4oOki(a<4Bx5=wLk(C4` zEPZN0;I#x9bL0$*=;GGr2|@MS*mtf>%`geInM19(Ry#lq1S+LMeJ~iL2I1Dn6*b4y zBxwzwcZ(hzW(@KZK?QEC5uw{9(@YygNnjoe+@?XAhAaIg?^*@w(y{MsZ_!*Ct^8&# zGG$sA9&%L(YLJs~X6$M}4DZaOW9*GdsUfIgORXBmu0^|qjEY%p@JP;s>g4zH=lxL! zb0L9pAVSISdyCL46aKay+2$F9Q6v34f=Ts^8C`FpNwl~4`=$NTS^6IRn#d z`|(H4bs3v?Npx!5Ue3z*;&l;mR{v3K$M2Nonsuhv)#K%qo+*(Ph9&p&(l{$=*8s_) z;4~r1tviLiaXzZ>@vC;nHHW3pK&sulUds|Ox!1bL{TzO}mMo0bU5$bC&%*$)>l?VW zGCv3Ce^KOw<$;O^8m0m*Xau5RR*4z^cd~>MxrdhX_0hokQUCc9iAchg_3tsUv9V7z z?j>oo&F4oBBH27_JD!)KKB1D*BArA$xgqfg>tn zqAd5s-`OwaAA#5 z>s|Y0c%V8#|1L>!28jCvu1Y!nEBD8c2d~KkCfTSV^=?9y?k_k0RpQ!hJ@DdsDzT_< z{E|kXF~3AzBD%`=T=(Y7HSvQc6-tnxnG8Q=qY$=wKP-^gSK|YzRT}CbUnJ5W^=ehvTctJQ4@0NaI*m1&KS3 z2M`-woR}4c6^SKo)up$t3Y;o~HrV=cW6ikVmj;4LF!jj4RTes7HPAvcJeYA_-2e$5 z*yn~4`h8PA8Vv9SM-#Clg9`>_d<>f#!|`}#)zR+}^b$l4PagyKYrNcdd@i^Ai*1fs zKwps*lB9Re*(Tiew7mFTmjvf?V}g&DDckB80mB-gV)`+ZFyI^K(V(BN(%-uJQb#wD zDKfEW3Muk19N@T-!4)xbS>4VrouOPO)u!f14ftsBHl2Vu-IU_`XDxt6gh}s|-+9c3=05BK6jnOXJ<#}&zbx}WMC3Tw zkmfF!h6(s_YD4rvhbg!@gggz8Ju`vsyv_N(%EDjJO!XU=WS7P<4oVeazS2PGS?b7) zqm%%SL#mk_L8SFWe8`!)0S`#fn#yQ3uGN^ySP#>{10B$T4LrOMdcesIR;~#xLwT1I z^8^JY0O|mSU?A941(RRb&?bNh02xj}>w)eI45SQ0SWzb0b%Y&=W>tWD>={+S!EQF#xC> zU@{~p;=n(fH3GPK&^@6&EjEO4bjee;@z+ND_u(6I9`8%?zJc}*)jn2`{dj5&UdNXX zc+yJ&S6shC^@oOgO>_xYy(gh@>sDQu^LH7kM*@qS-tXWjWdL73V-em13=*c@>9h9B zIWQ`alPRN+=-!(O`aAo&AYPV2njF?O@DD(s6aN5eKr#LjtvEV3fMjT_WTGx@W!%BW z7#zd_ZPv-eKY^|RK&aPcm-%l1@?+3}ggpzNskN+y)C!z?=;e(GYwov3KcE8-uu)pv z2Z0Z`BH0qXI09HMn0jj~!9p|nf3uRZs{*Gf&_W#+1T3IOcn3~)no6|>D+3uN1e+nK z;9WVhz<_^Y*t6m71!POd=TI7`UqDw&*6Dc6D z?fC^v9?k~=(GV)C6(QU(0ca7CdSWssu&s!gE2mJ510t{>m(Zf1Ab)q=y5e z_vh-WT#xlB<?oJc>M#+%?khGzLr%GBgY@LuAXW3XXu{>lPO ze;v&h1DO6n1h0!}*eXVH(0XGHeh#WH+HMvOPmVg#POnCQVLw%d$7gUp zeRN*>hfv6y>1_<<8No4^%K;&sN~!dm&dX;no`w9@y!qqxPvjNTNzSa(AYGD=-g6O8 zwb!dt8sqFNPs#JTD`&2aDy3YQl-xP3Jj=+9eHtfK{!!Izd{VbbP@B4Oat*&`VDu`s z*{G4z^rgNv`SeD18QL_kerzyZvpP4*d{U*TcXkDv4uz z>3P}CYh%O8>0tKdUxJBJI65mIr3Dz&QwY>9Zl#x=7g-skBr?c z+nFmAuje}y{Ztar^h~PHma?x5hdhm>I++wW(S+q4`+@C_x%pj>Co&ouL%9{DrBRSf zd1%;zb34^}hgK<9)Bjo2FVBM|$~y}$*SE$3R{<;EKwbwJku_30$W1CQclclJl<1R- zyPRBtmp7O6FP{$|tbX&LUZvFHXWqe8<_Ik0*#>;?+&v8fc08%!$7e&6Zx)-_RT&{z zrD7)Q%cU1<`gc>NRB2jN?-x9gdGyFQy1Xx^$M59a0eza4LW^>w8Ygj$9)?@Oi{8;3 zU^FhwEFgLf=mBPbYK2IR{fNo z|GMtnIfe!FTVRExs&*8SOKK21*t?jVehmMPWJedse>-Tn6w%Op9&lHJ~f z9`|a**eW9<18CeVY-}5b#?PPYe0DztOmzBW(uGLVp}mQI% z7~EW_O}elao%GD}cYD$pJ05&#Hh1?odhAy-rhZJHPPhLC^3}_#^0*{+l#FI4C&~i! zZ4OtW3W0K9WYFTpw|AS$gb1Kp!O-VLGcXFcW^?DJX}4VqnPk zjmd=I51&5K(7@h|4y*41rm>9?dU-j^vr>xh+`Snkzt}*e4jjlGh*oM`*<}Hez zF`!|Z{dn$zG)9Pc^7Bdll*fHIKxc^a>Z^O-phO-Oj5ww|`wlA@XLA4PIJ*#8e;)>s z6b+U5bEuhR6CWy8p@N^=0T9!*d_jw~)rrEm@$A~fHjL(X~|H97jeO{xcD&DY! z7|BYtwBVy}WHe^zZm~<+TdVQEf9q93k;)c&AFVF_ph;8+vCQcKkLuJeM^Z++bGSL8 zB~uLw$1=->%a}Hv*AmUT_viADydw&Q(XOK^0UNNm^V;2Yp%z7TYlE)U5PInJiLwcdci6t}wu$<6t%aIo>6)ZRqV54i;c#^ibwlxy zBtk|zGF)S!)n+k?I`I)>Fa*j5Ed)*%ulMt`;V|CYmpC>kM=;D#&VMb1A~7_V=%_U z^&K29Cnjp7@m#v{jsNk6puTW0wx}`*ydgSsDKUg+W3&~#6foS$Bk!%quBw@Gfosd4 z_k&GMz3>_1&D-o&4Fe{KD9>e{dG*{AZLS-rQ-RjUyv~i zh6d>B5v?OSM0T!uePV(-cv3Aj&)fXj3!0?8FYF|7v?Y~nCXEKmbO<-RP(Dxh3fzY{ z1Ya9^J(FLdcr)LjbqF``;4tIm>rA^>Bo>P?yTRAiIz&4_?$f&VW##JHzgy_kTg4xI zntfmTGAF*+-)kFn%ZTR1UVq$<*l%|bezdARS8~pt{`+7V8{$S*LBO#Gm5F%2EG&-6 zJV_gRsU%|*i8T^Q0D%6tD~$mU8fXYz{XqIuaYteEZ}F<3oth7q{+lgM*fERqo2*l| zS0|mY!s2H}qDX@@LA;*Wp|TFGse~mxmNtp|`x{pa!geZsEck{*SgsvK|6;X!VyQYH zfwpRanbplUM^|1M6TN$LTm1q0*54K)4OcC$>7Rt^wr^&>MF!6bAG4XnG%xH+RnS(oahy`158Hh4KSke#{2bpAqu zXEP!AYM2zN-sgScpWiMJ9thoYs<@Y9BXhj#{=${x;j-uK`jds6UR7{nm2Mm z|3=(HlZAAwPNY!ie8Oo=glQIoz@ksV2bf4SMTt6NZ>BR`LUS#03-e-pXDa>9;$I|e5f$OG3CFqx?%9<7;TfK8t2;r-VP@gMVi zBkR2*%1ma>m)XN~*P3(>#e&e@_mL`>ZsM^YF1;#rr35WJ1VL3F?*nZ?euNG8lG7nF z{ie(OtBR_iUDmCnEuaBEz%lHZz5!Mnns4u(@)Cz{db+x&y!oL7#Pfn>>f+VO zgv-kvd;C}4@n1ZbN310YEOS;w(GIDD6+c7;t3Om#N+6I7fTGtRr1-|s7AvXza^E%; z0m+HjVv8b&H-%INzpS!VM7B4@k73;eSz!o@OfH4h8E=)Dtn4(*6j6yaxBaK)$ z2*mZ}PTzJ?3whx4q)&?Cl69&vQJFssLnyd!75S2hdPmqZX?L318_M;cwmfl~3vr4H zu@YNO(3(1x=PH06NB9R02>(C^Z*J&h)DeS951;Aq^=kz*T*MPIn8iBA^yM#X<2U_a zQX|h*?-IcqBbx#(J=>i61k9;OM@(uvk0?`8r|kO@%34e9 zqw_RT-7#wpR-=DR8vfOBWgsU>&7rtaMYYo7mNTE#QeK`fsb}!0_Myx5a!pPirEZ=` z|9R_i%BAIuvHK`jyLvFyw!Kf6Dw#kFQ2hx{_d_6a+_$Wqi>n1;)fki(7L@~t3Lb06 z06<5LlxV&;bRT-N(2x0AjD(Vn;I3`z_QI?MKH%>w+u%*4@ip*dd7M#jF>WnJyOb& zF~#j8TxNrt{$eHL*9$5L{ zO=b`od>s)pVPYBV_6^Vi)j#}`J}@ah^HKieNQ$!-N$_XF&;ur?A5)! z@O-a7$-k?p(nI>TwpgEmMa#32+qPg&^TE@$mYuI_Fz{mRtFp%(d5e3fZ)SQwQ?QV;wA|Z3u__9E4k*7&oAAygtCu{0sbNU2@#d}=A%w}t$LebN#a=l9|jfw(~H=fG0 zx?(CLQwD^u!gBGK{=SGfaT@dy@g_ah<9v$Lr?-hl=BIi@O`yh?_WmT{6Gtx-zs zb|Ps}jFA7K4KSkCJtaeIrVy_Hs3Szxt|plWm8kQYn~XIiX)FgIOsJMXnc zdh|vV={8e;0)$*VRYcq}5W4c-8HcfU1Mrqd$Xt1>EAz4qQ9TW|8wKT3kxvi%-UN<< zhjq@hz97AD*Pv(@w|l*;Z#{B=)JI7In(zoDt9 z4n*62&pkN1@rRK@%zApvl%Zw}(7l)79g*y1U5!su=Q(Gk`|2u@R+k{&Us1Dkva85xG2}y+Y6{ch2m>{S-!jI%l$N=C%=FeCkZ2H$ zvX%<5@0nntdKtE)A|(QduIcu?{xkL14Jv)_RI%wNYJQ13aY z*goZ)AlmU4f#hMCtU&Ej`&WJr4-?x5McXw}nXlo=ET?0WeZ{XVCKA8%y~I@T4LKtv zcj*LKJgbNCRhP)OhdseW01hwxI52J%V5Qajlj<`(!$y%&%ICY@V*Pg-n;P%#=+5wB zy_*Tj3u~^3i_>47)YO`k<=$aE%9Z)t*-Ng{97OLn)bA&grlmrXBnam|*z9Fg9GY>) zuj_pLGn@~iPmh$XXd1e$U1};|dq@dPtnJ@BvS7E9o*p`F9^<=A@&9UlF>xjt-|;5s z(e1q=>z6ol>XO@N?w29Zx?yWG5U1Rc^z4u1K~=Ay&M&AHP=ftM&`+?xaPL7JF`~!` z?;++FT~S^Ak4gQJ#WOe{^8Tx}g$Do*&;8{LY>F(mkMt{PmtrNw> z%4W>dW$e?++yU(26t zM-ZYz1RO%L1kI5g+<~P2fa|rQKN<3=ADx4Q0BCp+rJTf3$Bmuj8;Dl0OZ|}cOUXh~ z1a$|Ak${8j00SPs{!MwhSpULtJj@BMyR6H}GcFGjYWou(tnX;+Sw}*L&4uio?rKf&%7AByP?7d(B=y}pyr%-O~zQq zigK`vazVd8Tciz^1K@Il@8V3HrjJBJc#>qSQ)TQwl|kKRUP)+5QiV}W%F~eAPnz<_ z<{|bNoUvoF6B_Yd4?^EO0I<`58C3^+?-`WcHC$MNYj11ITP&4o5ZSr?Or$M4F#CpB6Y{M~xK4xjy*qXcN# zd-n;uDuitpy+1=?Q6)LQBd}>QDqpud* zlu6prq}B1h{oGDAA&FLv)kkjWElpJ_ubn2 zLglNp3wsyX1TDJgb-we>%(vA-@c_8R%V9kFCZgD_@2h<4DqnR?sW75Ff0s|> zI)V@4|1aS43w$<{G;O@@%07imVUYbyT<3#Fv9OR)RgLQSb6j}I#)$MdqWS6S8iv@m z9dQVPFXr?`oAO8*nf>LZ5K^wu67$4@A5FvOHfl+iO*Qo!cNWi1hfsqhWhSSK3|r@- zCL4CJP_>9eHo?mJ=^kU2u6`LLTi{*K@u}ZS;eVcMTndu~mnd!WncmOtt|iSkOX9$^ zmw)q*1M<_(9><|Q6Hx*#S%y&U3kf z=I1`2L5^SVXR~$}LmqTScoO5*xg%&Tm=XhE!hfyWL;*7-Ga1&lM}+=@Yd!m5C79)y zM=IM1R@S`d5s!2qT|BEiwaj5$n=B1n>O#?rvF+P;hVRFi?*MTWbQKxTq z)osdpIuQ`(2u2^A!ctr#gaMcx6BwrRIeJwMbbD=>QuU6G+d& z*h{A2EhZB39w9d7i?*)9f<-`uNgH+;W)kK6u4^`O7z7{yuOcQr)H!7ICg0s5V}}}; z=GL@UWk4?R=By&H9KRT0LEJwW2>Ct8phq&J*{2SBviC1v4#vB1Cv_Zwhrtwp1~=y3 zSQjm39qhO9Zfi+kIToSN#{O{STw#zX<)BB6JJKWj!y&lyYUIvgeil&I09OKVut|jW z_Dr!y_Uf`z-}xyV32A=#uuY+CAK@ZfN(c?{lW9FpXqKhTC69Z7xlHGk!`JtG^lpzG z@H|RG$hTn>!rQMKfLW*A5t~wzW^SAmuuqsW=)tzDiM@^hE+X=8wW1i0fV7-KIudE0 zKm@ae#*XH@jlhR9bJ1zqZ8aeS9eU0WzlXzjyS_dSLVLfD8YkE%e^hsUu4}%ba_f;9 z!}M*`i|X`1P`1POssY#W)$fF*yV0@)w?B1pwL?nv;sXSuYk;6?qT0xuJ(jH>PYpWG z>XAIU30FV@cECj-R=*VEjbu4Vpir~;WQHtX#hDuWL4><>5_rikfDpw+nh|< zCC%o6oWnVI)d;dKRTaRwe=?v&=`VQ`FH$eJp4rtS1KKmg6$E&bqU%6;GNuE++( zSWyFDK69U<|0{1&OEI2f3bu1LkPuPTG|w0PkTNx0e!a`ltnRi}j9IUmM}WduF=%J=5MQ>g$;+b~9fR}`^0eKWB$KiA%!siCIF{ju2n<{1$NZSj{!4<9lx zG=y{o?Z(|U?p1scJJbEx=_%?hFuVYGXx5{uxRjKX%h9?`VefUm1~u(-Iq(0g2^JF11AR0E@XAH#W`s#{L^{fY&o*{i6l`#BS_cSM6$CcG`M^I@oSIvNRqny)x>1d!eLy;&U4ukHKUJ0wFN5B@W!^xu|>T ztWW`%q9AtDN?u)^;iJNFi6qSpmIT*RI+v$PqAhzsZ(e9Lkke4L9lPk@iA6Ji zd>B8XJpP0D=o32b$VD9J5l1g|bamAhn|>H+d3@$j(0?k|KP)_*?=P4eDhubM`V>Mh zkHl1!jCfp<>qqTxVaNhJ==u41iMOcO&Qxi6RaMU73qdZkIb{%GVWrOeNBNz-_*>u} z!(r|b6m+(U-CXxC&AK5XNn`#E#r$Cq|Fh`Mhph*@L4jdfoypAAGn!qtS+cpsYgC$_ zzXmuWIbnVLPRrjW1`l-Q<>a)Yv6vjC1ok`;fh7NImHxTT>C8Re7NP1M=be#pgPiaL zM4#;SU%GTDlIe*+Tq2n!+w)!%;k`dTN!68M)l3o2hKMK9K9Gkue=~ps|6@A_bloq9 zF-WwRL;N2k+E{wvze0C!;)JJ_&tiqn;5DT@uI$y}GtOvz{i&PaN*Gtr$LcG#O%z}I zhF_OY1dKnQ_DiRKsG*_p%XxmtUZQ(_tbX}|ji}7gn8Ub!l47!a6CWhXg*C-v!zQBS z*G`v3k;s}G3aSyXmFtA%F} z{+1 z2491y%{XIojOz@ZL8s{~w_RPZQ9Zv;QBe}u$}MQTkaV}LJ$iS#`R>{ziqo*PY=-zX zbOnD>=26Pcr+RuB@$q~?Ypq<1`qi$-N=-&wgMxzcEq=;@2f{@uTpS#nU-yO-j~s>n zcv(?=k^=e3jDH4x;Cx}?Lw=Cf;4I6+v?dpROM?Xn9c4*21#PHZ2~$(kneNQ+@6HE~ zWn)`M0~j|Im6Z**mPTd}-!9(xDLrrhrc1Hz>m4Ps?l+&m!K1#>6H$$f7-(afkE;4>J|Gh#jmdm;) zp5HCs`M~Lwhetm}OQz}blEv^xPO_4c{?HT_BcG2Ekg?q7{k7!AX*H*B&5{d`kqXp> z8&p($QmOLDO^?)gZtwQp&#m0afY+5A$763W^7)lj{jwc1Gc$ZD;_#6p^8uss5E+wl z@4JoQ&!0atva=OarDD5E*EQCiO_m~RMr0%;{90o;YV4~imQHyElA#=mU0q%00!F$C zDLCJ4ZP^Hmh}>XelF_aY=-!wgm=Bm(XD=MQaQSl1yLZ>c(~wPC8E< zue0WxH*a#@Ub}Y9a$%5zxWlOC{Z!=STy|EL1Z)&$AW#c0wNTmI!U}oWG&C8aftT&J zC&J-|_2?=bqmqi|=AJ|Uz9{~@YI3@{txXy{2;{}ZePBX0q${b2>L2(TOiMyanzlSv zZ#ewPrLm=jHRAi_mTk?vXPq>Xk#~|`5%1nQejm?-Gge$eq6@&{g{>{vMM0-Xwq2e4 z+3@BX^8cqhk}M(92EF)ev3$DfN9$W&;~CxF91gqkf%eF!QJF>1e3EZb8aI`)n1}|H zS~^rrO!98rxS^n^m<%+P^pun@3JwD-1x1jxCRAa+skHdLLh?(YqB*&t{pNzAib`bR zi=m9cL3KyS7)%)*NJ+04s&rHX--V8qi6+b8Plx7v-wSeC%`t7C^)t-bbcJP&#_r}K z+U+z-|7xM}hxWwlD#F6T1Pc!&B@5ylH&oDV(11!pLhhm9n%83v-#1`4{zf$FS_*pg zT|Jobs1VaiS@7(G6NQ=~I)^m~eTikFw=6m!yJ|JRhgUp+fhi{MRQr_bSH_PzRAhik4r#Mz-c+7Q?b!+NTWTF!4XgZuf-73_!uL; z?b>`B*x%dDmQ4~Vx!l&)ROa1IT#};)xwvF3XSa~V+d5h*kD*e_2fDhtjSUSZ zLsN^3xqg256znFSzUWjYGxNZzV+J}Pr}aV%IiV-&;Fr1oWuE?=hQhJlRS9joj|>m) z>NU5++hPS$Ls_Do{}&4-dHE!GGO?f>&58_&S^`Ikz96(3OG;6BIX8^O*|ndYs>LP= zdM#1OF)`$2R|j&KV>x1_(C;Tr*z*n1x9aNa2e2Oq2|1@xZX;W@w-G%vJFBRwnt^iM z=_Tp2S}6B4+vz?C%=E@Dv-lYVNo!r%YDLF4t!H~AVa({St?KCLq$MT_z<}qmTa$V6 zBms%HnhcmNAnVkg?a3~HJV}^R1=iQs?IuE%7iNrmpUu6uE}~R(Mn`;y;hG8r^BY&L zR9m(#MX?%oRXIC%npOyCb8?wa`XS+1=(?#kp3Omx0rUY2dbeii9dm4VSC>BcFa_=J zY!s+`)T5`SuCq}x{?@zM4jqhuf>R+(_gPn(EZMymF%-zP4xBVA>=Zo-DZWj%L`#aU z(c_?wB|5lweA!+?Ebd3LF z(Tn0RPw`K$IQ?HQcDLdC?U5bh{tEjd>YtQF&CL?%#d5IGQi2NFU1+3sla;l5XJthlh1VgB=Qy7q*ORBk5_Ju;t^cDBLu&M0oh$_I<^Lxe&Rtd zhDwuR9=*xUtq$c&*}@`!Wo6~M81#YtU)w%DK5}JN^XG_&5;)8!i{mz-2Q;>}D(UFF zgYuJ>pU)B+5^_HX>LZK!q=W`^6DKDp7{Q&twpXden!;}@=W14TRXW8OBh8dZR+a@oJ zK_w9eucDHYcn}mz%dMp+Es=Lc4STXWhO6Ba)zmV1>^FM9;lW*Psf6kFQ5?n@t^j(_ zJ1Ffla1GV>TCV~qd2^tgH@3CqRxdJYmOlh%o;X+$r?A0XVly|__tD4SzZ=?+gotRQ zl=s>-!SwWWs3eN~{4TH|JEl=$R=2(dMGk4BJ(y5&ad8bg(5G;i+~o6ca0rkB3JZ&3 zEp6@Yba~pk+S>a&hC~~Ar)7(ohLE%Ai=~5l_^xydCzjF*i;ZgKO<(N{9yT!@!^9U z@>XFoSc8;E{pt3Eoz*tp2^03N48@yJ6w}Mg2Q7E^cNf=t)QtBG!{XzqC^*c7X3@FI zHa5j(W@aG~5xv9mXK`K{kDOz`dD#TyNo08e^KWu$>Vu$-g`v!~H473Fl4ql}-ULnu zO}DGMBAB%;wpVmvF~MS!1$O}Z-N44i#&b}&i;Ihox9!4on%x;JjY^04e9sV-+L=B) z{Mcx)SQaeROgc3ga6X+^cPGSw__<_X0YSk6FeiHD={XB;E(+?@>~fv&1*HEUIgbCj zs;UYX53jqz-ul>y6PYlWpiF{@MxKxFCF<9bpyhNsKg3-kBd9A~p4D;3f>Aa9K76n} zw`xBFW+UqB*QX)aPYxDcFyFx23yp|Kd-)Ov+WrCW-deB+Aw>rCFa3c+qm0~KWp(u! zf5t;NQLac&xV68(k91>rwyxYqycJuczN&eTU1pb|6(^>qZgOy_(9_d%I_~7dW{}3& z*%@xpaH27^bg^Uf0 zBhGGy9@aO)8f8{6W-DFY+zND?&ij*d$T{w;b(AeuX0FeoZ=z7&!<=w%BHq)$ literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/images/erc4626-rate-linear.png b/docs/modules/ROOT/images/erc4626-rate-linear.png new file mode 100644 index 0000000000000000000000000000000000000000..09e8045e15abf6b96c089d171635693a60aed60b GIT binary patch literal 50813 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
_8yl~6 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

BP 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 H0c80ad(Q`15Ip#=?z_OZ#$>nl6p`M zT*6i2y=K&yy8Pp|%rb{uO_uqi^4^?V4BEPN#%IC+e%Z>uCoL|@`~x=zLpNDtM{1_I z_;8JlmfwBb0hfEpMT9))mlVp|f`gjk=_b#`$aqr+i-+Pw+o0av?_98<{jOX;Tup2( z{Aa#b{nE(XPS$wM@>Dc^+1Pe<9Zn0M5$1fKTV7wh01etwLRY=NuFS-D9(L7DL2HVz zpNDq=_tL0W@ocN`V8HL&eAnop&l0LR<67kc6_#lvbRu~k3A6RaN!b+)UzOZQ8T^2P zn@=*mgvmQo^4yO!zhT;tK7V-C?d z0#6FY0%G0{7XFj$HkhuUHXN#mb-;oRLb30`j1X${3FhckO8TJQ8h`{l(Xs%by^DW2@8 zBr|dZxfLl(X;G~#1|;xJ>-@o=c|eSga_FMDW_?=N-d23??ALWXyt2TCY3d_9vMCSA z`yiSAb-GV}X{2B3&dP>wez<>1{#N-U+OjYSM9*+ELnhFcjgG7i9^2+e`;M$%U%)!2 zrY>u)kDcW%RWB~0swy^8cK5s7r3PekC=STYOX2S>V)dL=gRdP${fm++fk-sehRZo*G8ea!3xu&wx!@%vWJ4jS5cV9Vhz&NnH0oCn)l9R7#4 zlCM2~IMLf{v371W*#WG-A;>>w53H8Ss^A~Eic|Iv;1Ds`UU`{eO z)J;z)^X;IA&Vq4z!9z=cf!pMK{?FNdy&JUt$Vk}X`(@U!aYr|>mA;P~n9hd06ie&e zWDGsE5A0t!TC2z3NJmHHn?=n%teWgzSB|b2wFcx>CPy}Y8|@>oH1|02i1QZZ5$!*^ zYWTHh?@C9mNrV(#v)_0?p;28hGnAz826`ihn)O<1SP6~H!3W?cBRBv4$(pQ83se<0 zH?}%_8CfLp+o|)ARxo|badMSw)6B(&8t=a|f4A$6zS)4^PIzL)f;@3)sp)jP_(Kx$ zGV(BD2$FjLD7Yz^Fd8Zf@V0=2(lNTS(E8+6IOecHC?Md|$1>hk>x^BZM(Bh= z4vWO~jo3Cab;2@-SONqQxa~MZwoSB4P~Qb0kkARL99Tf~zs&QSm4$(YO-cmiNydK0 zk@2=M;_<)sLH!Hpl>Q~OuX)J3!KL^_2<9JPcYAd+4j>`j-i0SdFuYE#8}J!qz&qo_ z3w9FVeRq5ocfiU6jZqS(ccUkiU0H$ts8_e|kfK}OQ!tq1tYp9B5^Uf6ulce0 z4)6m2g3*@`b&vXj#RDB`1?7)ox`Q1^t^ieN+Q+#}(7oWluI;Wq^gFck!^-2V3hby0 zhOR*?q1~UDNPUu$l?sqDl2Vs){mSv1@Qd!(H(#{Bl6(REKK(lXwd(8H6K#xRd>^5| zXI~kfls}!v79*`;`8)RY8bbo_pUB^VujWsIK>UAvf2+UZehd6Y^KI&z(zmp4H{Wc& zUHu-xIKZgJ^>~*4%^R}`vkB`8Q~gCI;lIy+^S|L@q+nIOI>Zm)Ic@yij3N2rp7x8v zUyW~F&mIVUXiuGfi86CDi!pOD^D~QO!Yy6k%$7{^7 zm;1OtI_ZV%_= z(i(8q&l*-WzN{rnUg4l)w_;yQ4ogNPW3#)nqhD( z+y>oNc|YGU3B4C$7ZMO+5Mujp)uct_-|%0c|1V`w(%+2Wlb2M)tAt_#KFX(%KloUZ zBppINa;M9GqyBJVcTwGgPY3>(KLe;cnEo~YrTbGHgno5~Q~NlSMgP4ZbUP&R`zcK$ zjee*J5wDOeek;dg-lQtFU54?yDsCF_x#Hk`ER#(33tz_Vn^=9BrjP)FbHhPF8c~@m zAmr^!VG=jSZOanC-rio--mQec;eCTx1M5ZjWXR-!%+6$PZjZD=NK@!li25t2poiDq z>ZDxmh|I!w@N)=@hsvI4?uvACXdr2us0XmOF`1AH?{2C2Rr9mvUrj?zX3ZwIaVp(m z88^#e5x4l^RZV+$TMuLC8s*ie#qJtiz~Z1M8+V1`%3=|20Sh^Lu|n#_eCV?%wJ5pq zWW!>kc7tWZaO1}Y@y6VSeu!WQjG*c}>}{akzmn7F)7lN~Zo40n6rJ1whX3|YA5KYo z%y9wH`}|X`Ztw5tHxHnH)Ei9Q6Y}2`dgTh_trgzL>B!H@z6>Xz14n+M1Bcm%17z=} z$ay$7vwx-$LI{zx**^`)YdJTUe(KY%|McP7O!!$%8~F2|@TOGn)Xy10I30v8m@c1| zh>njINvlHlV{nA_fVMj7?P~|wbt++6H&tuz?cPB|9KP&$mf1XqF6)34GSu9B`sVM0#2`||%Vaun;2mjQ{14p;SzqvGcjE@j&g7O9Ap%jh$`@tm>d7WX)g;7cJcJc*E-$(w#-lC2&C+lJl^b7#*BhG;8zOHv@&3n=_(aq~# zG!-EBVDdDDAdw)BASX6CHsMqHr=%}((lXtbIBwmO-z{+Hh?a@`h!~kxGq^=9z}8DME-scz|-$1(L74arON z(auZu0d1c4&i4XUy=P(CL?;>nLT6{a3}NN37nnL@TZGRld+~`RHSPr-8zP;(GGPNm z*ssw_FBD&>gsUkl4J)&%mMNC0xAB(pAVTOF5W5AU3Gxh{^igW*@gC;WGD|Lx%^*l`o)Ms37w^OfOI}SBjXdhEw{Oh#%*!b+n1W9~%@obSvmYdq}-#6kr zO^LgS?SR&?dRi%ce!CRjrYv9a{8C=Bb_R5KlkgHUc45 zUD%cNpm|G*5S<2;(fp{G3nGNw%o_PO@(prV@>238ayjxOa(?n)-rh(`zS&4)-gI7h zI}h8)krt+uqCc3_fKaUKhb@o(8&%lo-_erM-qFa>j?rmV8f4vQ+bD=H)S4o#*)DqU zZNgHPFps<6E=HO=FR3->Sb)e((v9@)?r1Zw%ZN}x#%fu9m?nlhjbDrNYxc;JIr7pexc(_dzBt73yVQ3g*JvQOTdr(?o zSC>8E#}5L{V;kn!KoT1HKrZutDy$6c>Nsuh>_Tk^teNbL>v8Kw>dWh@2f>4#62{|H z>Ei7E)?WTzE?3)!{`Gct6JutHP%(eCtB}K#x@POH1Sp5U|CRSrdp&S%M?Ef)2H^}H z!5I!444Y6I85&|4uiX)Hs~V1T0}oHRfyc}Zi#%ax*Crcgr=j((1Tqg+~)#rhOcfu!9>r)*w zzHlT28H~(7CqnX_BhOWkKki4)56-K{sN-F>VcC+jfNG%kLoaZTc&w0244KDveLy1u z^dE3XLES;&LHj{$xD>cx=1(uc zoGtaAbv`QmSqx&qx+WIWIW_pR8AOZ)!WAItFgf-6(~d<=8esVN0+SsCdlCI6ll5Qw zWbvdHnPKXuRKP6N*g$KLbe_b_)JJXOuQc?u=`kkK{^Eo)^OI2J7rhO-A4uFB_JFd} zv~D=AImfEezmFSsKVWZ;*;kEyYEBI{(z+w$9^`W6c;trU=JSH`&hyIh*up8otGMDL zLP+q~C=6%KW{p3z?ycu7$+<_65OZ)DX*cBV73Oisod9o~*_O>NOs9_XP;#LW>Lgig zyWzp%bm3bOGvV&x5fRRP(VOMrg_{}Sjhn_1p`^_u-o!Cn*+v1tSx;XKDHn^GZb#?8 z)=jlO7m_aXdxx`yK1mX^*1i8(P9H#Q&V0s(!xF}+#5%-2#Kpo^n_^<7W$tDgR^Q9s znqN>@08|zfTWbwP#$&5caTprZRakotjWATFbQqtz4y7=_QVv;eDz_DfO5?j!Aj%wt zafLXAh{D!FhQa^`XT)6LLgFjVITaT+XHIDO#ql&w;f%6fl7yB>*Tv9uv2qRzr1N5K zy7Is65WQP9#P~F{5L+cCDVqgquxn0YM4}BOo7i%&F|%=%v$p3|_a$32RrW30qfVfR8W8SO4bUdFjq|iloVd&rSTf?#{tT4jWA8!4|j? z-B}rKP66~g+aJpz8|Mkcxx_rgL&WppLE-1&W#MeB6s%R%v25T{`!X`qDMOyB|F*-} zZ1qPg4Rp?Q`uK<^^gE1d8#jZ)Cs;d67Y%EhH$V5)hVv1VvZ2^y*sNF+*ea8-l6I1G zlk{~2bj-|I^j}rJ)f29(gVr^>0d9}Eka{+_9%q?}S!A7(oM^CaHTO&>rn-g9T=Z6&UNM{h}_;By&*r{A!yt{FEF?xP@ zcvxh@H+Yk>El zxuq%IA{<>AZRBD~;s7S_WHjZ?Af^plik>I`Cr9pZipacYsrGjrm^Xi*RTU!hBy{8$P3kER-FjU(z`Wrtq<12iINp!R}w);BW4K^j?Y!@^)I%7f? z*JoGPmOf(M#(pxCtHZgPEd2a)xRPUyL%dQv#b|E&A}BM8YK5n))ZfY9c*Y=waTHVa ziEN7dD5k(Go~Gi;dMh1xOY(73QCLdzC?@*ZvavjW)P73G7-lR%UE`ddQl3i8CmZC8 z4*eF76q9jGmM2;%A>)|Rcn*hN)wb>W^=suzfetBi)#u(Gj!WqEnU%9%pAuCcPQwArS&(933_ zsiDy@Lv*D*UBt*ZMQj-pA83qZ#>bhGZ#Gb9h;}@3b_8YEJ0t9m-+oMN5af>mAKBC3 zWypWPa{<*}c|;$+gUzm4zEdLLshW@0oHsjfkpC?&z~6jfuws;9EaEq~1FF}-eVj?D-7{` zdpk08MlYPrIAHB2rV<@Eoex)n#_bgyQPEQWld7`h4Fu9WyVyV&w^)#$c4XAdx6JY|ZYkNwaP%kaAUN5Y1z zo2JPJQlePm*mto+v6Qi-vDBXkKI50*+dpr5#`W@!(edMK^5RAqQ9M{Brnai4s-db* z!UnS3w%pd=Hh=rW(|^1r^Guk?jd53XI4u(?H6AQn?@qGgG$O3^oA}?Ge&v zF85^Q6L|3W(s*blWIQx0MhGrM?K$o#v6~4GSQUn~{%)OY9c*2^d0bH;mEc-%Ubq|l z1zZ`<3fCSl&MMCWWrDH}G7qvQgiOZ+;m_dm@cMCh=7gyEm(IO_p_@7+F`N-zK0Y?S z0Aw0xR*S;EbRt&w14eF|kniB_W0b0hDrQ}DR@hXp~HeB&4bOu z%|p#2-h(~cqC1SfEvxJ?vBukR=ghthtE@1Fn7EZ~uU47k#P;p>9dN6{aniu>*7n&s zrf;5@QZ~Cr+(}&V@C>kxIDdI#b|c|i6woE65-1h;DVsx*{(1<1BxcZkTVbd5-0EEH z#?`m8L&}VqE1q;jV^C$AX{Yd<*SEDp+Ki<-J|{jWS$_ny-FPnI3%!p#jKAB#JD0yP zyb-?fy}`PXyJ5JoxgqvVxG(E43{+dE?c(gB>0)t7g2lsV0h$jA zUTm40`Ttl03BtQx|H5Sb7%qpSC!P3-K{chh2#W@%ugscdNjW;cJY}N@ix^mZV@CL< zU8_)sjXzwbRF_kmQ@`p{mCiF3eHMK-Pi5%zuMezwv@8qE3#E0-4F}kKtNUbFz^<45DVbS=RVB%0*v;lgot9ytSfN;D zh2A_fD9Ll!Z->0E@PdspF>1Je*nJo|j5{1LtTH?@%r?xpqia)to(|N>`I`F5_-Zee zbrg0~cI0=IcNBM22mA^s2`F&Vme7;Xl+cyXmeB9~)Wl}aYR+QL_ITB@iYA^Uo+MEx zQYU2%bM7$NG}kFSUKMgn<%imVx>synEZB*#q~Kwi9sHddH(BAb`_i}C(QHhK3BxQq z4d*Ft^5mb!Ss(p@Bmg;#wUc)qv#BRMr*Nr@(gFCT@N|N8A}A_CN^%`jDXQd4;v5qx zD*H=n9WyDa8B4+(tn`&uwN|y>!K_~+@X=XEPFJg1vs$-WyISAn zlZ%dv7IwLePBCkFcv)F`S$T+Knarb!Zaix|w=%mjCpv4fpXHb!V&bf)P{OgV=$R;D zC< zRXX~m8l@WL<#YV|MxI%1W)OMLQq=x4&-~kp+sxaJ+o0RL+xXkM+we9ei0)cNXAxM_ zTp#ucrqf*JQtDEEQgTvuQhHKOT|!+(UAi(GGY4qQYRpZ{PRw!59_^>>>+dh@3+^}X zzq%B?^uNTuRPZcrQ$Z=MRUT^sdVuD2+1;%99BSWX|H-BIh-qq5|pLJ+x8k?mYvgA<3s$lQ5DK&2GhY4WI^4 z<3YngBV_|+V@AWDWrDHNM}xgIy$AuM=cX-xIY(T&hcbY3UrAO0`v^vPYA*a#20lF-4r0?8ymEk3IXDV9qZu(*J6-c2W ze>rEF=cvU?TC_B4S{(#9*fl&ks_;_3GZw8jo#b>x+k>-aARgvx8rKUL7gFMi&3k|R zeo~c%A~yWKQNXUBc z*H!J6RUz8IdqomDU4~t9UAkRrU8Y?!)J3pTK}64`E#&)pUd$|@fXKhpLvcbP*Nd7D z-u;(VhgEtXi2GGb=usEPEHLd6T`JvLL&5`NBy@9%G#2dkuJ^AIe3y5ZhPN2E!M9E* z8mIKUMJW+TkHAwk?5%q`Y zhpLC{ht7v@54jIuU{o;$6Tk~6Th?Hb`&J22f;(+ub z;#RV46lWO+M#km#-nT{7zvZ-$H01OQ9*>KxoxV;VRz|_nDqTNxm-5 zvVg)tzAetennI6mWKh5@(u8heQ1C^hbp9Z-fO4c({^;WqGg7(7_7n*Dycorrzg)ue zR=(0T&9&9_hif6vV9RjJatYsl)U*7lM|8cVzh%B!x5Si0Up3FyRJ;c#I z1WXp<@RA=a0OkSngZaRM$J`<0-V|#6U-Fm8EE>q@qBQ&M^XFPd$gCR3kK{wA96ZRq zDc9sRaukfi=58Hg__a28BwLO~0GfkJ@Pi8!;O(=L&&&GEkIf zzuy*lkIe-oWn@%;eZPA@vLClUqF<$dq@S&yaZA^#{&CKx5Aa#@>G7Fabo?)&-eJ`N z>ago@3HTgfA7Jgo_rHRACwCL&{|M?ZG8lzu_}Kta6io!{#K1^uaM)t!&|;+N~Xw!$9Ic{vlQ2UGVXB-cw|=;vLC?O z$~%kM5D=ajxZp>r0(`$;eh5~LAa@E$&9zM>2g|3%*(Q=Z_ovp{W|F%wriR(Zr#Tg+ zmfEJLfo)UMisnXn>&M>mEsXLZ$DY~*1?kMRX>Ph-Z+DQg=Ds{SfPE6$QE~J9y z1f_xZRmeXqFz2LAODjRj$QTwv_C?{3A2X>JLaq5AQV}rAO?E#MZod0EGG|8)gL_fWc{bm@ zb-pMrhaE}CQQzxR{PK<==<{wL>LOh`?z%8v&2({j`n5yz9hq}m zM8v70tiOLDJCQ&BoKVK9F4>%Q^>%d->B$-K}+ZW>>I&+F*OvGQw0PnbXx>^O#OmG|Tn8 zd()oqcv+bN$WlfQV(7?_A^20I+DZEv#@>6cVy_W zvMV_WBWGt0E2EXrguDv1c~lWj^8%4W>b#mMQD^wH`0-7ylKxBkcjf zUj-xK4KW^-#jJM@wHsYpZ{|qqWQ@=Lg%9NTd8p+8HoDZ56TxgJ=Dw0M@**8F{;x;A zM-&8GzJ6DAC!|ZMitU+bhU+CP7B30C!zL$RGdZ+ifLvSWn!XqJq|RNFJZO7axpd1g zolI{1>!x%c8<`{^V%TB9d-R{Qjx7^`r1j#6kOtwXB-DFTXKW6PGr04U zRA0^*fTTAu8|WR|X){UAd1^P)+3~SBUd%nw>yq}U-}O3&3V~_o8yj@#f9tQ@g@~6W z+3*!%E4DuqbcTd;b_0d()>^AfgB|g%&Z$o1gJR~M>(yL;?^Q4qwjAtE3RM1rp6Vaw z;eL;vfP{G6g|b*bRiDPzn5oUSeQa0k#vNY*d*TYBu9-`G{+Y_8i*ME8w|k4Xh)p5U zICdP77Ap@^%vQ5Z>5)JY5%cqL7Lj*&hT)^uIkhIQa~M>jqQPG|5zHZ5ol=E=u4jm? z#1xa011Sxe^%jh@ib&hVI$4)4I|fwaxyWDgMGNibdRRsE9lR~>6h8I5|ILl(K3X2GEDxz_khpLZl@ z4$({|fCAbWHdkPDhtm1f+Y9hhL zB?X!e!QKv&GX&$;2rLagcvHOE39nIEL1b|tbC7r*ej$77w-k<3OlF|OpD}^4jx*AD zvi_^O8)uth$!nGGRbqDxL%(|HjIOj5sn_=C3l9$qv}*zl_>WnC9>idlKHKmq>zlV7N@P{92?@d{|G#504HzcR2 zJ#^(K+v(pM$dBNSFpCw6dK^;=rtIzK{87d?W+1KqcsY}@iZ&1Pqpc(J5Ftk$bQBsd zEL;&Hsf98=LA;J{D4Ve*)Jzm;=eA{hgLsOmA=+qVSN5h~-T)4W>oY^`8%qTfdT7t{ z$Ts$KWqmCNk|ae?#7JW}#dM0(6C(-SNT18Ych)%O9%$rSUA64NyrUq5<=E#@xq)H& zU4AWlLY09ysg=IAdB1`J2e9pQuQ>rOeJo{3fnNdpa+lOHrv~-10x>6p4aMc3MBWx1 z&dv44>v&BKI8aoTDx2(D;g6>0C2Lb8HwQZUhEpw?Qr4;%?*cN- zy)OpAxHJ6nF#;CGpj8_B55o^0gIL|h`OXpJ=9*}V;LR(06|6~W7QbE`xnZffWk8|z zwkCGJpd4VQ%dgc|f`sGJjv=&uy^3VcXV1fLrC-~n0dlwcP}(?=cdN5XKf$XCMeg{K zjhQ=Dos*BBfP2|~swvnUm#naO5XhE>}<}+(4RwInH=Gv z2~4(5qq(Xd+YVGr&w@H_AWuea#gK<8qSS?y?uJULvDU9o_u02_tW@D5{!06dSfN@VkU8t21Yg+vb@vBL^_kY}9r2Nsq5PRvO%Kjji0135)c9%o4BWb> z&jAc@YVWYWfs+s_8)41aB0td*Gj)^888K=%%izFBE;$nHF&q$j8KC0%{_^HlKKZc4 zJ|&Ect`PK2tw&K^cGD$?c%+1Z$X4lB57UVc2c&%VX0n7Okyncm@s?C-Ojx07smFxe z>E>VLXBpavqQwylySXj1Fm=L>)*Im+0SkC2e zC(j41FH>xsO#yqGJP&N|dhsd~u+oy3K7y+-SwCL?g&Ch3P`*Ll&i|D|>DQh7Pvz&pQKxhIo{np;LSzEtYbCV)P0CTc`=uGNG4xMe zi-}knid4F#k~WPH-)eksY@|dwtHc zt9d1In66SNtg|d@r(=~t`~kxFWYTP5Sgyf?aNwF~#Ich}CE=B$U;Dt$kyUP3d194Q za#PLY=890$$%ZN4vCIcA@ljwfibJ+eVCA{W(GIO*YlJ$faXU=2&g*2(g>k3aqB9#BD}UBk<={a`UZG=uwQyn>0&XLy$jq@jBg~YNoY}TVY4mZNNE! z9WUhI^u;Y^b%FC$@8O>+yvjG82xdJmf)@bux)e!I&a$o1G}A+YPbjC&>0NG`{KyAN z%WtC+Bz;rD4?oBl<6g2l=S6;8pv`(E2yKEjPD_v4VO>g%3^{4dC|O(3(k80u7|Ae9 z4>>ixwD(3j?5eVR(cw`VDr>6-so!2L_{~c_{UP+Yz$NNODTMz*-n5T2{&N2aH?akT zMzbjB`F(#KbgJ)LR%Ctnm9Hm%N$n}VsxmSLJ#59fNZZj~Owj)Oe$CF|3~#Gj!!x1H zUnGmh!z{1}oL1RlE<_!3AsR20wTsV5@sh~jMl@1ZNBm{-$UUF4fnp}zSb{k%pQ0U2 z&e(h1jDb3Ex|>d$p7wx-q@i z$bM_m(Ya3aYXC%QcD9`)=G@~d6DDr5Yot`vnVCj^RNW|ybSm#wzAk}^%hT5QCTS{in25cTN`0IJdVoF%G91sRBze!Z;Dw|pHBl2tMop& zj`ZjxPVNca!+pvX!FeLjceY_zppX9d)o(^KaaQ`KF8%jjy=p8uF7@2lvw4>+DB$$x;8Ykq4cdKQEkcUR%Tc`Z=Q+9O;NK| zQ!8BO;a$BtZjjA{9A-)?ods^bwwID-_l>FI*Cb-lobqza8#QR=BjWH%^O#EbY?~30 zHa4Q;m(0uTdrDWAH#B0bWZrP-8%6z|iZ)8pT0PlpMZRB);@_UahY$uJzw_lEbM)Oh zZ*+9^t(Kdmor#i0nyi(?xI8YKcp>ftTbySyRAyy+sg-?_e*|l!s=ntb-80tlsys?7 z1(O^fl4on2eQrViqi9F}s{z%BI(O8mj zpTb9c*y2vBt#eeG&1#9>T0ik*ffsD1^7vgH)zt~VH9G_DYmy_VA$y!$%E{JoOy+)h z5KVmC6vnD7#p}=0L++^LiE(sqlK|JGN&m&<9#t8Alx$lspO_3on({Ve*Rne4SMoGe zTu?DqM2#YZ<}a;!0_vRCCEE@yno9nzF`JuWa?0(=ll0x}fhN9PUjhF64oUUOni`n9 zwZ4mvMTkUlj#$xr7d`uK?*|y_jp*0(70*6o~D+*F=B8bnD(% zbWbk4f7N(7ii8ZXU&Ynn7Ig>JNnR%NP`NKh1!j@SX<2f94qYE=$`TQ8o%`72t*J^& zW6Axfj|R$#ZS0#L-H>ywwa+|51h@iSc6hPO-#Q!EuT!Y16O`6?k^By~s!nGFr58%+ zm;hd1Oq0pmOQA#WI3eg{51|KBECc>_T$)+f5%uIN%oYKsi^pX^t!|1EM}%+Ag^C2+ zoFxsf%Lm2U6lZU^v!r2tr`29O11WSd%s%5-vCGqMu9ZE&BN1KK82xs<-nv%qwMVyl zT-nyId>*u(s}FDl<^Fz+F`~dc419j{97o z=Bv~JA|Im=dK>J_y_asMarn|}phGe<^Rr}G?HbSP`%f=wRg32~@AfLDoNIfI1p(VA zJ4wnIx>_>N73ywvgk_KY6{mo}mxK*N84G&hAe!$}!eK4j=6Uvnd&{kv$<~}! z_6zs1?UdQVG47B()Fp@I+xD2#kEzx*IgE4b*RJhdHpadUWAG1|+d@p2 zWgj4m{8{ZWg(L|MOOmZCNVn)czm#U{_9Xu$mpl(;FY`@&M#6_*l(M2>g}@X-r2h(I zk4n002H(JdMlPPkNRexC>2IxMwa@GA^|38eKgKnT* zxP@!mwryi-+f&<^+FxzEo!a))n%cH)+jIWpoC`R~zJi^-lQ&uKdY)wSh8-vP443)X z*k*O^d5&m-WYIb-wK^<0T`ckdYE?=f`bP&&HyVzND^rB*o&>(Z04OsfbmNJ0xAhoi*^LkLedZ9w%JdicOEbzJE41 z>%m2p^Pe&S(~$Q7+Pnb9p|LWTYE%(h+~hRd1YbuKg2@ie0M<~7oJaveR166U8qxhN znl#uw3JjX~t~5v=Qtw{=z|5ocwk^>5p#-PKK*w*nC+I@G(JY` zu893q{oW6ruY4Tye5PMs{d#Yl!*kv&<5$0Cyv8=4mFTVj#XN$MYi_Trtz=@8Jmdx? z6Qe?4pN%jgd4UzzFGwRJsL3*)SXe9-b9gTgmu>#A6;zu^kepD5Fu- z;mArxIEI&Tt<@F(y|ZNx#s~8k6WfqahP=&c@B-x9{PlIqmo$UcI0qX3vs|tk)m^J$R!Y>f*?Gp7^55cuFr)q;rDPy$Eb z#yoE?Kt1pqRgq>|BjpkXL6}4Ay~vdvh9BUD6dt&KFovD`79Gydl`NFOVf76nc)}P{ zFZ+i+I%>^}M|g%aeM?h}o4}%dyBu$vPUR8929Xs3De?}o(*^_oz%PUK6=(SwfcuKJ zm6{1~(hJtkpNKnV@CG2^}yll z50}oYO?kj?{muQf;6XDq@pW`3O!Yokh}UBa9Tp7w#-F6g2u$i;s78wr9O?_?MM~uf zMsZ#zyqL(WCqb^=N&yLqNW^Z!U8oBB>KzBZsH8>@6F3uvxBQ2MXObxlwI+MM zq@Nd`EaUgj85u^|FL!Z@J54sct+Vc+hI@}${~Goe+b-yo^RM9r;@w)kJct%9F^AD|9lcIVRnyrIBn# z&oPlKm+v=39>`h{H@xEv!Y$94<^Y;T4aI(oYJ-VS6_$$Sf5U*1Gp*ybYACsC*7q|D z+!Ip00N8hfhDsmtOiShIE zV%p!pYZ^6>lfv0_sgLe77HC8b^HZw zj?wxCZTUo5XJH&_tbq=-$8G_0m(LukJ$lCO4981MX{$eO-O1$2f+I+PgyRGPOD#*a zU1~{|qxd&RO>~$)|EL^-T}?tFzE--Hf|A+zMXnc!A|(dTip`Ka6H#K%!xuzjYL+J2 z=mDCvmOjTYQ&3ew_OtHQK6Xr(bMwp8FWw#LdnPEMlB|@Ud?O@}R;l zbhAACGT%YyCv?}N-?9<|_znd4<{btwZs3Xpa9-2Ln2_2`*M{nXQM8oG#;47$6=0?E zNg@74VW3yd#+dzrs6U0&-#}AN$Z+kFEY$pmCLAt#5TXD1CM?I$n@z01_{H!>T%;z) ze}7hBVDCX~TTnVZ@uqJK7F7F!06x={{bP3-bkcp1!YN9K_GEq zF`O=QR$+D}-%6AQlQlR;tFqm<(!ExUY#;Tm6f&HW z154tZ<#(bMmzrTSuqL}&TD~PdMrRrs2rB51vyLrxg{jN@2UG+RjI*ay z0-#If$UmSjP$+cFO`wg>9U{?I=F^{oLgWn@W)-Zg<;qZw&x{uUc@RP;~4h7KSl+Kd6txY z{>8A6*F%+?ib^VUEI~1=pY4$Cpb&HZ>e?>>kF@>JK$m(SKR|@g-&e?I>KhJqB)%?5 zUaMmOVbK&QcH*EyWa@kgNvblMrgRT79wD7ljWqm7%SDH}#VirVx3o&5>u5Nk_y^8P7eYlLJ!9?Ho^{RrSf z-fnHrh588S7H;iZ<19tBrrbo&_9c0}IjNzxDX}l~%E(4TlX3ia#fxxoLUMpjPP$K- zWph@A5>8@z_g}HBF^WVeOr#b$|Mx1+C7ftgr^wgy1B`AD)^1q@y0Tg>b*4oAf=CB9 zf$SB~Th203AikIMhc%8DnKKo$vf@%%Arc~t$_3utM4kU0YxbN4^5+;GRs=-?lQd~q z)etVEk;s>sI)zU5s_dZiHIoGWhid$L(fO>$@)-l^LnkNVyxs41qU~p5j(Jet zAo|l2GI1kH>uN6)5uQ5E(JX-mXiS^ti>%HyGK-YbLtPK4 zDymAFfw51lgr{Pym5l`9z^KjNBuPZ7Xyp4#uzlx>R{VvPMIVE+>Q<7}(|lD^D8b*$05 zo2)bBm4fB!`9e9?6Ab?C`mUx8&9tCYqlhNI*Ila02=n3g9wkMv^K3XPF?a%CC}1K} zvK@DL}+MHjx~ zl_PE#SX@Y^%J zLmyYhD>ie@9BvGy{#F!VHV_7qZJ(tS=kN`SxwnxR+R^%JN0e}XjLY`+<^LvIAlJ4( zo6(@dn1>2{Lk277TLF;vAt>zwf{o*6V|2N3=aESjFi^yKmWKrhXFo1YbcZ; z$=$J!Em^UQdxths;nSE6EGlNFxTjUz&sA828YHPh@tBFK3vIoJZMa-(-)Jm zeg^NMvIL5C&;{p$s`>yZgZ$b1UqK-i81IM(mY;o2!^q|-;kZCFL`>|@Z;M9wB_y{b zrWln=5Izbtm12;(1pD+esmfKf#UPmkv7P%0WgGR;;u`U2n#+E}19d8!-{hJq@roRh zXGmAOd$yT9+eDA*2#}Z-cNXFtXTg;m-fgnp^#-awG+Xpz(xAY+;zWu#RwSlsu|NBh zFBYb%%L=M{z1G1zN+;Un3z*S!RDR^p}-Q+86Uq{XV`0sC5$P;+$25 z*r0XP)GKAPc>Qa{8&LzVcjonZ83JIRtjYx+L}O*oAX0RgkGO8&ygS&y&GLiQXND4# zJRqeQhFcWvI)?y1gr-ZZ*5>y&GH(yHSkk^q8+9NJ-@_Q*&s@ACXdO&; zH4BT5we5`O9@Hb1ToZ!*=%pGgvkh0d){4a(hI)Y)*ibSbBgii;qh@bp~LwOy}&W{MOjydHV?mWkQ$ zyO~Sdc0Z6>eW6{tL@2|q$dgu%Oj0%aYuKOvIPLka$T@)$Kdp_?ip`}?z9j_epe_6u9NH$E8Atg&<zURK@l5zodbqNyv}wYLTeR7O8zrv z4C+0SpPb*2+)ZRiH07vIl#A03+oM!LLj)l9MH}$s$(Cdl7oWY!W-82h%JbG{_agkH zwZy%c7p1%Q99j#BnqZ}3P@?$iJZ&q(9C^u64<;mDC|AS}?F^TArys7e0{;cwV`7lfksj%(#R=d zoV)eM*Nem8r|yqnM3uCo^Zcn8pU+>v|z zOEHAmrGya4o9S}CgE_;NKenolw6JUbw7?q$?K`;BGN_jyHJkXoUn26YkNmD)Q{*l( zN`dGVCbKL3&mfKSJmAl$_o!4In8|bopLynW$R2Un6xF6F*yxWI7Jbs8Jh3iKfG1Ba z3HIDJ%%b_%(-CB_k*@5m2LT9sO7E*z{R7gFq{dFN{Er=lUiis^T;84zzGc~9TzFyQ z@#Si@^#C!WX;s%%CY#?`yYn&HaMr~Si~A)U#?yu5OS0ikGcyB6b`d4lqfDqyd~1QB z$(cAPlR;x$fKrI*kR}ZJsj{Wd+>$TP#NTo8o@+s`3fNAb1$cwzq-Jd zW5gA0n;MsyhV)KFI5?tLg%!|AE)G3p4_23S7cD#NroLmPMBwXUOnR(Bco#4>d;Lkj zsSp5te*XzuY8+_l@4>B<_Cl;^f1+TKgV{=jq5Zh`d8m za0p7xVDlPIgCeL$gKYZpNMFG{hHKu2+4?$V(>>b?%mZL=|NgAbt9;2n>1hg070Ho& z^XBU)g3PzV@Gbk=!ZJ34KX>jZ(ggB+aS0 zuw_WW=;QYy@fDe7jxxK1$6~fzr3kdh@CFr$Ol{A8_eM*GEkYs`{PMXta zZGitezA&N-rPnzRAy(k%Jm~}7cva^NR>5!<^cdWksqIB8KTl8OE3l8-&s<^+&Tm>T ze9>fx7j^29()T#@s>#Ii7nj-KR%*v}Q%R5!v(vZ9$wKy1)~%ZpErbeTpqiZ6nS=S5 zd*$)4OqjGiA=v&S9~90-FcCLurGNLU1Mm@F>YuymIV`t1{ud{Wz>gp}(K<8j{|?Z= z^M^>YQFKG9minzr(Av`}H59z5T9xN`K@bS&y7?17kITZNy=s z3D7AW6}R76L1_7M`bamIy);b2wf@&QnPZW11sF+ge1ksJUs132GCY(6UX#zslNcLH ze*hico8zwN2Q&TpowvBOrIQ)Q8UJa7x9OWMqaMrN7n&D*W7*3K){k>bL+~rx0KY9W zL+*{0mrL6=KSa)dViN}D-JV^5nZsAp$%P43*hd`8L8m-7`ehHYCH2(Q-t)@T>!pT; z_H+ED)i<-QJD`^R6@hD4bu;8SgGaPY_&KBeqEGMfYYNAQ(DbtXRnXvI;JEIJ2YDRN zzT<$M{>0LdZRM^B{910K6>RtU^L? zF0Rhz#`bWY>svlPp6W~OFIg1qO=3xVe587WOk{BZFlj$+?Iq4jBv3L0omFh*p>U>Z zl3_{pz&(`-M1EY8L}B*{s#xcxIaim($3K-5kNmVHOpOLZOW;Q{d8R;L-TClYSx;6L z6g2$z_RTYv?6JLOd*$YJ1Kf0K4nu<-NW_(6W}3WSd2H@|bli;XiCW#j+zkv8dO%#m zGW=fFipaA^6@Bb+nVh*9EuSnez41ByIJ`@)C1(BFEZh?1@qPR%D4g$)noN>^uf?2w zDYFQJi{{7tJOtTuoZl(WeC*p2JRsPMedKnsuy%c(F?4mu*l$tf03>Cs_b%aBuMBLQ zcm1?4WKY5mu;kPwXRn*-!{a!o2Tr>udw;G(2xW3EZYOvN#xH(bW}&>WVPxW}5ybY> zHnQbTX2|aVw8Dgx$Wf?qDja@s^z`7uI?kIdc?}-%_{{#2Rn=pgruy{TGn?c+BKg=1d7n}rUI%lU%fg5BOyAjQ@ib*3+qD8nI2Sy!C0;@+Bc1rw`3 z{Si~@W$EIzkExgYDyY{JU61xnRMI2CaF)~=YWoXn_GsFwP z%Wtd0y+<<)!QQ~UzS^xHPBa@G1tDLzhBy!7cRe4uU(2)f+h~`J>}PfOU2uR!A_Lr< znrk67p;ej)MM-P2e=K=cumR>^C*Fj~4#UPZ*|ma6PdW-_#T4KDe;~xWzAJ$3DE!Ii z_50?VXOtH{d|<< z_Hg&CjAdkEnbkGb4G8F-Q?an75Yj_^QQ_R+h?!vD#;Xb`J(cxv7#|gw<6i+fvZEml ziM>W#0nz+YGLG(g8uiGNrhXhUy)$~cy7I^n%gLDemA?`z?~(|_Li5}h#6`ZQ1Lj96 z-V+{{X9FIHcN$t+$n+}lVVJ4uNDo_@s{04&^wyZE1*6r`sWo7#Ri(Y5QNyZ%xyr8^;Pew?)4l@I-H= z25{pZw>0$><2nr0B-ULsc%3f9KM03$k{faeOrFiB<#kJnBDjyfD;u zFv(2P1qh-hVI+J_$uuP5)37wzZpM9Ew|GO+^wm{0aC0%qj?+S-Ma@?&TrG;bY=$TQ z#c9iz;~M`YjzD30z(jR}KYK{5lx}O}(ZfJYPuHNc5XU@eXmMV5Ym2cGZPd_b!p_Jw zl`m%#D4OJ^{i#8~G%^8D$loVsC7z*rN;cnHDK zCn*(qXQ%acRjbZcz{@?D{B_LtkWFMMWl3#mX^H(`l)zoKl6iA^&EWLx^z8cV{Otbx z>)FlV=#lq>_k$mr2b!!yiP{1l#d zyO{6s?wO7`kNtkHq*xwPK4d-oyjS(A@k+mE@|e2JI?~r==~|iH)-p7Ic6q#5s`Ka- z`l`ieam)9gI`}34uHBFgcDx4?P+Mc#fWZZF&A3men6f|d;XvUSG?J(AYr|P|$$JJj zKeujSxX>J0`yZemtvluwPwo*jGi|myN{eCder3*MpkBPxz%jDwG2cCQqLJxLWP(p5 zzD9E(ND7DSt+CU{*ELLaB_BPfaNiaTXTcwNIH4(Nu6(Hpt5}A-t5}U`rtY z^Y?o1E*7_NXI5Clb|K}ih*lAk1#DeUn2}?V!`iH=H3S`; z(@27eToEWTVzke|59yA0vhrMxu;gk9%ijSBeGJWrUH-?I@rw_&_{K6KS%Aq{#E5wI zygtJ*f}$D5hR4#c0yK7}fS2V|2XER&biul7A;hw*p(mwQ%KB_1mgg_?S%Z^FdEMtQ z-+DS~dIHXvxZLT?l7N`$$H0D&Q@HySnKO zq>xyaNrq?=_J-e!O*p|qW%UIPs%fQQMcX?jwti+ODU?uijXFO8na0p5*I|pz{4A+M zmX<`XcK@w)w-9Y;8Sq&p@EMo68FSx^;ZjjyZ7T$6R$<$v@2$_#PDIinZf9S5*pjIM zt{&KQCNfBUK(SeGmTCkUK7oE1<+kv#(Evs5s(@%)y4KwInAgHW9glN_AKM?ra1LlnG@}9U+lQ|*B`7&sa zgp)AOAl`GPTNqFC%BsXIxv*6(ON$oxFf7p{K4x*Kq(JrF08H}zUd;^a8F&8mh;~H3 zHbIOv$Go9-f-s9asziPfkXrg*CuC!d2Aur;Xksp+)bG&<#|iB(sOJfe>k@Ji_=l)A z3eHm(K(yQf+y4(dOR{OuEki-i6sO8?=k;CE6KP#vt70Y}l1dc^@$Bh_VgqFAJi-#3 zrLF%o*6!c*&QCJ%ov(UuqURssV;qqTr}PS6oSKeMLIF#;FtNxWzu}Ulql4Z;kK{mo zf)RFfd@cFC##bR0Fd$l_j18M+Mc(3n#x|g+09Pm*G>>&BB?Vf7X~Q_9%pG%j%e;2c z&!v9~iVZy5k;B*#tzYL?v9h2!&PEo`nFZ?c%g#ENj3OBlnuu+azhS&@K;4;H@K~^K zjMbeR7Fg5>S@ezHx+sq38zwd&l;RburzA$tqaD3 z0uF(Klsh|_W*OR(KP6K-aLPe6M~+Ru1ZdnrI!mAJZUb4lqodq#iEcEL2`!POMtqq4J% zUB2*r2(O4skV!r{w2g7rj3W7|mCi*8Nd(lXS+VuWq7f8Br~fpmzuUlp=k|;>ju@yL zr==Ldy?{43deaf!_S7`xB&9z#4~Oyh5DniZAPt1>ev)~ldYva`hP}0^A0?UmRYcig zq>(TL8FiyrNc7_k`XzZB*2$)`i9X~l`fX5v>BP0$hHdP4yH2`8W z=)qxCm)68Vx-yPh#Td(evBXYk?xnH+wTar5+8kY_!$Z4iAQZ60p~s!)!3wq7DWnAd z{zlV%@C22j7Wm^`Cl(Z&kHp+(;274Ql&_%G+>fF>8a*^Wo@dTv2X-a5!VV4N-ly!O z(+62UIfuIyuXkKP?o3=ETh0ju}Aq(YaGN=Z9ek{E^ zwT<(YlQN!&8gY5@AkU5b?rSt^awmH&SU$iyv(xgkgkVt~&h`{<9}_E#?ne^MYnlam z8v-Yni~}>rDknx%1 zji{*zQnXTBb~~!2>f%17DomH<8GYgO!>}T{mLJ|up!q#N+J!BQ4dF^gPMN=X53nNKQ3+}-cV-HF3L(9 zA_+2*Jx`li=j8GVz5MSH`mv0sZR}b2sqrUm)Y|G`?2@A8TUR#EkFm*u{tdgE?JbAu zTAPy4L*E|GuF$vUntsXCmO~8#Qxiz;P3d>kkmy)}noo8nvt@`MS-eAvS|!3+#jpi8 zsV9m&uIF=D0c=+DKSS<98ezr%2TtCVG4! z5)P|oo4x6FG?4g6_5ty{#F~-gkfw-e!87TpG?0{3FUD(MylSE4P$fGh6?r6dd6-D~ z1h7LTfgzD~$0|w1`aB>hJ*?$HUd&!pkw>Qe%Mj65W+CBznY>z* z=@_(-WV&<GBs$TvVE}6`+c(W+FNRsLj@Oj!bT}9r?mKP*x7l(ozT~+ z8R*NAx=jS__eWn2WZ9lUp5~u2k!?d>?ES=r?JcuIkt#9l1(@O>&yHvY&*_P@kn8}z z4P>Bpg0g0QUa3TL+HlA>26FFaSgdS)V)vky%_nHOk4&aMHdd(FZ`b`l98kZZJKT&k zkJ%1C>4OO^8PA~Z&B6|jNZ9)3G8*dx;1*VDLkd;ZsE7_v5A0&X2cp~iyY-Th0$yWA ze`+1!y)gun8%A!C!izdf)(Rh;l12gi$zoIN1~{l0<*#TEgHU!#gzzZ!Gnnp3jWhdh zP`PoL!;j4AFp5veJbR%T*FYU zak_bZaCmNs?I}}Z>U<~`?XY6y(lAKUTB5=q+8NLAWpD{d&Zx%;{>%gU_g#QggZFYj zy61A(Q;NIfNUeQ2TBk*bRIo}?!=P21wSR!08x$<`!4@bv5c4{xmBM z7~vQ;y3tA>o0Vb48b2{w*r$`-8)l!|NXUjPO}PvGTa-K z)@sQqcA>X$Us+MT3dV`e>I?jm+-SYth9CCtg*4KR!lNQscCR)X6K|4$X7KrOc;OMgskhJl5#%VTSdx z9VwxkA7s*LZ*qQU10*H0hLm746(9xlpGg1BttHb=qtO3cLFvyYmf;o&0w^hU`ipl} z4Di`gNUw{axmQ_;*C*cqQ7+CITKIkm?$Da; zkdT1k3$W$mh}=H^jyQv#Rx&k(z8Cg`kJ(5sN)RVxCEH&#yRsq6Ohc2srWk=JclxHC z!%zDksjWhp^%oIkBq?wU8WCP$YK*!q_LCXhhR1x3Y3|FOa>ixA>TuCsTSKja28uiS z(+n*4d6@Veyg%l@*@y#@QkZl6r|{y-`NellhNE;kANx5=u-_4s16p$AE@?2NjT}nO z#cy%kf0jx%Ewq$5*b9mHdafK2E|>!Z$We#QE{zri95uE z$5t#hEjgBF)6zx*!tO^b$VZYU>Ox(q1vLlAM*x$wBVme&Ha`RV-#;)k0Yuevs=C0j zg1fWGi}FKXl^sp(aRi1h!KezPfq9IJ7IB#fl*yOca4uox`_NK@*zhu=9vq1$h@TF# zuo@m4q_YU>6*k|8LgT&EDr!Oqn#D?H{I8f{m(#!qgivxoTe!pvB}&R}HAu+twICSU zDvZqh6Z!O=Pb2*XGZbn=gv&Ki6g*19uFs26XBF>XWO0&m&OyCniJ-%4^pE@Ue{sNMRzs@74$spwPa2(;S@KNsjQq0SMAi&|FG;W=iKUf!d~q4%IH9Ks|=gs30qg!GASTSUK~%QxKIf*24V5-?P@ zLQQ539i9KnE(FJKCY8i?J2r3}{~lYDSd)m&`)j?TOWu*-O&S27Z|H5ulEc)}L4^3A z2$~_m*lfaeq8DL?5Y9s!76&K{MTbP31Crj$^)zP?2|SBR9PRh%0F)M=ISvN^&*p7v_E?@Ydji+hOSuLO`zV7o_+`GdI$Ltba4uX@!@r**W= z$!-LD1bMI%zDGG6SgF&>JOz$|6hIzL3_S}GcncvkI#1mp7%kcx)xeMlM7@W)d1Abs z3ypl!wC>2tlmz3lvW8q4;Z4f`+hIj8YE_VSo~RpaPsFCLFRgDMMA$-FexLaM51Vb# z+pf`smRJ*aq1?lT)*NfQbhaW!6qe5CVz1Aev5wY>Po<1{rr{1u0;HNCZ~(APbw2pH z9_u09`3rv|f74US{ebY7y5eAy3p3BHR;Cqm0gTiezr8@Cj(y(HJ-uR8!}v@B#bwc- z4DE?2t74odWZB@4RXz!AZcKfz`hgb;-Qkeb|3I$>phFYhUW4sut7dNdZtFNItWha3 zZIB$Go;VeH_?K!|vaS7N?Qr72*yw^1aa40y3)F$^^loKOG3XA87)+p!qL%-483K2T zJP(Y_0c4Z0$hb=AFIHMo*rM^zmAtJ6;oI3d=b#U=ro$n^i#G(e7J!jThhpMC7fS+t zv;o&Jb)ABHg&352EeKp5kKOTZWNaGwsQ)pE@xjFj308IT3uK6s=IBB8;zo0>1zp}x5{mi-5L0(Do@^%4Muc3v{Y}0{e3`pw8J_w=^qub-#E!wIIhLc@AD11#r8>Y4Tr`&V&b7K%Z8ONcpnESgQjlz$rv}qUa5k9 zfhtxCOPxnV5P^9GOhG|?s%rMO38=P8NIbsza>YjEqV3Bv9hurlSn5pP-9eHVfiIX7 ziu12GFwiV-5J+fMz)M>`>5_)lRlYU`SJRM48uUcex3DLtPNMqAO&ro><{(ffp5d(j zjw^WhI-^Kpi?rA4+z8XA$_>La3y}N|V5K|Lql2llD(tam29>K}d^@9u-Z7sp&jCpd zsD@HPRkGMgEd8@Zx=ByB!fF&^Nb#b&T=LcE_{XI`NL@{r(}~YI#qQ6H8!eYCBeKHi z;qu8xY?;Of9e}UC$B7*%XiCJxu7)Lat;LIX6I(xZPK246P7RU=h-Tqss=2bckNBfk z9YkyJb6u?tN(s#rETj6$3`kMKqef!@{&V>9V_2rjK^3~>buSs8+OGIx@L<*mQGGk$N35j5f@-mE8YO$nD6suBr%#i>-$NmLYFr zAPU30R2SmddyhMnCFOI7AKC}nAOCY+j~?NQmD0t>GM%Gy*%AnlXV`1RVW2knEwfU_ zDyWwcL5^;t%1*G0+=)Gp810})yMu~4xA;Rw(Bj~iXV^yW^_!=6gwUvAPtnZ!V5vI8 z4|s4j@cxWfAYNIX(HTJNsIhOa!H4s~l4 zf)C9{@@=)*_?fzJg2zL1(rHC>0Rh?*TLRre^64MGXO-#Tgg`p*iyC4P0Eh6F|$c65lmdvz7+;9XY$EDi+F`4lV9j|>tg9ZWV z@h|%|uXJk8K?1Yw%cB^CC$@_S|2#fGNb^FO>x;s^f7mH*C@qPdaG3(O^T67)zlVKH ziqRftC-+bco0z!uk>c+b&=Vy1mkuAR+=X8QYU)6Z314ILZ1S!$J=zsQtC!$Fu7}kL z=<0f>h(~NA3;U4Za*yv~wA!iNZAyin*G^FSwW99gmbGWEJp4DuVwu3=t;d&f`$D6CTPl-8!K=NuO} zKJ`61pkI83H=z@nNogHFeA-yLKd*7pA(l2y*$$J*uqIfZFE0T5i22zNcfbg{q=0IS zGc!1^XwylD;e0+ym8d2a+D%@k+bV~45xbO?8T>os4gs?-!^jL2KHI!mzXYao+@IRb zs;)XIR%0yoE|fmFipXaR|m2Y9l+O_6Cf_hV&jCvy|Y$nkjF6D|O$ejt8sm3Yp6Eto@bm$8?pYzy|GU3CI*mp8>(S zt~`Fy7~j~tXG;r3*|+t|GSO=6%%>=?&+&CN-2>Vq%f+5PI366ETY|4wb13RE9+~}x zG}Th2dW5BMx}Cvaor!#L2bfGD6?<}tTH%Xi6*c@4RT_XOi^{L6Y%%bu#tc#tj zUIs3P%sl6j&mYi~4>=gY2s(l3RIRm!%7^OPDK}*(URZ)mt@1YBsU2X6CZS1LLb5rs zEx>6n*mENv*)CJ55LWON7#Ac?A^Bo9vmACD3|P9|TVWJRH+B(4m4hBk_?;_WaHgc^ zWr9cAK11EQn}c5RAEJ}2Gd3oh{2)91r~(U|bZ$$U%gkzDNiBjw*5xrryC3#a2c{8- z0m&<*gow3!YHggA!-I;n!hYwQIqCzxB*5E;I?^28qTCbMTau%jEzz06e?~qOV%K<# zy@x5kC*-|{UM@3*qcpuIE@0XoThgd3Nx9;-(K`#E@Job#}=?tCu z6+hcH#n{NspO9^&*E;&y8|KQR`@h=_Q*Qs6CgBAj`2cu5jiKc{PMblGd!apn$>LkpC1r@m7 ztmqt^Y8<>iZMIzaVi*4DSL>EDmpZoJwo~4dcCaX--;(o@Kft=WPbQ@Wo^ORn{gjLx z|8bYfKLKHa%0XAQtIxl2Pmg;ZKbpi~1C8vUC5A((XS1*KP{|$#dvc@u(a|Fy4T|ev zcV6_G8F(#37ms~HB6iSCuCVb5iI%t*9oTz5K*z|j_`MZThDqxfTcC%AYxKS@yst#< zZY2JDbV6*KhYdSOZy);~QEj(NuoOWEBOc@pS3tdh08__5v3SCeN8AuUdh~sCtrp7! zNS=RO-?>g(R^`@eM^7&BKCxcnmyEFg_<^y1ME|k81`2| zvRiYx?~o4PdNZAsXmG>a)p!hE!5fIU&@f5qx+`&h*SUYfVap)=k<=Dk+=7rxRK1h9 z4gfMLRLsB%l~gv^bZHtQf=gMzVy|N1(pIMv2~WQti2ti+h3>Z9W555#{AMWDEVh?! zmu8iQlm&=XsZ`e=x+q>l|I32wVoum96S6RMy7X&e4rZ_ugHjq~##brK6}L~q)FTch zMuCN^HRcg-gbFlTje>Ee-j9@D9@4atg~8s<&Hhj8J?8helQCIGVuv|7a-DsA7kjmVl@hx&okmNbepwx zowef{aMBFFs(z-LL$*81qQxUsjaT1Iqj#FggMT=w>`jm30JyWwkx&a&`~ zLC84cboSQtH!%1P>*_*$Ap!(DJZ|>HyjR#s`do^8iMOVw8*MZPru$iLL6mSb$2FS} zo|$wJdF!#}NuW9HhNSN`Ih}f47j4v`q`D<-%R@ku*<$ILyV+87q ziLl%v{s)P^tHK`)%Dkr*U)=tq;|l*xaHrQJ|E zQdRjWh7=C??v9Q$$V3>YNElNdd0umbr83BmAqpymw0{ z$xhhIy!q59H9n3wDYxq2v^^+8me$5BycaBoz*{( z`T3e8pIsJJpI=*{t1**|(2IZcbT?n#PUnOR{~IAPsFWdEA4KO=o5(6avCpm7wc%A_ zqztYkJ+%%ShXe$<(eK_Sa;wN4MwcNYijtEH77`g#{t$!+a%p~Du~4%6ebo0B$d zT!K8h|BTdz(~xmH|4Y3A9&$lmNB(9Us(4~#j+9I8a;IlKH&e-*icj{(C1eBGIS9WJ@%5wBoUd3mhy zSt6_`ViL&c^%hCR<`W0Lu~9AbboEWf4_rxnn4h!3inDNhcpCxjR}WIk`n`JI&D91a z$>PQ{Y5?}ANkx~eQ4O{hNgm}fX*FoR3IdI*5RXk~hE7!|=F79gTEoTm)eAd&R0cD_ z_nC}^#okDD>eqCa^&eMG&VOHecmu2;?&}v|Rdrfsa35$Xtn8Lnd zMpj?m2Gd}ATiYs?8!KYzn7cM3KaeVv{e~i~P3-MA2jh6mi%ZE-gN}f0}2K5g>{RjMi`fGFstr#$?_HA~9oq!22 zAar-YEaSqes-q_-M-RRPC8^VAz*dOoW-xfv&G2D$XoPCkV6TNu0ZH3w)KeFFO7h~Xf;%oz{k*;gf4o-sP`jiKW}59p-#`XQyQ1^ISx}MC4O-;Ih+Rtflql zC{=@!{c*P;Cgb~_90z7fgpHClx*#kV6h!ECgtZp4M~6|gC4KVLMdfV%MX-5#sWYQ^ zcdh~Yf3OB`x;!B8q_Ukb%Su&4n_fKg)I~hgrsvU;gewuER7C=vg%88nrpvRCn_W_z zon2g#eg3f|wLUgWk7+Y*U0!B4*_X`3ZbCosVJR#f3tkRnLpn?6vbp~x$)0G_C7v*h zM4qQG^+Q3+dmn7iXMV8%DNxql)>5N#lj@4Hf3gb8o332_m$Zu`WDjJ78tyHsfoffs zVr|jKqAN~XVDYl$)5jcNZr|LTnMV%-qSP_xe5i3+)iiEL;se{(^Xe%gSA21gYly0J>7Mc+YWG9147;7z{e z;2@ox3Tu1za!3xEyIR|;XN{NlB;%6B#>rDh(XqldbI-Up`;8tmW^})hWkRbt_NQax zM~@mcdR!IC$1oGOb<{gYo|i#SV_{)EoBoJRuQ#e((3czJa~(fjvN(Ache0UTmoP^K z4S&J-PS;rqJqV7wQ2mO8=!El!2m47GiMn05e5K*LA1>3? zvx6~J-D9^|)!e{*6pC6`qI^fx?JuiBr=p^x?+3G)nOy-#5NDP6`xL(UkHbfHhi#ka z<>CS(!4|KPuMaskan!Z5b=OCaL)@t-*BYm=l5z7iC|jx{XnI4Jt%5M$WO6Nzs()>6 zsfjLOy>ha$^768>a)f$1`S&~dfpcLr*ntK-=dvCbF8}ad!?PU3OS_P?H zrw(Wvi;ndWgUwAx_XK|vx43cvYh#ze;r@lkCUH|v)@~~rI$!5*Pvq8eUc13h5^ z`?0F*lfBzz+o0z-qYgZi*l}yvDVMrP1qy#a21_%s25|Z03$<Q#5~VqM*(%j~L|nft$T&~-@JUduJ1AutLoz!F9U&EM3C{}ly4!tR(JAM}6n zMzgp0%AqAIGV)BO;kyz){e&KreCffh8@~Kh%2wM-JHsNca}DrBE>1xi-Q*qgv-$+M zjm89j5#r3tA&y-Kdy`2FGrJWO2$SgKWe2HTDMJCLl($^(%@)=bk?+dtH{lV8%E0~h;d$L}(D2aMk3pYQE1Q45JhV8vy7^WO=AT1{s0){dVJoc14fKXla{~*0 zZ{+7K8#oA8qyg=z zM()3;Z0fF+&5d@RS%fn)O9R)WhJ5Lpn}6qA;K3z}zM7M=?vdrx58r>kTrrBO5cenEU%?u1u(8N@4h02av2>}kK=rnCY>rxOB3E*f9TNgPgd;S z1-rO2aaW?rZDqT{qpp8*2~mh5P7IUJaTr`!*`kiZRPa8^-ueYYagMONtyKA5Dc0jS z+}UaLHa7ep9!><^!OGGP$|?`Cr+dHHw-*^!(MeK;klR*DW26$~^FzKYl1$zri^(DqOIFHX5F;_OI}mp=3@<%CMc@br zG6~15B#B6Gk!TW*8EOdj`jF)~-W8uF%HQK0$n0mb66IDj{zfCu+jx({k=@A@d|HAM zf=CjMh{UnsC}oj6(gB~jdloKdR;Sy%xsO3nL zkmAWwe9z}kM6P&QraPk06VK=PJGnb~JHD%v_O)nv@|Ryrxy5OFyF5~U|LxeaI{Ewl z@xfNle&N|SRU)6H1^J6^53B@$`9XtszDMRO$^XA#5ZV;vFS~%~C*gex`YaHC@+N_Q z1F$qWc?y^EO#~B{isl6im)A`NK7Yru-959r5KVM>YShTOE=W*Oa6!cPg9rlRBY;2< zM9>oW{&8eOp z6g_@9_s9Klzxz#=VC4KiIG!d@NVq>DhBL{?9HACNzKrS(Gqwlo7b3qn#py4s`tJco5b6yJ92NBX- zT%H&a9+MRrg^>M+Ab&SLe1C0{>00Cf{o{e(CLugNYVbpsJqY>!0QXiUB_^-xPDjBo z{%qEdm~SMsY69N0a&fw58C1@af$DI#x*I3|;K z1j+|;W4^sIeuH=p>M`-Xmk%d_YJk#2s_At;B=WTbl>o(y_nB?gqRv=oHvw%D`y!38 zMczH}hI7j+X9*;iK~M!Lf8hTRuy1=A&t$;=j=n$EBldG54F-Ax=oVaI;hIP1;5-`H zKAP^nplIffkE29Vi@V8PH6i zIG`jTU!avhsX*@orHK>@v|gkzpc0XmAf!P?GKH}<#7L%#;A~+eQ_!s$Y$Vg z3CAHnv>GrIumJE9f4YkY;)Qqx-iv?2e>WmA>4n2d5XmB6l3LQty~$~~Ndo$Cae#%~ zx7-6>%`fIx@}>L{{*s8d06Y0k83#B`wnDaBc0txHw~;TFr_1*M9+O{IC=??U^A+iU zC5r0`gULH4PA0x42`2e~wSbQ$lu$wmC6rJ?2_=+JLJ1|5e^5dRCH((`raYk8{{V-* zf?9--@RzQnLDqz1$P^*$tmM?3(%Di?_b7X;VhY&VquSabha|H zv>*z#i*)S&0F{k!l;aR@eZ;M_IV?g$rKu;S}C*B zXk2AVXQhUBphY;^jUG&<2iT8Q4uww&F#~YZHZ4iyycr5z_e-vV&#%+2%9!neGI-~n0?+teJ zLDonOz2jV*ovbV^nX*5z6OL-(4!-H*GTv{l+xR&dxj8wxc{w?G z_a8mFfB)e_a?jPz&vm@7Hz}qUX&pUJFXA!S1-s%gC3G!KrytX`I17J-H{dLYK_^1I z2Rx%Ue-P5BlrpWxS*fyA;kWPvdI0;K$1eJF6?}?!mDjVT3iemeyMSK}wzAJOfl=#% zTwPqW<80N#T z*X^9UK5zPr@5ef|{W9avtlB;bX6OhWu z3J;T8T8M*jtyA)RzB@CfBH^#UKWw4;e+O8NExZb7OL{>;`sS=G+1JN3cC?lL5*bGy z|KkDe#3^_iUWaps8kQg1x9`}WzC3V{d6ok)aRtxhf@h#amBUpU?p`J)2Cb~g|5>`s z0TE-?^L9NmoaH^oa$Cs#HzC1|>Cc^|jo=Nq#8(P&hHS%}3Oi#pr{dHkrU_>Mf1e)V zy2b>!d_K^e%c067zwsWu>~6ywVEG&Cw9Xs24tV`^lzF`JdBH`jmQ*IzvlZ?x)!#nodqyzap~i%;D@k7FG=<^$mjG z5b?1P@qv)va=F?D8AEI>#-7bZe-~GNtWRk$y+Ld66g(uj)W@sBzxMR$+Q9vuT6=rE z11I4GTxf6KFlj1G!3*>+^y1V>;B_|mF<LmYfqe+5oLc37p2Yd!8>sx3xn3B!N-#}KoII^(@+)OR>tNz*fU$S z9|{iCTbOK0b#^_mudg>;e`QNUF^PQUK+YQ6*i)$ke)-G(YT;PNO`>P9o4y9S(X-%0 z*M^D;J{|b40)Miw@(B7ZRafgtqdrpBBvvth=ud_IzDys?co-g1T&ebl!bdQV`yHp9 zh4j%eu)~BrdFCLq)BnzyKfA`}WC}zmM6A{b_Y`E9lIFeITy|zre_>{3VNvGg#|A^^ zW4+-qX~UlQZF+}(L#yZ=JP>=*b#x;X;w+qrH`0wvH+!xrLWY?4%5z9>p4CDqNmzGx z%kJV_>W#mCh6(NNx_e&cXlN+N$jC3ddA;RthH_>heIQtc{lJhdYx?O@ z!Ev(4aPQ;I>Dl6p^a5|}dMY3*cGlMax;)s~+1hHy4TrUpe|s2mG*nj>zdiQScl3#V z*$;8a7h(!4QnoF>_$_|3)xTO+dcl2S#)^m-^?^cZVX|sDLIXQG|-yMf8cH+DJFct1aJ4O8=J zFkkGUPu;UtxoTM%5u&4o4Ue zE;#yle_&4SBwHKeydo(o>GCmqZ$o-nX1$4?E}oa!>J%Fb^7e5ZVtohM;z@B_wX z**VVk`RG_!M1}2?tsQfl+mV&A`IApJXJi>}j{Ge8{I6~Gk%gnHs)*y2=Ic#A{Mby^ z&JTnVcAx%6YXcVKf>Q9h2J~ydx7V%~J&y7kf3My1sh(cP8qCit^7YLB;LBsjE5mk8 zb8~RO(wD{^p4Tf#1b1s|luEv>DrUTzOo@Z_CQ4>`y|2P0msuDcxY7>AEtNI>A2}+MjIO)UTpY7Lgum+3}!w|eT7op zUX5@5Oy9$>`B;)&Ew|98;YJ!{@F3IE=@cec0&;K2hYI6CtY9FRUo7TiL-3-jZU1$c zUc;?;$A=#k|8R@B%1kaScy?x=frO-+C4O`}wnOLs%@N+MB^YV22 z{Bu>r6z?eR-t1Y8wAHYNr2n3rcH6Llq<18-GanWxoS>SG#bjqu>4O_lC=S*!49V zh7E^^HG#i!kbcd|C-1%Te>oCcp@zc3cVTYHg7s-3-*{!`wqIIsBzEby3)gLaWHReN zK5af9RK~k~J$m%K1#`UAZ@pc-zU;VKU9HtdEZR7hyfrj`!`}Z^u?*S-Hk-)y7@rfH zvM;5Pl0@TD8XB6?aHFAu&0N_YdP-*~`EW^oSv?8U;mJL9aS$KMe{>jEMP}#oGG<#- z6_@n!Cr6Kt-M^x;9GA0)%^o#L?J6w~kG#P&M1mFZe309# zTMJt5?_8x@xuc^cUl)+~WOv@cL3zbbbp9`oVJ#|lwS6&?QStMU)asU*x&A?k3Bmrk zGbcYSkIS7pH8-yO>E!Cb+$Y6(g9hg9ev%ug%Wvt}u~N5c=lzxf@T0q(#PYW}E4BmR zw6LP^Pf2WLe)&{7nGRtev-i+~l$205WIe}K-=S^^w4Xgct$LdifYQ4|V? z$~qG6dZGm2XW!|YhL!``p!v^rB@0(kaE=0*)o_di&Nt9ZXvKmCf0P0o5x^S`T9yi& zHqd7JZ9q!`9Fu@&3Fuu0d^RAT2;+teeXr_SAZTG4e1IzfXgW|Lii3VOelkcU3Ob#@ z3TNc{f0BHUbdRj@ckod6M~MAC+v~m|$RbX-> z`NPA#KCpse)DI9t^;E*q9K8+4cK|Wu@kn8ZJ_e3XXaXE317b845QA6K;W!iKI7Yr; zH%9NH5IBYcViX35A!17qHVrThfa4<5MR1HW4UL0ivS~8H{{)U&c<7h$O#~E|z4HYN zmw2)REq^sKH8?UhRFWnpa!c-qZ5X;>6jw&zxL-D)gpHQL0`+KniV$>K-G%+SGW}RdlHLfV`hzRa+qGL4bBymaHvS`$($?44HJ>RK< z0gTR}0cO!lxme&2l+2Y>_$hslS1KJ+f#*eb>~H z`d8`KAuGRl#==cn7=U+w08>T2bH)@FG@L-pOcgJ1=DXUIL`XEALcdq16izLEunR(o z|9?LS?gLCm1-SX-(2WzD_5KND`CEPAa~0;>di&K}`*!er;3=Pz?tw0p1ho&*$U7cz zs;`145ZvI@=wU$s!;KnbDN(8b17_9d`DQC(k#E{1pXzNrz80yAJ; zU7tfM<_ob<2vcAtQD#FXWI-{lx0ZgP5DXy@Nxd|J`hb%l*@RgLfi#MSJSskosHPet z7qu>Gr%|0jm?H_E2f0M;12IIKN3)3p%Y+i@I|(y{u!4=!3zA?6BtruA8t4M*>3>EW zoTn0`5DJK2W6I$~;bZh0&t?&3F{B&#)9NV#NvlYL<`ef^;;G|IrrA8=oJM*FLOA_; zKzE3s68>0Oz=#svFRgTI8m$TTQ`;Jv(~Ox`^M#H?(Gv!fbjieH8q9p~!0=}+B$%u? zl&qmkG2_7lW?@YjhaaJ`QF!D$6n`xiu!BF*^{E~cBya0wPxZh;%6_UG^X@yw$&)i=YDs5@AK4`X#8h`8NC#Y6C zwSYN9O_6K?E7+hNbR~~OKpz-LK2L&iWc@sfuxDTayiBsb53683Nx2Iu$g)S_?{Eq( z!F9L^ci|U!z*MrBl|98A%%26YC>G5Iu|KepYy!(<`D_N8!{)Q4>~;1I`wRPsZDPAv zB|FHDvCr6->@xe7-DbbA|9_UuQVXfI5z0v`ds=-x+dL_ewH4{irifGlH17sa)2Br_mrdMLGmy; zNggk|+Q>x_fwhLFTZml7dWUQAvKDv!r-f zE$2i7GqC|PyvImmYJX(9D{{aNT4F=- zu!x*ryCfnvv&iKt$a5BCWT)pD66Q5>Pi!8oQ(#ONG;&W5S3zcBV;dIME>>h<7BzBD z#8hKy>O=17Sxhn)H}XqZ?R-p0W1c0oJWJ{n(ET#qU^cx$iRu6H%XGsHGaDc?jfq(X z!K`{?&2kkL27eY$CWcfOPuKg!S!4ieCl{6!=}q=DeKa+DhF+7w3#fHvXJ!>sTaaxK z>e2?s^sH?{Yn!mx!YMP0bUdHbPU)$geL{mnB7M?k`Z$ZTrxXVIMCaxC7~rWsB(SS! znkysFIO%+hla9_VU!Wt2Eu^c8#9m}j%IH*u_KYoNbbnB>mjV0$-UhaW(K!uez+M5^ z%Gj#_+kq`((2=p_5_AUkUyKqo?2y?qhVZI09@hP)4Jpl~K<5SYrDae8PaFjb*HwQBq(B0OEo21s$9)0O$y2 zhXGD9b_D1!W=9zeWbBwkDUKayly+blz-fv7ovwAjPBKd8>=dIUM`tdjBz9JY*8mb2 z`$C3u`rQfsA+w82zwJ=+c^xQe!CwFp8T(3tQGdX$09*iemBAZyQ37^draJ&!l-c(( zBr|qPhOrDT1G@`w1sGl4#sRyp-x}FZ051WM@mqMXpPSMRfRY~N6Icn*3@B-WjnSn> z&*78wd`u~aZWeS60F9CsPzI-@1RjhMsD9V#4bWFlzLfi65$M^sgi$_+=|CA=zuDyg zU4N?}m+9#b9Zu04@!5Dl#Y+V-{2ECP8R+jN@RKpZNu6! zJEbxo){gnI_N)Wz#THRmy~JLo{SRN@2TMIorc3*j`Fz`&bpZ=_LD9<>$)5vM^dti zl6%R$<>B%OIZ+-dkCNxf^W_Ed3-UtwMR}3DSbj-fCtsDX$=Bs?F7dPaeg4@!q@-@5}q~{yduJ z@LZn9U*wDUV*V0;nJ?kzR7sUpld7m(Rn-_ZR*h5R)dA{2b&xt(eOeu&E>&MqPpkLT z`|3~X&*uK-XmgA?)*NS!H_tH7G=I-BKVzP4UT@xD-e}%rE;Vm9Z!wpdx0+9xPn$n8 zpD~{`pY!nW=dKCMct*7j=$w1e6q?XY%4JF0!G9e>mQrhTFv z*G_1E*G_7mYNxc*+GpAs?W}fAyQW>&zR|wbzSF+fZfG~PAGBNAZS9WsqjtCH6HVJU z^>b}b@B_YGP}7@>1-}ee#00*)W}x*lzNNg*zZP#PmWJo`lU}%oqk8Ww zdR-g2cl-yAW$K=f*6ls!{eM&MFNNJMR&(qsHd?XUUA1CE{-p7FImr$`QD(*d>TO=R zb4ubcSK%m!1;@C%&9UNeY>Q?LTCbv(nLCT7QtBPFr-46#9G3!7~4?E$4R0Mwim??L4R|hHAk%OzL(Uy zw}y+ZBCKPgpFuP6q?bDd+lUJ6j0@2w7NQ?+L0io*UaxBsF%88J@eKmMf#LrG<3x7= ziT+3IM5K$Kchtzj3rF%Hb7Dt#vWezV-(WLrdj2{#zx?rpVSjO8EJwd=VMUV|s*B&X zPW&$Q;$ur=p|!YPtbfI@Uxte?u~MwZl_FG^MI6(iG#knr`N1-Ps$d*p z1=I{Bj1SyH6%qR{zWMH8(l{H#NK7YLHx$D}4wcY4T|!8gTz?04GYL!hHS|R7>u<=1 zo?izEO?ZZf2v0|Y;IA0U&O(Kw4cRRU0K4xE;c$qR(dHtG?KPk-~_p%*BId)ng;H>bsiSXb+;o%V5+zida%X?x*(coSa zx9xw?F1m8jVe!2#*wz@yiKe1Lyijea=7C~SWTST zbJEzz$Vq$8+l}!6RYajeV)VDy8{`hI^uh>^EA*`w!+%AOnpw&`F2dcj6cNWQZf{KX z!USUK&R3RmG}YuQIE>@dI(jtss~JbwzV30lL)#kzKYbAWP_)pq7B|eXUfx-;@@>cR zVHoKCs|Xr4kFS|IZdasD_;wZ)93twL0Q7TgLLX~P%Ki)ID-N7KRXIAMPwMdKMjo`3 zyPJ@r9e-{iU=&7mK`wkFQYRG7bj-s|B8dO@^BABWS*}ifduPfOTNW{kPCfkjrM*?B zPFIYK>X|Ys)?rzJCy9mM-OJeGT?YlQb52@*vaNs0k$;>kKYDKe)|?4%IWUByb++$S z(a|RQ3MM>7un0m)Sg^0-5tTUU^vm-iNNk#?K7XW?qfRUPztw699wb@X-#CTxN{8^_ zIkVI96KpXldrrLIa6h$+W5-}@C1OOlkc29FiZE1#iUD@f1g}~5y|$<>*gSRBGb!caHeu`FEBc7O_d8(QOS{Wgz2{h-zRI>aUSbGd$G(`Th>@I* z*^ryN_fQJn+ z_ws`qcT`q=I!Q@s?GI&3V z8$H;9$j__YmvZ_*hM z<-n*;2%fQqYX{C})*W2-KGQGV#m>58PtsFabk8!VgYbkLdnvY1>k ze4e^;_QXBiZNjgU0DW8jAUOJ7+_QT%MF1Dt32SW1f%A@w71f`eDId|>9x-NgoZTWK z4CUwZ>lORY*^i?C7#^28A%k`$|FTZu{`fYj+vUQUA00SH9PRM6_nNiumw#8TF3Ek> zj@}%l`C*5I#U|SI>l5M-)3NeiH}Cv{8OioBdGBvt=n(C=&(c!^v0L3iD0<*wub;8i zKt8@?Wtju>&`$QVQ|j%gc2Nsw&nUEyrFj2J6sy8~auBNay7TkZIo4BqOE>+Qv>AaN z_1F>>>d4FqNuzVO?6>1V8h_tcg><-FFlbsWZQ#GdJTEMfBk4s@e2>k&^YNsl`rPLgRg~3=fe-TYsSECG70L6)N3I zJTHkBqM7iF5S{H}g-YqlGXk60v9oEctMrfq=c|XdlGm(Ct0Cw4pyf zsZjnJE85{(^HtGKv{Zzq-r`3$I46zEnw&T~XVcC_4w6TxUsA#fvRg{A#*21Ivs363 zhaG5tbPzVWIrMbL>JAVwH6s+!8V{+K*?54~>w4op9e-NbPk+MdNXeT1+%t|B_#5b} z;7V*)(}fq*^j5G7fBTJn@g51x4*JbS&C0ulQOpNt1R1AT~(HzoSfkt?Xdj5M-)}yd@GjWlcMZb7XaaV z)}3xe&R5`EML0NSiMdu(`J1&vi}?OwxJBHt;D@c;cdd2*IW(>Nh%13re?_I@NrizI z6-IBt`hPzq9jP#SSq-B)ii-Z)PA~5CVm^ZEWGebgaVpg%R2a;qqJINfO!XT5I{~bt zqJIq8LiIr^436p_80b~^1l6ahkStOp_(})#7xCZsNSP2X`$DHj=8x;2CmM}IA09uJ zS101t`C(qtg$6p->9=`2MElg^@#UEiFJ1jNuz!e0b$KlBv<7-<;jeU^1Z^HmlM1#+ z=62{`Fn(h#@`v$H*MIm(v!TQ9#$5t2QtqSpzve+Y{w7bh3vAN7U(u!N-?i%Cp3wdO zq(@>S+SrEvL;ib}}{+|EBer z`9c2BOd9@p*kkcCpbt9Vma%Ev{3V7PAy$6&ORo5qRXyE3<_T4ZA}CCcV8UT4*94pyq9_RF5d{Q91qBh*1`L=mXH*nX0Yy=e zC?E&|GQc4HpHojaukZct`tQ1HU0rqduBub}Ice`_pJT6aL`%yECX$)K1TnQuf`4G$ zI=<&eh6!85FrS49Ld!MRoaoMGn6H;I43jmanHM6OBkIZhyX5|s5u<9@Tl(A&F-(A! z+~3;X**5<4{J0$q6Z#9eU#v=PV&cflNPULZcc|n+SWCnSqMsyJq1xFudG7GwVusn6 z#W2=&oo$oHTdJ=muL9CY5!%((DSzFcIL}G$XE0324dc6e`@D}bENS0l^4`h>(5eix zZS?N8v9Xu@9TXXKfIML~4*z8VmA~DU^SbvTZ%xpTf!mot#=`jjA>RbO=dEC}f}SGp z7X|&uh)go`uYWAn40EknGJ%0(0_HFwMEjcj%LL?HkSueSyk6BE3_r-w47!P}T7rkfehOksK$jZqkt=_7chOgmFXq>ZU( zT%@eLptJwdxqs>PL~CQZ$bSgoNxkpwFk-cfAoLN0@r>kqGLE3Nk!uRMAHsB!`zwjo zO`cfE+dih(v?h~(HKv1HZRD9`+WuedRgo_xQm2EwYbVb;$y3@6jXdpTs>#y^-`oH6 zuh+bz?3pYH0$oK)+YWvpyeoi?3(0X@n$dk%s$ZeSj`dB%zku`w$itXSz4K9QqndVX776c)xR!t zB!~QKmi_be@3zt2^?#c4?yp7dO!2jiuB>qv#_Hc=K>I|$n&9hSrEd;;Nv#gkg8yl! z`Hg0-jQ_6LPHJ{v7$1ytpSjLx%Ve{MJ-)Y-%+{;s==J?;#3W|2*)BTjy=J{0vu2-f z1lq`3w0oktGX7tTC|2J8*$Z74yse(8{g=*}c0}?$#?(tm5VxYO*zP~W=kA@;k? z@#yod`2YCE$p7+q+}WA|Ao!}w)a23^M6(KUtgJl){LhNlSCteJfWhD#Hqs-b;t9a=km zaT%dy%l8Z|HwVyiOCT+`2Axh0o*or=MO4tBWl%b6oPX&KAzR$r z7G^uMn>olFV+NS>0U-fV0f_+_0eJzX0W|@RfJ+0K1FjAj889Z`)&M1-D_}yvw18Ow z4+hK)Sbr4obim4hH36>$yc6(oz?T8v2K*SXBVb>^;eZnXX9EKQErBtCNr9Px`GIAD zwSmsS%L4hpVS%FpZwPD)R0F#MCkEaTcz0lb;Jm=c1D_6D5x6?=)xfs{KMMRJa8uw9 zfxias4g4eUc;J~JCMYZ@CMY?`8dMTg9aI-|Nq2zlZ!8@^{F&(4f$W(D=}_(A?1C(5g^- z=*6Lpp+iHj4ZS|}mQX3QBeXYkYUo{|_kV{z61p&SN$9hot3ua?z7_gm=;xu|gl-M} zC3H{dq0qlVPltNLLc^lNlEN~>^25r)YQvmimxb|R!@@>|-4NCmriOKgO$@su?C!As zuz6vRhb;|zF6_my*Tdco`y^~b*mq&u!ghx34?7ZeGHlQiXbHE(SyC-ImLf}~g@3bL zWNENmX>@wcKnGErzAXGQ~2(a-Zd4%L2<{%W{jy@``1hU#QcaSBc6$PA>!qTHzVGU_$=b9?5OFl( zRK!1#!I6=X36bfM*2t2`>d3mtOCp;huZp}ba&+X_NI9}IvM=)X$eEE3M1RhSd@S;* z$Q6;RBVUbtJMyE*FCsTZ{t)?V)a_BTq8^EQB5GOG^HFa`eGs)iYGc%vsO?d^qYg$LiyDYJ9~}}M6`dHJ z5uF!Z8eJ3Zh`uzsIr{48k$=%+qHm2>qPwCeL{E>tKl+jAh0#l*pN(D>y*B!-=ntbm zkNzflYxFPCd!i3T{}mG&6CINjlNpmAQx;PjlGiHCxk(iS)gRz0J;eWAlv8k~+u|=_! zv0Us$u??|T#*T<>jlDTmj5T6=VyDE;h`lfN;n)SSi({9^dSYLRT^IX7?E2V^v0Gxd z$L@|j7<(*sAohG*NL*B0Vq8XCUR-HhO`Id{(zxchtK&w-jfuN8PKoP^n-DiGZdTlb zadYDq#XTLjGVbNLH-F>akNYg{>$uHvKgaEgI}mp??o`}A@xk$t@d@$i@z(f~`0Dt& z_)FrO;;)Jy7C$QfhWNI4HNHE3V*DNPcgOd~&x?ONerf!3@h`@|9{+CqC-EENzl+}% zzcYS+{E_&R@q-D03E>HG38@Lzgt7!S;o^kmgliI76UHW}34gaGOih@T@L|D8(l1GSk`5>Rope4qEIBSYJvl$QBH5mN zX|j+!BDpnrY_ghsTk_Q8yOQruek6He@{;6dlUF6LO@1r+!{pDCze)Zv`M2bQ$;Xq= zq%bLADKRO@DOo8+Db*=;DVL-)rCgPAUCQW`u_)O8Ft>*Oa{}f215wIg`qyhNZ@&CZ}el7NnM^+EQJqm!}G;*QBXy{)sk>7TrXEWjNIjnx zk`|Sgn17a$mX}tVR+HvPyELsi?dr6VX=BoEO;ggk(k7%$OPiJUVA|ZYMQKl`txQ{! z_FCFIX&(gxF+^pNz3^w{*I^z`(c^n&!#^s00= z-I;z#dPBO9es%h_>F)F!(#NKY=~{YMdT;vV^nW|jXQtnm{!sed^hN1Ur>{(3lm1%z zJLw;%f0_Pm`j6>5()XnwPCt=;HX|Uzk`bGcl98QJm{F0zX4Gd~kufA=c!oRUri}Is zJ>#~F$r;l#?#-B;F+bzUjAt@l$ap#9&5ZXmKFjzzV{^vO8M`tLWE{;nmGMtzaAss? zLVsp@rZux9vpTab^ODS_%&Ri5%N(6KHdD^*%EFtTftd^`Bv&Lm< zS>v-NW!;%|Pu4?Ok7hlQwJhuTte3Lh$bWh->(i{SvcAvyDeJea-?RSA`aA1fc2IUi zc6@ePc5ZfYc2%}L`{L}z?4jA$W?!FuOSY8Vk=>g;HT$mY`?DX(UYNZk``PSO*=w`k z%Kk9>^XzZ3w`Tv6y(jxn_Fvhjv%NW?Ing;uIhi^6Ib}JuInJERa`>EKIiqrJ$bV_e zQFFR;Cg$9cb9YXE&b*w*bC%{jm-AxI>pAb{e3G*v=ewM3IXiRq=N!p7nKPIhm>ZrO zmz$cKlUtNqnakx~l-rPdW$uXF*4&$O#att|CwEHjjNJQjAI@EnyEu1wt|#}E+;zDh zxS6fF~$5?N*D%LLR z1nV^GEbD{Txz!fkYpkzX-?4sd{nGla^+)Rt>ptsY>j~@GynsAQUTj`UUUpt# zUPT_8SD$x9-jKZEdG5TM^4jzCyxa07=S|PMH*a>{{Jbagp2>S5@8!HV^MBsY`z-J4 zyv=z(=k3ZnkasliRNg=N!TFK-3Hj;y*8Gx$y#-KQ!L}}p1_;3e!QDN$6WrZBKyY_w zaCaC6cXxMpcO5jiy95aEIOp7R>%9NI_rI!DUw^%N@2Re-?!CHq@3q$7lfP|$Wp2`M zs>iw+0nTqL-rU`+-AwNfe^Z}O_NGSx^zWsAF`vqB4JHA+@A-e3ZaDYf1%=U&r0&H$$+pp@gB`?}fhh&zDTz1@?x&6Z*^$H886fyvwip`=$}QY1Xy>$o$Ug z6A1=jW-;CYu!6C~zENPBVC{bcVlHBFM>5jwM228S)3)W7R7x_OFs5lbOU!PFy^7sN z5He|N-|5aqidBE>)VY(I4avKVXu>?F2OBsS9Q~U8>G0FR-9gvE(m~!q$-(5ihPRS~ z$Te_~EJ7#Jju9&|dM9g#W{@@FlFqlqUFdpeXMZpYu%i*Nj0sTe=yEr{PTn~mY>!02 zBBCRt7BE0pMpsMFOi)VDP}Wvf*`aQ2C}`a~NrKbRlE5)x}G3L zp#Z9&*HXFB9_6JV0(ooQ*o?+f@Jc@b@fo?59Glp37L;*y(v;Kmv^BMr#*&vn`bs{CgV%VS!A7@iD1&Jc2o0NY~3!v#B40}rsgK)X78rrCgA4AQKz;1?~P;b z-?W?L8@%yhpw<5FI2CY9%U9t^WIr}pP^qip@6!!vY>OID{dfK!qmt=k05;!jjla{s zxBt}@Qxp>w^A^(nC>IiF>n?r1xy7#0Zo97z^Sou+NUu? zU_Px~iFLUuT{|6>`lgbHn+agQV{DiXq%NTGWVC+@yj1s9Zq2=EAF~1S(miSrmhx$F zYhr3*>SJmEwSg*Xs%lz;YI>@R07Vy*#jvuavh?x++WfL&OXcZMU6R<589Kv{xW3R)gm zELhrG_`0aNWY7L>k!?{|%}H`2{$zao7^h+P{feCyHS;*@Ui6 z4a~JB)+D~B#?gFknr?ksD_1*Nvtj17($v$m(zJe1+i9d@_R`a|*)-cU*tF5K<~rj# z!SR!KI;2L)nl!!lptjH&>GUU)N&0H}YWx}YgSxt5`wGGtn=95w8Zusx-hjYl)vx86 z>e|a1qI!e6tvbOv-W8uS!v`3zWW;THU3yTTiBqsH?6BkgI_5g6E!W|J?GBhbvpqU} zLhkTl-_+^0m8#XO z8(Yi`jYiX-q@s9$fXPTn)kT#9zjAu63r*+l8n_TaPus>u#7P9jLvdr4KYVk|(u%|y zl^$k!rqbC>N*qsAMdu|*pmc*?sTWoWWfo!j1|KzfTvOz-;u)+Ymi3bJtalCw;=t0mk{4GRggmv?VHy*K@)@vc0e!E=61Y(1LL zQS3j-@n-5iX_i|rlEeN@iER~7 z`)?5DO!wyat|@n9g8^O*a^(QMCBhPZO7ma-(qRgkiTns3$@lTL0HFuNj|xHoA`5~K z(jCeV1_A*B9fBM}5+V+Q81hE|!T{+-eb@{Aj)2a%&3F+Qq1{5Mg4QD{*zQ4CR5 z7!*i8QD3b1Y_UBvv?r~9Q=6W8$mvi2rxvs&tsoaH!%Q(vGmu#nECyhI#$D76Yc^Ai z*bHq%3&s)4KjS}WV}A<4&>6Gouq1I(J^VhsFn6Tl6zTs)U5TJ@WB;ji=_!XMgyM}1 znG}g(Dtt#`>NqCToT^uLb9a4&*$rF{+)fKe%qYKAtT_013U15kB;*ymm|5);?e{}aN_ z=t+4aN2ZqY7w?(XQHe{Rg#%mL;32597Q2xMmF^ z9SJ+&Q;O{mUbMejFaWIHY_TgdJpUQ^AaPhnEu{cegS)JwZ zU$U}$X)ZN|C+s`phYQ*6LB+=ofEKa;36K1x*B{gq1B(J`i}?r*Q^UFapQ0(zPHss+ zU{mnA67q)^it=|4IetnIbfW!FbY$5+eVE&=z#j>LUbSCgw|C76^yE@RfjjyAgCD;n6#CvihY)nJ1dRo{ zT!KadJbcB+4ughv_(_cXWx(H`iy0N}(@xKi|4Rst(k798QeCD_CR^F;maf8}^=j-wK-@#CY|>%hymMs7;egnt3CZyd*~xc2V~gdmdE$xTpR{hnZuO$s`cbFpg<8_3u#1nM?m>^1>8 z#KV3C-20CIPYF|gXhS6E`Z);q^Oni!rrO||sY`pRdgWC*)4<^mX0d_86VUU=^HSa9 z0o+yd@u%GpZos(5W6`59xkgg&+qj5jZKA96^|%a>>IH3Qh>9t2nW0D_F4a8xV3P_8w8 z1TUz(BR$A0HGyBw!X8{jy?FCFJA2K7Lw~FbgA5a3u|Pi)U@_4$LkW0Mc!j$NNVZ~X zY$Y_CK~V{?IGDz8hIS8=>Ejn(QBA)~Mq~0Rd1KN*Z3(b^fMMHyWp(plbs-Y@{JiqQ-IJO874N0%a$r=|EgeZffvFI7Nv{x*YBoN^kG8*h>^Rlpz z8q5pCFc{J-dglYcyQTbD|8Ee;0%Mf$`~UBdm|-IsZ!H-g_LGDQo>!D02)Ym9cR)#~ zx!={Qb%1%uHVKRh8&ox#do(C)7KQ}0k_bx#>$eQkK?(Ys2uu8bH&MslO{VkVVfI1` zA}Q)=HtuF^U(xVkx>((YMY%|>T9LBT+l@)nNW6z8DB0Hx2a)Lx1mfVa>;KDygnoA- zk`%>&kzF$cBhUY%si6(IvI<(C;OfLr1FnN-dl9l4+5I0zME}1m2OIzu|A#$9FC+}y ze-l~WpJ4wgF28MvfPY&CYo2Rv??Rlexi1D5A#QB_ylKA)Ca+>l-6dE%@^LnM)4wOd zI{-*&An%4X;18T?tYKBCSi5O1UsV#7)r`i0(S3yrm7dbq(Q;S~bUP1q%z`LrM za<5<|JdH8~$pT@CAZtO^Kv*gl$(N027rF?_6ZkVMZun!N{%E+Hi7U(}#S3{;g2 zi~Dh#z6$JrEl{S{5m#WCK@bP%J-SdSG7wG(KN%q>DEwMKl!~YIbN{_OOsT~B|6XAa zgs4LAQG;rchR{IRrUmtaKK>-Q6j)@KQ;{D;pjQwq9_RuDONuOj-Xj6^M-qYvVVexn z8-z9q^A+g}xt}MDeT?EEYlv$A$TtP{8^|x^}yI+O; zEFg;}St*;A3civ4V+jps7+Ml&|AUJ}7xf+$2{js34s{Y$fVlVTzgEXtNbnrQD}Fx% zzvFco&yr)w8C^q0rqP4@@)$l~#|g{Ea2D$W3=>7jxYz=qq2OSU;qlW3AOT^}sBrW6 zvQEsCy=-vvuFmtwFubT|KWh3L2#U^`Z9*-0pMOdaok7$=k02BC`kg_PK`Z`iD&_Dv z7yf&0{g3IE1?`T!&Fyyxkp-=eU$f(f30J}Oa}bhFUelp30^{5{FH^o@KHQZDFe^_5 zKK@vR!OIhK`EUP&FcP=;Z42}2nHT@xhNT8djwi$-bP3^)dkD)Pz)z|c|AZ?wAsyVA z(Mw=*HXjV0Qu0sBw2`60+S@&?MYrJx%!|Skm-aa~d|KGq1^|oKsG?Vwkog)Ncf>+Wt%ZcL;;iL1c8l-KBfw@1Hca*46TK#B?K;!ZRFlreN*Qu# z{#Jsb_ zTWK>G{KHnRL*FWPf$m;5Y2YIn2$ou1L|WcGnqaCCunpWUW>ipLskhihu3Rf;jkipq z<<-95C1t~(phg*XrBEqT`w(BP1D~?K#CWgG+widi1$l2S+hhXf_ zyM{g z8nFi-PPG1M0U@BWI+m>`iU?zk=WQB^BJfeLXhy8KvTKQ++bVEHPMgz&X zI95{Fct(zJhIt2!G>_g3v~H4YdH>k++VjlPZIH`OI3-iQWF-Z~MZSgCSC8DombS-_ zd<5sC1{kMs?>&d;ZY5e_l)gj9r-ysTj1Y}BX6mkuaNa32yvkM;kKR+ay2e|v# zg|L@36)IjOwU;J&p@GhwwQ@xE>{M{^m6X5WL6y|Nqz^j~0p2Tgy*cqkVRS-3UUrcg z^nH&P@ikv6^G=~-mI6Z-ONRQ1IKY`aBHZ-zXhLfsn+wmaVozGnhQAlo<0qeQ4RlBD zGl1|K6#8bdi%0?-Ub45Wy*1^E~8LNSs~Ba zro7w1+ChiG>%qZ6PONF#bnUJdOVjfp9vQv(UHM_*Vb9^i7$$r;d^mzod;ph7M($LN z3@PJjmyxYXxFT1<{z>X`&2m)B0ocm68*q?oWrvG1Tet1`k}}Cj8Fk+7GTrhr%z)Qy zv99%9m;M{wfOM^N1(wx-_AF%`DHV8-XO@IV45Z1VPp(g|PpVI=PpQwSPr?xvZIrk| zf0IXyj8^?MG(sa>BeH+MYT)+{{I&Kqk3#{Mv^Cm>QMr33mo`4xCh;cqCiy1)CV(^s z(++cOz`>}UI>DYgGNL3Rtt2vJg~T;N�M8zeDZ}&1+D=A%IIMgIpe~I80@5ZO~$e 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 VEgG)VO%I|lsoS};$B@3d{{y8PI!*up delta 255652 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%0Dmu(TMqrK4N+HQ*K(LrjJqp=6bmLE6%w-{ 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 z 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 From f7b236dca87865752f20585e532c0bffa3fe39d1 Mon Sep 17 00:00:00 2001 From: Francisco Date: Wed, 10 May 2023 20:58:56 +0100 Subject: [PATCH 130/197] Configure Codespell to check hidden files (#4236) --- .changeset/tender-needles-dance.md | 2 +- .github/workflows/checks.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/tender-needles-dance.md b/.changeset/tender-needles-dance.md index 04d1784a4..75ce9fbf8 100644 --- a/.changeset/tender-needles-dance.md +++ b/.changeset/tender-needles-dance.md @@ -2,6 +2,6 @@ 'openzeppelin-solidity': minor --- -`ERC20Wrapper`: self wrapping and deposit by the wrapper itself are now explicitelly forbiden. +`ERC20Wrapper`: self wrapping and deposit by the wrapper itself are now explicitly forbidden. commit: 3214f6c25 diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 1f03aa38c..ec17fa2f7 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -109,5 +109,6 @@ jobs: - name: Run CodeSpell uses: codespell-project/actions-codespell@v2.0 with: + check_hidden: true check_filenames: true skip: package-lock.json,*.pdf From 3e1b25a5cf3be6671014ebb61a716669890bab4a Mon Sep 17 00:00:00 2001 From: Francisco Date: Wed, 10 May 2023 21:08:05 +0100 Subject: [PATCH 131/197] Clean up pending admin schedule on renounce in DefaultAdminRules (#4230) --- .changeset/loud-wolves-promise.md | 5 +++++ contracts/access/AccessControlDefaultAdminRules.sol | 1 + test/access/AccessControl.behavior.js | 7 +++++-- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/loud-wolves-promise.md diff --git a/.changeset/loud-wolves-promise.md b/.changeset/loud-wolves-promise.md new file mode 100644 index 000000000..544b52c5f --- /dev/null +++ b/.changeset/loud-wolves-promise.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`AccessControlDefaultAdminRules`: Clean up pending admin schedule on renounce. diff --git a/contracts/access/AccessControlDefaultAdminRules.sol b/contracts/access/AccessControlDefaultAdminRules.sol index 6cdda81a1..33ad56e40 100644 --- a/contracts/access/AccessControlDefaultAdminRules.sol +++ b/contracts/access/AccessControlDefaultAdminRules.sol @@ -112,6 +112,7 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu newDefaultAdmin == address(0) && _isScheduleSet(schedule) && _hasSchedulePassed(schedule), "AccessControl: only can renounce in two delayed steps" ); + delete _pendingDefaultAdminSchedule; } super.renounceRole(role, account); } diff --git a/test/access/AccessControl.behavior.js b/test/access/AccessControl.behavior.js index 49ab44b58..e7a91957e 100644 --- a/test/access/AccessControl.behavior.js +++ b/test/access/AccessControl.behavior.js @@ -267,7 +267,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa [0, 'exactly when'], [1, 'after'], ]) { - it(`returns pending admin and delay ${tag} delay schedule passes if not accepted`, async function () { + it(`returns pending admin and schedule ${tag} it passes if not accepted`, async function () { // Wait until schedule + fromSchedule const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdmin(); await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule); @@ -279,7 +279,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa }); } - it('returns 0 after delay schedule passes and the transfer was accepted', async function () { + it('returns 0 after schedule passes and the transfer was accepted', async function () { // Wait after schedule const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdmin(); await time.setNextBlockTimestamp(firstSchedule.addn(1)); @@ -660,6 +660,9 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa account: defaultAdmin, }); expect(await this.accessControl.owner()).to.equal(constants.ZERO_ADDRESS); + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.eq(ZERO_ADDRESS); + expect(schedule).to.be.bignumber.eq(ZERO); }); it('allows to recover access using the internal _grantRole', async function () { From f355bd3a2a4969a78cb20ab721a1cd2b3c6da4c9 Mon Sep 17 00:00:00 2001 From: Francisco Date: Thu, 11 May 2023 17:41:02 +0100 Subject: [PATCH 132/197] Prevent attempt to publish to npm (#4239) --- package-lock.json | 13 +++++++------ package.json | 1 + scripts/release/workflow/state.js | 14 +++++++++++--- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6611223fe..139ce68e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "solidity-ast": "^0.4.25", "solidity-coverage": "^0.8.0", "solidity-docgen": "^0.6.0-beta.29", + "undici": "^5.22.1", "web3": "^1.3.0", "yargs": "^17.0.0" } @@ -15175,9 +15176,9 @@ "dev": true }, "node_modules/undici": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.0.tgz", - "integrity": "sha512-fR9RXCc+6Dxav4P9VV/sp5w3eFiSdOjJYsbtWfd4s5L5C4ogyuVpdKIVHeW0vV1MloM65/f7W45nR9ZxwVdyiA==", + "version": "5.22.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz", + "integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==", "dev": true, "dependencies": { "busboy": "^1.6.0" @@ -27985,9 +27986,9 @@ "dev": true }, "undici": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.0.tgz", - "integrity": "sha512-fR9RXCc+6Dxav4P9VV/sp5w3eFiSdOjJYsbtWfd4s5L5C4ogyuVpdKIVHeW0vV1MloM65/f7W45nR9ZxwVdyiA==", + "version": "5.22.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz", + "integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==", "dev": true, "requires": { "busboy": "^1.6.0" diff --git a/package.json b/package.json index 8ef2738b5..b198a0823 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "solidity-ast": "^0.4.25", "solidity-coverage": "^0.8.0", "solidity-docgen": "^0.6.0-beta.29", + "undici": "^5.22.1", "web3": "^1.3.0", "yargs": "^17.0.0" } diff --git a/scripts/release/workflow/state.js b/scripts/release/workflow/state.js index 4d905e260..5cfaafb86 100644 --- a/scripts/release/workflow/state.js +++ b/scripts/release/workflow/state.js @@ -1,7 +1,8 @@ const { readPreState } = require('@changesets/pre'); const { default: readChangesets } = require('@changesets/read'); const { join } = require('path'); -const { version } = require(join(__dirname, '../../../package.json')); +const { fetch } = require('undici'); +const { version, name: packageName } = require(join(__dirname, '../../../contracts/package.json')); module.exports = async ({ github, context, core }) => { const state = await getState({ github, context, core }); @@ -34,8 +35,8 @@ function shouldRunChangesets({ isReleaseBranch, isPush, isWorkflowDispatch, botR return (isReleaseBranch && isPush) || (isReleaseBranch && isWorkflowDispatch && botRun); } -function shouldRunPublish({ isReleaseBranch, isPush, hasPendingChangesets }) { - return isReleaseBranch && isPush && !hasPendingChangesets; +function shouldRunPublish({ isReleaseBranch, isPush, hasPendingChangesets, isPublishedOnNpm }) { + return isReleaseBranch && isPush && !hasPendingChangesets && !isPublishedOnNpm; } function shouldRunMerge({ @@ -80,6 +81,8 @@ async function getState({ github, context, core }) { state.prBackExists = prs.length === 0; + state.isPublishedOnNpm = await isPublishedOnNpm(packageName, version); + // Log every state value in debug mode if (core.isDebug()) for (const [key, value] of Object.entries(state)) core.debug(`${key}: ${value}`); @@ -102,3 +105,8 @@ async function readChangesetState(cwd = process.cwd()) { changesets, }; } + +async function isPublishedOnNpm(package, version) { + const res = await fetch(`https://registry.npmjs.com/${package}/${version}`); + return res.ok; +} From 3ec4307c8a79ddfee9bbaf1f26435c908fae0cae Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 11 May 2023 19:17:06 +0200 Subject: [PATCH 133/197] Fix bug allowing anyone to cancel an admin renounce (#4238) Co-authored-by: Francisco Giordano --- .../access/AccessControlDefaultAdminRules.sol | 4 ++-- test/access/AccessControl.behavior.js | 24 +++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/contracts/access/AccessControlDefaultAdminRules.sol b/contracts/access/AccessControlDefaultAdminRules.sol index 33ad56e40..07a5b4f70 100644 --- a/contracts/access/AccessControlDefaultAdminRules.sol +++ b/contracts/access/AccessControlDefaultAdminRules.sol @@ -106,7 +106,7 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu * non-administrated role. */ function renounceRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { - if (role == DEFAULT_ADMIN_ROLE) { + if (role == DEFAULT_ADMIN_ROLE && account == defaultAdmin()) { (address newDefaultAdmin, uint48 schedule) = pendingDefaultAdmin(); require( newDefaultAdmin == address(0) && _isScheduleSet(schedule) && _hasSchedulePassed(schedule), @@ -138,7 +138,7 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu * @dev See {AccessControl-_revokeRole}. */ function _revokeRole(bytes32 role, address account) internal virtual override { - if (role == DEFAULT_ADMIN_ROLE && account == _currentDefaultAdmin) { + if (role == DEFAULT_ADMIN_ROLE && account == defaultAdmin()) { delete _currentDefaultAdmin; } super._revokeRole(role, account); diff --git a/test/access/AccessControl.behavior.js b/test/access/AccessControl.behavior.js index e7a91957e..3e61616a7 100644 --- a/test/access/AccessControl.behavior.js +++ b/test/access/AccessControl.behavior.js @@ -621,14 +621,15 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa }); describe('renounces admin', function () { + let expectedSchedule; let delayPassed; + let delayNotPassed; beforeEach(async function () { await this.accessControl.beginDefaultAdminTransfer(constants.ZERO_ADDRESS, { from: defaultAdmin }); - delayPassed = web3.utils - .toBN(await time.latest()) - .add(delay) - .addn(1); + expectedSchedule = web3.utils.toBN(await time.latest()).add(delay); + delayNotPassed = expectedSchedule; + delayPassed = expectedSchedule.addn(1); }); it('reverts if caller is not default admin', async function () { @@ -639,6 +640,15 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa ); }); + it("renouncing the admin role when not an admin doesn't affect the schedule", async function () { + await time.setNextBlockTimestamp(delayPassed); + await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, other, { from: other }); + + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.equal(constants.ZERO_ADDRESS); + expect(schedule).to.be.bignumber.equal(expectedSchedule); + }); + it('keeps defaultAdmin consistent with hasRole if another non-defaultAdmin user renounces the DEFAULT_ADMIN_ROLE', async function () { await time.setNextBlockTimestamp(delayPassed); @@ -677,12 +687,6 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa }); describe('schedule not passed', function () { - let delayNotPassed; - - beforeEach(function () { - delayNotPassed = delayPassed.subn(1); - }); - for (const [fromSchedule, tag] of [ [-1, 'less'], [0, 'equal'], From 1642b6639b93e3b97be163d49827e1f56b81ca11 Mon Sep 17 00:00:00 2001 From: Francisco Date: Fri, 12 May 2023 18:22:26 +0100 Subject: [PATCH 134/197] Reduce frequency of version comment updates (#4244) --- scripts/release/update-comment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release/update-comment.js b/scripts/release/update-comment.js index eb9f937ca..9d6df2694 100755 --- a/scripts/release/update-comment.js +++ b/scripts/release/update-comment.js @@ -16,7 +16,7 @@ const { version } = require('../../package.json'); const [tag] = run('git', 'tag') .split(/\r?\n/) .filter(semver.coerce) // check version can be processed - .filter(v => semver.lt(semver.coerce(v), version)) // only consider older tags, ignore current prereleases + .filter(v => semver.satisfies(v, `< ${version}`)) // ignores prereleases unless currently a prerelease .sort(semver.rcompare); // Ordering tag → HEAD is important here. From 96b95592c300a1b38953b276f0b6c77017f5c942 Mon Sep 17 00:00:00 2001 From: Bhaskar Kashyap <31563474+bskrksyp9@users.noreply.github.com> Date: Wed, 17 May 2023 07:53:52 +0530 Subject: [PATCH 135/197] Fix grammar in docs (#4250) --- docs/modules/ROOT/pages/access-control.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/access-control.adoc b/docs/modules/ROOT/pages/access-control.adoc index 56d91e1c1..e52c10401 100644 --- a/docs/modules/ROOT/pages/access-control.adoc +++ b/docs/modules/ROOT/pages/access-control.adoc @@ -171,7 +171,7 @@ Dynamic role allocation is often a desirable property, for example in systems wh [[querying-privileged-accounts]] === Querying Privileged Accounts -Because accounts might <> dynamically, it is not always possible to determine which accounts hold a particular role. This is important as it allows to prove certain properties about a system, such as that an administrative account is a multisig or a DAO, or that a certain role has been removed from all users, effectively disabling any associated functionality. +Because accounts might <> dynamically, it is not always possible to determine which accounts hold a particular role. This is important as it allows proving certain properties about a system, such as that an administrative account is a multisig or a DAO, or that a certain role has been removed from all users, effectively disabling any associated functionality. Under the hood, `AccessControl` uses `EnumerableSet`, a more powerful variant of Solidity's `mapping` type, which allows for key enumeration. `getRoleMemberCount` can be used to retrieve the number of accounts that have a particular role, and `getRoleMember` can then be called to get the address of each of these accounts. From 0f10efe2326dfb79442967b82bde4ff1b28c69cd Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 19 May 2023 22:48:05 +0200 Subject: [PATCH 136/197] Remove code in preparation for v5.0 (#4258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García Co-authored-by: Francisco --- .changeset/beige-ducks-flow.md | 5 + .changeset/fluffy-gifts-build.md | 5 + .changeset/friendly-suits-camp.md | 5 + .changeset/selfish-queens-rest.md | 5 + .changeset/spicy-ducks-cough.md | 5 + .changeset/swift-berries-sort.md | 5 + .changeset/tame-geckos-search.md | 5 + .changeset/three-weeks-double.md | 5 + contracts/access/AccessControl.sol | 24 - contracts/access/AccessControlCrossChain.sol | 45 -- contracts/access/README.adoc | 2 - contracts/crosschain/CrossChainEnabled.sol | 54 -- contracts/crosschain/README.adoc | 34 - .../crosschain/amb/CrossChainEnabledAMB.sol | 49 -- contracts/crosschain/amb/LibAMB.sol | 35 - .../arbitrum/CrossChainEnabledArbitrumL1.sol | 44 -- .../arbitrum/CrossChainEnabledArbitrumL2.sol | 40 -- .../crosschain/arbitrum/LibArbitrumL1.sol | 42 -- .../crosschain/arbitrum/LibArbitrumL2.sol | 45 -- contracts/crosschain/errors.sol | 7 - .../optimism/CrossChainEnabledOptimism.sol | 41 -- contracts/crosschain/optimism/LibOptimism.sol | 36 -- .../polygon/CrossChainEnabledPolygonChild.sol | 72 --- contracts/governance/README.adoc | 4 - contracts/governance/TimelockController.sol | 6 +- .../GovernorCompatibilityBravo.sol | 11 +- .../extensions/GovernorProposalThreshold.sol | 23 - .../GovernorVotesQuorumFraction.sol | 16 +- contracts/interfaces/IERC1820Implementer.sol | 18 +- contracts/interfaces/IERC1820Registry.sol | 110 +++- contracts/interfaces/IERC777.sol | 197 +++++- contracts/interfaces/IERC777Recipient.sol | 32 +- contracts/interfaces/IERC777Sender.sol | 32 +- contracts/interfaces/draft-IERC2612.sol | 7 - .../mocks/AccessControlCrossChainMock.sol | 8 - contracts/mocks/ConditionalEscrowMock.sol | 18 - contracts/mocks/PullPaymentMock.sol | 15 - contracts/mocks/SafeMathMemoryCheck.sol | 72 --- contracts/mocks/TimersBlockNumberImpl.sol | 39 -- contracts/mocks/TimersTimestampImpl.sol | 39 -- contracts/mocks/crosschain/bridges.sol | 94 --- contracts/mocks/crosschain/receivers.sol | 54 -- contracts/mocks/governance/GovernorMock.sol | 17 +- .../mocks/token/ERC20PermitNoRevertMock.sol | 2 +- .../mocks/token/ERC721ConsecutiveMock.sol | 2 +- contracts/mocks/token/ERC777Mock.sol | 13 - .../mocks/token/ERC777SenderRecipientMock.sol | 152 ----- contracts/mocks/wizard/MyGovernor1.sol | 79 --- contracts/mocks/wizard/MyGovernor2.sol | 85 --- contracts/mocks/wizard/MyGovernor3.sol | 94 --- contracts/proxy/beacon/BeaconProxy.sol | 21 - .../TransparentUpgradeableProxy.sol | 27 +- contracts/security/PullPayment.sol | 74 --- contracts/security/README.adoc | 3 - .../token/ERC20/extensions/ERC20Permit.sol | 8 - .../ERC20/extensions/draft-ERC20Permit.sol | 8 - .../ERC20/extensions/draft-IERC20Permit.sol | 7 - contracts/token/ERC20/utils/SafeERC20.sol | 18 - .../ERC721/extensions/draft-ERC721Votes.sol | 9 - contracts/token/ERC777/ERC777.sol | 517 --------------- contracts/token/ERC777/IERC777.sol | 200 ------ contracts/token/ERC777/IERC777Recipient.sol | 35 - contracts/token/ERC777/IERC777Sender.sol | 35 - contracts/token/ERC777/README.adoc | 26 - contracts/utils/Checkpoints.sol | 196 ------ contracts/utils/README.adoc | 25 - contracts/utils/Timers.sol | 75 --- contracts/utils/cryptography/ECDSA.sol | 3 +- contracts/utils/cryptography/draft-EIP712.sol | 8 - contracts/utils/escrow/ConditionalEscrow.sol | 25 - contracts/utils/escrow/Escrow.sol | 67 -- contracts/utils/escrow/RefundEscrow.sol | 100 --- .../introspection/ERC1820Implementer.sol | 43 -- .../introspection/IERC1820Implementer.sol | 20 - .../utils/introspection/IERC1820Registry.sol | 112 ---- contracts/utils/math/Math.sol | 66 ++ contracts/utils/math/SafeCast.sol | 3 - contracts/utils/math/SafeMath.sol | 215 ------- contracts/utils/math/SignedSafeMath.sol | 68 -- contracts/utils/structs/EnumerableMap.sol | 68 -- contracts/vendor/amb/IAMB.sol | 41 -- contracts/vendor/arbitrum/IArbSys.sol | 134 ---- contracts/vendor/arbitrum/IBridge.sol | 102 --- .../arbitrum/IDelayedMessageProvider.sol | 16 - contracts/vendor/arbitrum/IInbox.sol | 152 ----- contracts/vendor/arbitrum/IOutbox.sol | 117 ---- .../vendor/optimism/ICrossDomainMessenger.sol | 34 - contracts/vendor/optimism/LICENSE | 22 - .../vendor/polygon/IFxMessageProcessor.sol | 7 - docs/modules/ROOT/nav.adoc | 3 - docs/modules/ROOT/pages/crosschain.adoc | 210 ------ docs/modules/ROOT/pages/erc1155.adoc | 8 +- docs/modules/ROOT/pages/erc777.adoc | 75 --- docs/modules/ROOT/pages/tokens.adoc | 1 - docs/modules/ROOT/pages/utilities.adoc | 10 +- hardhat.config.js | 6 +- scripts/generate/templates/Checkpoints.js | 86 +-- .../generate/templates/Checkpoints.opts.js | 7 +- scripts/generate/templates/Checkpoints.t.js | 118 +--- scripts/generate/templates/EnumerableMap.js | 30 - scripts/generate/templates/SafeCast.js | 3 - scripts/upgradeable/upgradeable.patch | 13 - test/access/AccessControlCrossChain.test.js | 49 -- test/crosschain/CrossChainEnabled.test.js | 78 --- test/helpers/crosschain.js | 61 -- test/security/PullPayment.test.js | 51 -- test/token/ERC20/utils/SafeERC20.test.js | 37 -- test/token/ERC777/ERC777.behavior.js | 597 ------------------ test/token/ERC777/ERC777.test.js | 556 ---------------- test/utils/Checkpoints.t.sol | 123 ---- test/utils/Checkpoints.test.js | 192 ++---- test/utils/Create2.test.js | 13 +- test/utils/TimersBlockNumberImpl.test.js | 55 -- test/utils/TimersTimestamp.test.js | 55 -- test/utils/escrow/ConditionalEscrow.test.js | 37 -- test/utils/escrow/Escrow.behavior.js | 90 --- test/utils/escrow/Escrow.test.js | 14 - test/utils/escrow/RefundEscrow.test.js | 143 ----- .../introspection/ERC1820Implementer.test.js | 71 --- test/utils/math/Math.t.sol | 3 +- test/utils/math/Math.test.js | 139 ++++ test/utils/math/SafeMath.test.js | 433 ------------- test/utils/math/SignedSafeMath.test.js | 152 ----- test/utils/structs/EnumerableMap.behavior.js | 11 - test/utils/structs/EnumerableMap.test.js | 5 - 125 files changed, 701 insertions(+), 7390 deletions(-) create mode 100644 .changeset/beige-ducks-flow.md create mode 100644 .changeset/fluffy-gifts-build.md create mode 100644 .changeset/friendly-suits-camp.md create mode 100644 .changeset/selfish-queens-rest.md create mode 100644 .changeset/spicy-ducks-cough.md create mode 100644 .changeset/swift-berries-sort.md create mode 100644 .changeset/tame-geckos-search.md create mode 100644 .changeset/three-weeks-double.md delete mode 100644 contracts/access/AccessControlCrossChain.sol delete mode 100644 contracts/crosschain/CrossChainEnabled.sol delete mode 100644 contracts/crosschain/README.adoc delete mode 100644 contracts/crosschain/amb/CrossChainEnabledAMB.sol delete mode 100644 contracts/crosschain/amb/LibAMB.sol delete mode 100644 contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol delete mode 100644 contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol delete mode 100644 contracts/crosschain/arbitrum/LibArbitrumL1.sol delete mode 100644 contracts/crosschain/arbitrum/LibArbitrumL2.sol delete mode 100644 contracts/crosschain/errors.sol delete mode 100644 contracts/crosschain/optimism/CrossChainEnabledOptimism.sol delete mode 100644 contracts/crosschain/optimism/LibOptimism.sol delete mode 100644 contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol delete mode 100644 contracts/governance/extensions/GovernorProposalThreshold.sol delete mode 100644 contracts/interfaces/draft-IERC2612.sol delete mode 100644 contracts/mocks/AccessControlCrossChainMock.sol delete mode 100644 contracts/mocks/ConditionalEscrowMock.sol delete mode 100644 contracts/mocks/PullPaymentMock.sol delete mode 100644 contracts/mocks/SafeMathMemoryCheck.sol delete mode 100644 contracts/mocks/TimersBlockNumberImpl.sol delete mode 100644 contracts/mocks/TimersTimestampImpl.sol delete mode 100644 contracts/mocks/crosschain/bridges.sol delete mode 100644 contracts/mocks/crosschain/receivers.sol delete mode 100644 contracts/mocks/token/ERC777Mock.sol delete mode 100644 contracts/mocks/token/ERC777SenderRecipientMock.sol delete mode 100644 contracts/mocks/wizard/MyGovernor1.sol delete mode 100644 contracts/mocks/wizard/MyGovernor2.sol delete mode 100644 contracts/mocks/wizard/MyGovernor3.sol delete mode 100644 contracts/security/PullPayment.sol delete mode 100644 contracts/token/ERC20/extensions/draft-ERC20Permit.sol delete mode 100644 contracts/token/ERC20/extensions/draft-IERC20Permit.sol delete mode 100644 contracts/token/ERC721/extensions/draft-ERC721Votes.sol delete mode 100644 contracts/token/ERC777/ERC777.sol delete mode 100644 contracts/token/ERC777/IERC777.sol delete mode 100644 contracts/token/ERC777/IERC777Recipient.sol delete mode 100644 contracts/token/ERC777/IERC777Sender.sol delete mode 100644 contracts/token/ERC777/README.adoc delete mode 100644 contracts/utils/Timers.sol delete mode 100644 contracts/utils/cryptography/draft-EIP712.sol delete mode 100644 contracts/utils/escrow/ConditionalEscrow.sol delete mode 100644 contracts/utils/escrow/Escrow.sol delete mode 100644 contracts/utils/escrow/RefundEscrow.sol delete mode 100644 contracts/utils/introspection/ERC1820Implementer.sol delete mode 100644 contracts/utils/introspection/IERC1820Implementer.sol delete mode 100644 contracts/utils/introspection/IERC1820Registry.sol delete mode 100644 contracts/utils/math/SafeMath.sol delete mode 100644 contracts/utils/math/SignedSafeMath.sol delete mode 100644 contracts/vendor/amb/IAMB.sol delete mode 100644 contracts/vendor/arbitrum/IArbSys.sol delete mode 100644 contracts/vendor/arbitrum/IBridge.sol delete mode 100644 contracts/vendor/arbitrum/IDelayedMessageProvider.sol delete mode 100644 contracts/vendor/arbitrum/IInbox.sol delete mode 100644 contracts/vendor/arbitrum/IOutbox.sol delete mode 100644 contracts/vendor/optimism/ICrossDomainMessenger.sol delete mode 100644 contracts/vendor/optimism/LICENSE delete mode 100644 contracts/vendor/polygon/IFxMessageProcessor.sol delete mode 100644 docs/modules/ROOT/pages/crosschain.adoc delete mode 100644 docs/modules/ROOT/pages/erc777.adoc delete mode 100644 test/access/AccessControlCrossChain.test.js delete mode 100644 test/crosschain/CrossChainEnabled.test.js delete mode 100644 test/helpers/crosschain.js delete mode 100644 test/security/PullPayment.test.js delete mode 100644 test/token/ERC777/ERC777.behavior.js delete mode 100644 test/token/ERC777/ERC777.test.js delete mode 100644 test/utils/TimersBlockNumberImpl.test.js delete mode 100644 test/utils/TimersTimestamp.test.js delete mode 100644 test/utils/escrow/ConditionalEscrow.test.js delete mode 100644 test/utils/escrow/Escrow.behavior.js delete mode 100644 test/utils/escrow/Escrow.test.js delete mode 100644 test/utils/escrow/RefundEscrow.test.js delete mode 100644 test/utils/introspection/ERC1820Implementer.test.js delete mode 100644 test/utils/math/SafeMath.test.js delete mode 100644 test/utils/math/SignedSafeMath.test.js diff --git a/.changeset/beige-ducks-flow.md b/.changeset/beige-ducks-flow.md new file mode 100644 index 000000000..4edc870b5 --- /dev/null +++ b/.changeset/beige-ducks-flow.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Remove deprecated GovernorProposalThreshold module. diff --git a/.changeset/fluffy-gifts-build.md b/.changeset/fluffy-gifts-build.md new file mode 100644 index 000000000..a1ffe8be1 --- /dev/null +++ b/.changeset/fluffy-gifts-build.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Remove ERC1820Implementer. diff --git a/.changeset/friendly-suits-camp.md b/.changeset/friendly-suits-camp.md new file mode 100644 index 000000000..bcf1d7ee4 --- /dev/null +++ b/.changeset/friendly-suits-camp.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Remove Checkpoints.History. diff --git a/.changeset/selfish-queens-rest.md b/.changeset/selfish-queens-rest.md new file mode 100644 index 000000000..739fa1356 --- /dev/null +++ b/.changeset/selfish-queens-rest.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Remove PullPayment and Escrow contracts (Escrow, ConditionalEscrow, RefundEscrow). diff --git a/.changeset/spicy-ducks-cough.md b/.changeset/spicy-ducks-cough.md new file mode 100644 index 000000000..bf7296e4e --- /dev/null +++ b/.changeset/spicy-ducks-cough.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Remove the Timers library. diff --git a/.changeset/swift-berries-sort.md b/.changeset/swift-berries-sort.md new file mode 100644 index 000000000..9af6cba64 --- /dev/null +++ b/.changeset/swift-berries-sort.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Remove ERC777 implementation. diff --git a/.changeset/tame-geckos-search.md b/.changeset/tame-geckos-search.md new file mode 100644 index 000000000..6b6ae86d0 --- /dev/null +++ b/.changeset/tame-geckos-search.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Remove SafeMath and SignedSafeMath libraries. diff --git a/.changeset/three-weeks-double.md b/.changeset/three-weeks-double.md new file mode 100644 index 000000000..d267e72c4 --- /dev/null +++ b/.changeset/three-weeks-double.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Remove CrossChain contracts, including AccessControlCrossChain and all the vendored bridge interfaces. diff --git a/contracts/access/AccessControl.sol b/contracts/access/AccessControl.sol index 3a73de78b..24fe0c00c 100644 --- a/contracts/access/AccessControl.sol +++ b/contracts/access/AccessControl.sol @@ -183,30 +183,6 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { _revokeRole(role, account); } - /** - * @dev Grants `role` to `account`. - * - * If `account` had not been already granted `role`, emits a {RoleGranted} - * event. Note that unlike {grantRole}, this function doesn't perform any - * checks on the calling account. - * - * May emit a {RoleGranted} event. - * - * [WARNING] - * ==== - * This function should only be called from the constructor when setting - * up the initial roles for the system. - * - * Using this function in any other way is effectively circumventing the admin - * system imposed by {AccessControl}. - * ==== - * - * NOTE: This function is deprecated in favor of {_grantRole}. - */ - function _setupRole(bytes32 role, address account) internal virtual { - _grantRole(role, account); - } - /** * @dev Sets `adminRole` as ``role``'s admin role. * diff --git a/contracts/access/AccessControlCrossChain.sol b/contracts/access/AccessControlCrossChain.sol deleted file mode 100644 index 95be5091c..000000000 --- a/contracts/access/AccessControlCrossChain.sol +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (access/AccessControlCrossChain.sol) - -pragma solidity ^0.8.4; - -import "./AccessControl.sol"; -import "../crosschain/CrossChainEnabled.sol"; - -/** - * @dev An extension to {AccessControl} with support for cross-chain access management. - * For each role, is extension implements an equivalent "aliased" role that is used for - * restricting calls originating from other chains. - * - * For example, if a function `myFunction` is protected by `onlyRole(SOME_ROLE)`, and - * if an address `x` has role `SOME_ROLE`, it would be able to call `myFunction` directly. - * A wallet or contract at the same address on another chain would however not be able - * to call this function. In order to do so, it would require to have the role - * `_crossChainRoleAlias(SOME_ROLE)`. - * - * This aliasing is required to protect against multiple contracts living at the same - * address on different chains but controlled by conflicting entities. - * - * _Available since v4.6._ - */ -abstract contract AccessControlCrossChain is AccessControl, CrossChainEnabled { - bytes32 public constant CROSSCHAIN_ALIAS = keccak256("CROSSCHAIN_ALIAS"); - - /** - * @dev See {AccessControl-_checkRole}. - */ - function _checkRole(bytes32 role) internal view virtual override { - if (_isCrossChain()) { - _checkRole(_crossChainRoleAlias(role), _crossChainSender()); - } else { - super._checkRole(role); - } - } - - /** - * @dev Returns the aliased role corresponding to `role`. - */ - function _crossChainRoleAlias(bytes32 role) internal pure virtual returns (bytes32) { - return role ^ CROSSCHAIN_ALIAS; - } -} diff --git a/contracts/access/README.adoc b/contracts/access/README.adoc index 80ca0020f..117cd7c97 100644 --- a/contracts/access/README.adoc +++ b/contracts/access/README.adoc @@ -18,8 +18,6 @@ This directory provides ways to restrict who can access the functions of a contr {{AccessControl}} -{{AccessControlCrossChain}} - {{IAccessControlEnumerable}} {{AccessControlEnumerable}} diff --git a/contracts/crosschain/CrossChainEnabled.sol b/contracts/crosschain/CrossChainEnabled.sol deleted file mode 100644 index 4c9b9e5c5..000000000 --- a/contracts/crosschain/CrossChainEnabled.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (crosschain/CrossChainEnabled.sol) - -pragma solidity ^0.8.4; - -import "./errors.sol"; - -/** - * @dev Provides information for building cross-chain aware contracts. This - * abstract contract provides accessors and modifiers to control the execution - * flow when receiving cross-chain messages. - * - * Actual implementations of cross-chain aware contracts, which are based on - * this abstraction, will have to inherit from a bridge-specific - * specialization. Such specializations are provided under - * `crosschain//CrossChainEnabled.sol`. - * - * _Available since v4.6._ - */ -abstract contract CrossChainEnabled { - /** - * @dev Throws if the current function call is not the result of a - * cross-chain execution. - */ - modifier onlyCrossChain() { - if (!_isCrossChain()) revert NotCrossChainCall(); - _; - } - - /** - * @dev Throws if the current function call is not the result of a - * cross-chain execution initiated by `account`. - */ - modifier onlyCrossChainSender(address expected) { - address actual = _crossChainSender(); - if (expected != actual) revert InvalidCrossChainSender(actual, expected); - _; - } - - /** - * @dev Returns whether the current function call is the result of a - * cross-chain message. - */ - function _isCrossChain() internal view virtual returns (bool); - - /** - * @dev Returns the address of the sender of the cross-chain message that - * triggered the current function call. - * - * IMPORTANT: Should revert with `NotCrossChainCall` if the current function - * call is not the result of a cross-chain message. - */ - function _crossChainSender() internal view virtual returns (address); -} diff --git a/contracts/crosschain/README.adoc b/contracts/crosschain/README.adoc deleted file mode 100644 index 266b1530f..000000000 --- a/contracts/crosschain/README.adoc +++ /dev/null @@ -1,34 +0,0 @@ -= Cross Chain Awareness - -[.readme-notice] -NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/crosschain - -This directory provides building blocks to improve cross-chain awareness of smart contracts. - -- {CrossChainEnabled} is an abstraction that contains accessors and modifiers to control the execution flow when receiving cross-chain messages. - -== CrossChainEnabled specializations - -The following specializations of {CrossChainEnabled} provide implementations of the {CrossChainEnabled} abstraction for specific bridges. This can be used to complex cross-chain aware components such as {AccessControlCrossChain}. - -{{CrossChainEnabledAMB}} - -{{CrossChainEnabledArbitrumL1}} - -{{CrossChainEnabledArbitrumL2}} - -{{CrossChainEnabledOptimism}} - -{{CrossChainEnabledPolygonChild}} - -== Libraries for cross-chain - -In addition to the {CrossChainEnabled} abstraction, cross-chain awareness is also available through libraries. These libraries can be used to build complex designs such as contracts with the ability to interact with multiple bridges. - -{{LibAMB}} - -{{LibArbitrumL1}} - -{{LibArbitrumL2}} - -{{LibOptimism}} diff --git a/contracts/crosschain/amb/CrossChainEnabledAMB.sol b/contracts/crosschain/amb/CrossChainEnabledAMB.sol deleted file mode 100644 index e69355d62..000000000 --- a/contracts/crosschain/amb/CrossChainEnabledAMB.sol +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (crosschain/amb/CrossChainEnabledAMB.sol) - -pragma solidity ^0.8.4; - -import "../CrossChainEnabled.sol"; -import "./LibAMB.sol"; - -/** - * @dev https://docs.tokenbridge.net/amb-bridge/about-amb-bridge[AMB] - * specialization or the {CrossChainEnabled} abstraction. - * - * As of february 2020, AMB bridges are available between the following chains: - * - * - https://docs.tokenbridge.net/eth-xdai-amb-bridge/about-the-eth-xdai-amb[ETH ⇌ xDai] - * - https://docs.tokenbridge.net/eth-qdai-bridge/about-the-eth-qdai-amb[ETH ⇌ qDai] - * - https://docs.tokenbridge.net/eth-etc-amb-bridge/about-the-eth-etc-amb[ETH ⇌ ETC] - * - https://docs.tokenbridge.net/eth-bsc-amb/about-the-eth-bsc-amb[ETH ⇌ BSC] - * - https://docs.tokenbridge.net/eth-poa-amb-bridge/about-the-eth-poa-amb[ETH ⇌ POA] - * - https://docs.tokenbridge.net/bsc-xdai-amb/about-the-bsc-xdai-amb[BSC ⇌ xDai] - * - https://docs.tokenbridge.net/poa-xdai-amb/about-the-poa-xdai-amb[POA ⇌ xDai] - * - https://docs.tokenbridge.net/rinkeby-xdai-amb-bridge/about-the-rinkeby-xdai-amb[Rinkeby ⇌ xDai] - * - https://docs.tokenbridge.net/kovan-sokol-amb-bridge/about-the-kovan-sokol-amb[Kovan ⇌ Sokol] - * - * _Available since v4.6._ - */ -contract CrossChainEnabledAMB is CrossChainEnabled { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - address private immutable _bridge; - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address bridge) { - _bridge = bridge; - } - - /** - * @dev see {CrossChainEnabled-_isCrossChain} - */ - function _isCrossChain() internal view virtual override returns (bool) { - return LibAMB.isCrossChain(_bridge); - } - - /** - * @dev see {CrossChainEnabled-_crossChainSender} - */ - function _crossChainSender() internal view virtual override onlyCrossChain returns (address) { - return LibAMB.crossChainSender(_bridge); - } -} diff --git a/contracts/crosschain/amb/LibAMB.sol b/contracts/crosschain/amb/LibAMB.sol deleted file mode 100644 index aef9c43ee..000000000 --- a/contracts/crosschain/amb/LibAMB.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (crosschain/amb/LibAMB.sol) - -pragma solidity ^0.8.4; - -import {IAMB as AMB_Bridge} from "../../vendor/amb/IAMB.sol"; -import "../errors.sol"; - -/** - * @dev Primitives for cross-chain aware contracts using the - * https://docs.tokenbridge.net/amb-bridge/about-amb-bridge[AMB] - * family of bridges. - */ -library LibAMB { - /** - * @dev Returns whether the current function call is the result of a - * cross-chain message relayed by `bridge`. - */ - function isCrossChain(address bridge) internal view returns (bool) { - return msg.sender == bridge; - } - - /** - * @dev Returns the address of the sender that triggered the current - * cross-chain message through `bridge`. - * - * NOTE: {isCrossChain} should be checked before trying to recover the - * sender, as it will revert with `NotCrossChainCall` if the current - * function call is not the result of a cross-chain message. - */ - function crossChainSender(address bridge) internal view returns (address) { - if (!isCrossChain(bridge)) revert NotCrossChainCall(); - return AMB_Bridge(bridge).messageSender(); - } -} diff --git a/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol b/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol deleted file mode 100644 index 5068da395..000000000 --- a/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol) - -pragma solidity ^0.8.4; - -import "../CrossChainEnabled.sol"; -import "./LibArbitrumL1.sol"; - -/** - * @dev https://arbitrum.io/[Arbitrum] specialization or the - * {CrossChainEnabled} abstraction the L1 side (mainnet). - * - * This version should only be deployed on L1 to process cross-chain messages - * originating from L2. For the other side, use {CrossChainEnabledArbitrumL2}. - * - * The bridge contract is provided and maintained by the arbitrum team. You can - * find the address of this contract on the rinkeby testnet in - * https://developer.offchainlabs.com/docs/useful_addresses[Arbitrum's developer documentation]. - * - * _Available since v4.6._ - */ -abstract contract CrossChainEnabledArbitrumL1 is CrossChainEnabled { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - address private immutable _bridge; - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address bridge) { - _bridge = bridge; - } - - /** - * @dev see {CrossChainEnabled-_isCrossChain} - */ - function _isCrossChain() internal view virtual override returns (bool) { - return LibArbitrumL1.isCrossChain(_bridge); - } - - /** - * @dev see {CrossChainEnabled-_crossChainSender} - */ - function _crossChainSender() internal view virtual override onlyCrossChain returns (address) { - return LibArbitrumL1.crossChainSender(_bridge); - } -} diff --git a/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol b/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol deleted file mode 100644 index e85993dbb..000000000 --- a/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol) - -pragma solidity ^0.8.4; - -import "../CrossChainEnabled.sol"; -import "./LibArbitrumL2.sol"; - -/** - * @dev https://arbitrum.io/[Arbitrum] specialization or the - * {CrossChainEnabled} abstraction the L2 side (arbitrum). - * - * This version should only be deployed on L2 to process cross-chain messages - * originating from L1. For the other side, use {CrossChainEnabledArbitrumL1}. - * - * Arbitrum L2 includes the `ArbSys` contract at a fixed address. Therefore, - * this specialization of {CrossChainEnabled} does not include a constructor. - * - * _Available since v4.6._ - * - * WARNING: There is currently a bug in Arbitrum that causes this contract to - * fail to detect cross-chain calls when deployed behind a proxy. This will be - * fixed when the network is upgraded to Arbitrum Nitro, currently scheduled for - * August 31st 2022. - */ -abstract contract CrossChainEnabledArbitrumL2 is CrossChainEnabled { - /** - * @dev see {CrossChainEnabled-_isCrossChain} - */ - function _isCrossChain() internal view virtual override returns (bool) { - return LibArbitrumL2.isCrossChain(LibArbitrumL2.ARBSYS); - } - - /** - * @dev see {CrossChainEnabled-_crossChainSender} - */ - function _crossChainSender() internal view virtual override onlyCrossChain returns (address) { - return LibArbitrumL2.crossChainSender(LibArbitrumL2.ARBSYS); - } -} diff --git a/contracts/crosschain/arbitrum/LibArbitrumL1.sol b/contracts/crosschain/arbitrum/LibArbitrumL1.sol deleted file mode 100644 index be7236b24..000000000 --- a/contracts/crosschain/arbitrum/LibArbitrumL1.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (crosschain/arbitrum/LibArbitrumL1.sol) - -pragma solidity ^0.8.4; - -import {IBridge as ArbitrumL1_Bridge} from "../../vendor/arbitrum/IBridge.sol"; -import {IOutbox as ArbitrumL1_Outbox} from "../../vendor/arbitrum/IOutbox.sol"; -import "../errors.sol"; - -/** - * @dev Primitives for cross-chain aware contracts for - * https://arbitrum.io/[Arbitrum]. - * - * This version should only be used on L1 to process cross-chain messages - * originating from L2. For the other side, use {LibArbitrumL2}. - */ -library LibArbitrumL1 { - /** - * @dev Returns whether the current function call is the result of a - * cross-chain message relayed by the `bridge`. - */ - function isCrossChain(address bridge) internal view returns (bool) { - return msg.sender == bridge; - } - - /** - * @dev Returns the address of the sender that triggered the current - * cross-chain message through the `bridge`. - * - * NOTE: {isCrossChain} should be checked before trying to recover the - * sender, as it will revert with `NotCrossChainCall` if the current - * function call is not the result of a cross-chain message. - */ - function crossChainSender(address bridge) internal view returns (address) { - if (!isCrossChain(bridge)) revert NotCrossChainCall(); - - address sender = ArbitrumL1_Outbox(ArbitrumL1_Bridge(bridge).activeOutbox()).l2ToL1Sender(); - require(sender != address(0), "LibArbitrumL1: system messages without sender"); - - return sender; - } -} diff --git a/contracts/crosschain/arbitrum/LibArbitrumL2.sol b/contracts/crosschain/arbitrum/LibArbitrumL2.sol deleted file mode 100644 index 715a3878f..000000000 --- a/contracts/crosschain/arbitrum/LibArbitrumL2.sol +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (crosschain/arbitrum/LibArbitrumL2.sol) - -pragma solidity ^0.8.4; - -import {IArbSys as ArbitrumL2_Bridge} from "../../vendor/arbitrum/IArbSys.sol"; -import "../errors.sol"; - -/** - * @dev Primitives for cross-chain aware contracts for - * https://arbitrum.io/[Arbitrum]. - * - * This version should only be used on L2 to process cross-chain messages - * originating from L1. For the other side, use {LibArbitrumL1}. - * - * WARNING: There is currently a bug in Arbitrum that causes this contract to - * fail to detect cross-chain calls when deployed behind a proxy. This will be - * fixed when the network is upgraded to Arbitrum Nitro, currently scheduled for - * August 31st 2022. - */ -library LibArbitrumL2 { - /** - * @dev Returns whether the current function call is the result of a - * cross-chain message relayed by `arbsys`. - */ - address public constant ARBSYS = 0x0000000000000000000000000000000000000064; - - function isCrossChain(address arbsys) internal view returns (bool) { - return ArbitrumL2_Bridge(arbsys).wasMyCallersAddressAliased(); - } - - /** - * @dev Returns the address of the sender that triggered the current - * cross-chain message through `arbsys`. - * - * NOTE: {isCrossChain} should be checked before trying to recover the - * sender, as it will revert with `NotCrossChainCall` if the current - * function call is not the result of a cross-chain message. - */ - function crossChainSender(address arbsys) internal view returns (address) { - if (!isCrossChain(arbsys)) revert NotCrossChainCall(); - - return ArbitrumL2_Bridge(arbsys).myCallersAddressWithoutAliasing(); - } -} diff --git a/contracts/crosschain/errors.sol b/contracts/crosschain/errors.sol deleted file mode 100644 index 004460e97..000000000 --- a/contracts/crosschain/errors.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (crosschain/errors.sol) - -pragma solidity ^0.8.4; - -error NotCrossChainCall(); -error InvalidCrossChainSender(address actual, address expected); diff --git a/contracts/crosschain/optimism/CrossChainEnabledOptimism.sol b/contracts/crosschain/optimism/CrossChainEnabledOptimism.sol deleted file mode 100644 index 1005864ae..000000000 --- a/contracts/crosschain/optimism/CrossChainEnabledOptimism.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (crosschain/optimism/CrossChainEnabledOptimism.sol) - -pragma solidity ^0.8.4; - -import "../CrossChainEnabled.sol"; -import "./LibOptimism.sol"; - -/** - * @dev https://www.optimism.io/[Optimism] specialization or the - * {CrossChainEnabled} abstraction. - * - * The messenger (`CrossDomainMessenger`) contract is provided and maintained by - * the optimism team. You can find the address of this contract on mainnet and - * kovan in the https://github.com/ethereum-optimism/optimism/tree/develop/packages/contracts/deployments[deployments section of Optimism monorepo]. - * - * _Available since v4.6._ - */ -abstract contract CrossChainEnabledOptimism is CrossChainEnabled { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - address private immutable _messenger; - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address messenger) { - _messenger = messenger; - } - - /** - * @dev see {CrossChainEnabled-_isCrossChain} - */ - function _isCrossChain() internal view virtual override returns (bool) { - return LibOptimism.isCrossChain(_messenger); - } - - /** - * @dev see {CrossChainEnabled-_crossChainSender} - */ - function _crossChainSender() internal view virtual override onlyCrossChain returns (address) { - return LibOptimism.crossChainSender(_messenger); - } -} diff --git a/contracts/crosschain/optimism/LibOptimism.sol b/contracts/crosschain/optimism/LibOptimism.sol deleted file mode 100644 index d963ade2a..000000000 --- a/contracts/crosschain/optimism/LibOptimism.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (crosschain/optimism/LibOptimism.sol) - -pragma solidity ^0.8.4; - -import {ICrossDomainMessenger as Optimism_Bridge} from "../../vendor/optimism/ICrossDomainMessenger.sol"; -import "../errors.sol"; - -/** - * @dev Primitives for cross-chain aware contracts for https://www.optimism.io/[Optimism]. - * See the https://community.optimism.io/docs/developers/bridge/messaging/#accessing-msg-sender[documentation] - * for the functionality used here. - */ -library LibOptimism { - /** - * @dev Returns whether the current function call is the result of a - * cross-chain message relayed by `messenger`. - */ - function isCrossChain(address messenger) internal view returns (bool) { - return msg.sender == messenger; - } - - /** - * @dev Returns the address of the sender that triggered the current - * cross-chain message through `messenger`. - * - * NOTE: {isCrossChain} should be checked before trying to recover the - * sender, as it will revert with `NotCrossChainCall` if the current - * function call is not the result of a cross-chain message. - */ - function crossChainSender(address messenger) internal view returns (address) { - if (!isCrossChain(messenger)) revert NotCrossChainCall(); - - return Optimism_Bridge(messenger).xDomainMessageSender(); - } -} diff --git a/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol b/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol deleted file mode 100644 index fa0994834..000000000 --- a/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (crosschain/polygon/CrossChainEnabledPolygonChild.sol) - -pragma solidity ^0.8.4; - -import "../CrossChainEnabled.sol"; -import "../../security/ReentrancyGuard.sol"; -import "../../utils/Address.sol"; -import "../../vendor/polygon/IFxMessageProcessor.sol"; - -address constant DEFAULT_SENDER = 0x000000000000000000000000000000000000dEaD; - -/** - * @dev https://polygon.technology/[Polygon] specialization or the - * {CrossChainEnabled} abstraction the child side (polygon/mumbai). - * - * This version should only be deployed on child chain to process cross-chain - * messages originating from the parent chain. - * - * The fxChild contract is provided and maintained by the polygon team. You can - * find the address of this contract polygon and mumbai in - * https://docs.polygon.technology/docs/develop/l1-l2-communication/fx-portal/#contract-addresses[Polygon's Fx-Portal documentation]. - * - * _Available since v4.6._ - */ -abstract contract CrossChainEnabledPolygonChild is IFxMessageProcessor, CrossChainEnabled, ReentrancyGuard { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - address private immutable _fxChild; - address private _sender = DEFAULT_SENDER; - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address fxChild) { - _fxChild = fxChild; - } - - /** - * @dev see {CrossChainEnabled-_isCrossChain} - */ - function _isCrossChain() internal view virtual override returns (bool) { - return msg.sender == _fxChild; - } - - /** - * @dev see {CrossChainEnabled-_crossChainSender} - */ - function _crossChainSender() internal view virtual override onlyCrossChain returns (address) { - return _sender; - } - - /** - * @dev External entry point to receive and relay messages originating - * from the fxChild. - * - * Non-reentrancy is crucial to avoid a cross-chain call being able - * to impersonate anyone by just looping through this with user-defined - * arguments. - * - * Note: if _fxChild calls any other function that does a delegate-call, - * then security could be compromised. - */ - function processMessageFromRoot( - uint256 /* stateId */, - address rootMessageSender, - bytes calldata data - ) external override nonReentrant { - if (!_isCrossChain()) revert NotCrossChainCall(); - - _sender = rootMessageSender; - Address.functionDelegateCall(address(this), data, "cross-chain execution failed"); - _sender = DEFAULT_SENDER; - } -} diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index d47f59a77..171283662 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -80,10 +80,6 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorCompatibilityBravo}} -=== Deprecated - -{{GovernorProposalThreshold}} - == Utils {{Votes}} diff --git a/contracts/governance/TimelockController.sol b/contracts/governance/TimelockController.sol index a8906a5ca..d2c2ae93a 100644 --- a/contracts/governance/TimelockController.sol +++ b/contracts/governance/TimelockController.sol @@ -88,13 +88,13 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver // register proposers and cancellers for (uint256 i = 0; i < proposers.length; ++i) { - _setupRole(PROPOSER_ROLE, proposers[i]); - _setupRole(CANCELLER_ROLE, proposers[i]); + _grantRole(PROPOSER_ROLE, proposers[i]); + _grantRole(CANCELLER_ROLE, proposers[i]); } // register executors for (uint256 i = 0; i < executors.length; ++i) { - _setupRole(EXECUTOR_ROLE, executors[i]); + _grantRole(EXECUTOR_ROLE, executors[i]); } _minDelay = minDelay; diff --git a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol index 1332ac79d..db7301ef4 100644 --- a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol @@ -26,7 +26,6 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp } struct ProposalDetails { - address proposer; address[] targets; uint256[] values; string[] signatures; @@ -56,7 +55,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp string memory description ) public virtual override(IGovernor, Governor) returns (uint256) { // Stores the proposal details (if not already present) and executes the propose logic from the core. - _storeProposal(_msgSender(), targets, values, new string[](calldatas.length), calldatas, description); + _storeProposal(targets, values, new string[](calldatas.length), calldatas, description); return super.propose(targets, values, calldatas, description); } @@ -75,7 +74,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp // 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 // is added there is also executed when calling this alternative interface. - _storeProposal(_msgSender(), targets, values, signatures, calldatas, description); + _storeProposal(targets, values, signatures, calldatas, description); return propose(targets, values, _encodeCalldata(signatures, calldatas), description); } @@ -132,7 +131,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp bytes32 descriptionHash ) public virtual override(IGovernor, Governor) returns (uint256) { uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); - address proposer = _proposalDetails[proposalId].proposer; + address proposer = proposalProposer(proposalId); require( _msgSender() == proposer || getVotes(proposer, clock() - 1) < proposalThreshold(), @@ -182,7 +181,6 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp * @dev Store proposal metadata (if not already present) for later lookup. */ function _storeProposal( - address proposer, address[] memory targets, uint256[] memory values, string[] memory signatures, @@ -194,7 +192,6 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp ProposalDetails storage details = _proposalDetails[proposalId]; if (details.descriptionHash == bytes32(0)) { - details.proposer = proposer; details.targets = targets; details.values = values; details.signatures = signatures; @@ -228,12 +225,12 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp ) { id = proposalId; + proposer = proposalProposer(proposalId); eta = proposalEta(proposalId); startBlock = proposalSnapshot(proposalId); endBlock = proposalDeadline(proposalId); ProposalDetails storage details = _proposalDetails[proposalId]; - proposer = details.proposer; forVotes = details.forVotes; againstVotes = details.againstVotes; abstainVotes = details.abstainVotes; diff --git a/contracts/governance/extensions/GovernorProposalThreshold.sol b/contracts/governance/extensions/GovernorProposalThreshold.sol deleted file mode 100644 index 3feebace0..000000000 --- a/contracts/governance/extensions/GovernorProposalThreshold.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (governance/extensions/GovernorProposalThreshold.sol) - -pragma solidity ^0.8.0; - -import "../Governor.sol"; - -/** - * @dev Extension of {Governor} for proposal restriction to token holders with a minimum balance. - * - * _Available since v4.3._ - * _Deprecated since v4.4._ - */ -abstract contract GovernorProposalThreshold is Governor { - function propose( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - string memory description - ) public virtual override returns (uint256) { - return super.propose(targets, values, calldatas, description); - } -} diff --git a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol index 403570260..e103ea0bf 100644 --- a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol +++ b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol @@ -16,8 +16,6 @@ import "../../utils/math/SafeCast.sol"; abstract contract GovernorVotesQuorumFraction is GovernorVotes { using Checkpoints for Checkpoints.Trace224; - uint256 private _quorumNumerator; // DEPRECATED in favor of _quorumNumeratorHistory - /// @custom:oz-retyped-from Checkpoints.History Checkpoints.Trace224 private _quorumNumeratorHistory; @@ -38,7 +36,7 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes { * @dev Returns the current quorum numerator. See {quorumDenominator}. */ function quorumNumerator() public view virtual returns (uint256) { - return _quorumNumeratorHistory._checkpoints.length == 0 ? _quorumNumerator : _quorumNumeratorHistory.latest(); + return _quorumNumeratorHistory.latest(); } /** @@ -47,9 +45,6 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes { function quorumNumerator(uint256 timepoint) public view virtual returns (uint256) { // If history is empty, fallback to old storage uint256 length = _quorumNumeratorHistory._checkpoints.length; - if (length == 0) { - return _quorumNumerator; - } // Optimistic search, check the latest checkpoint Checkpoints.Checkpoint224 memory latest = _quorumNumeratorHistory._checkpoints[length - 1]; @@ -105,15 +100,6 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes { ); uint256 oldQuorumNumerator = quorumNumerator(); - - // Make sure we keep track of the original numerator in contracts upgraded from a version without checkpoints. - if (oldQuorumNumerator != 0 && _quorumNumeratorHistory._checkpoints.length == 0) { - _quorumNumeratorHistory._checkpoints.push( - Checkpoints.Checkpoint224({_key: 0, _value: SafeCast.toUint224(oldQuorumNumerator)}) - ); - } - - // Set new quorum for future proposals _quorumNumeratorHistory.push(SafeCast.toUint32(clock()), SafeCast.toUint224(newQuorumNumerator)); emit QuorumNumeratorUpdated(oldQuorumNumerator, newQuorumNumerator); diff --git a/contracts/interfaces/IERC1820Implementer.sol b/contracts/interfaces/IERC1820Implementer.sol index a83a7a304..c4d0b3028 100644 --- a/contracts/interfaces/IERC1820Implementer.sol +++ b/contracts/interfaces/IERC1820Implementer.sol @@ -1,6 +1,20 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1820Implementer.sol) +// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC1820Implementer.sol) pragma solidity ^0.8.0; -import "../utils/introspection/IERC1820Implementer.sol"; +/** + * @dev Interface for an ERC1820 implementer, as defined in the + * https://eips.ethereum.org/EIPS/eip-1820#interface-implementation-erc1820implementerinterface[EIP]. + * Used by contracts that will be registered as implementers in the + * {IERC1820Registry}. + */ +interface IERC1820Implementer { + /** + * @dev Returns a special value (`ERC1820_ACCEPT_MAGIC`) if this contract + * implements `interfaceHash` for `account`. + * + * See {IERC1820Registry-setInterfaceImplementer}. + */ + function canImplementInterfaceForAddress(bytes32 interfaceHash, address account) external view returns (bytes32); +} diff --git a/contracts/interfaces/IERC1820Registry.sol b/contracts/interfaces/IERC1820Registry.sol index 1b1ba9fcf..a146bc2a6 100644 --- a/contracts/interfaces/IERC1820Registry.sol +++ b/contracts/interfaces/IERC1820Registry.sol @@ -1,6 +1,112 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1820Registry.sol) +// OpenZeppelin Contracts (last updated v4.8.0) (utils/introspection/IERC1820Registry.sol) pragma solidity ^0.8.0; -import "../utils/introspection/IERC1820Registry.sol"; +/** + * @dev Interface of the global ERC1820 Registry, as defined in the + * https://eips.ethereum.org/EIPS/eip-1820[EIP]. Accounts may register + * implementers for interfaces in this registry, as well as query support. + * + * Implementers may be shared by multiple accounts, and can also implement more + * than a single interface for each account. Contracts can implement interfaces + * for themselves, but externally-owned accounts (EOA) must delegate this to a + * contract. + * + * {IERC165} interfaces can also be queried via the registry. + * + * For an in-depth explanation and source code analysis, see the EIP text. + */ +interface IERC1820Registry { + event InterfaceImplementerSet(address indexed account, bytes32 indexed interfaceHash, address indexed implementer); + + event ManagerChanged(address indexed account, address indexed newManager); + + /** + * @dev Sets `newManager` as the manager for `account`. A manager of an + * account is able to set interface implementers for it. + * + * By default, each account is its own manager. Passing a value of `0x0` in + * `newManager` will reset the manager to this initial state. + * + * Emits a {ManagerChanged} event. + * + * Requirements: + * + * - the caller must be the current manager for `account`. + */ + function setManager(address account, address newManager) external; + + /** + * @dev Returns the manager for `account`. + * + * See {setManager}. + */ + function getManager(address account) external view returns (address); + + /** + * @dev Sets the `implementer` contract as ``account``'s implementer for + * `interfaceHash`. + * + * `account` being the zero address is an alias for the caller's address. + * The zero address can also be used in `implementer` to remove an old one. + * + * See {interfaceHash} to learn how these are created. + * + * Emits an {InterfaceImplementerSet} event. + * + * Requirements: + * + * - the caller must be the current manager for `account`. + * - `interfaceHash` must not be an {IERC165} interface id (i.e. it must not + * end in 28 zeroes). + * - `implementer` must implement {IERC1820Implementer} and return true when + * queried for support, unless `implementer` is the caller. See + * {IERC1820Implementer-canImplementInterfaceForAddress}. + */ + function setInterfaceImplementer(address account, bytes32 _interfaceHash, address implementer) external; + + /** + * @dev Returns the implementer of `interfaceHash` for `account`. If no such + * implementer is registered, returns the zero address. + * + * If `interfaceHash` is an {IERC165} interface id (i.e. it ends with 28 + * zeroes), `account` will be queried for support of it. + * + * `account` being the zero address is an alias for the caller's address. + */ + function getInterfaceImplementer(address account, bytes32 _interfaceHash) external view returns (address); + + /** + * @dev Returns the interface hash for an `interfaceName`, as defined in the + * corresponding + * https://eips.ethereum.org/EIPS/eip-1820#interface-name[section of the EIP]. + */ + function interfaceHash(string calldata interfaceName) external pure returns (bytes32); + + /** + * @notice Updates the cache with whether the contract implements an ERC165 interface or not. + * @param account Address of the contract for which to update the cache. + * @param interfaceId ERC165 interface for which to update the cache. + */ + function updateERC165Cache(address account, bytes4 interfaceId) external; + + /** + * @notice Checks whether a contract implements an ERC165 interface or not. + * If the result is not cached a direct lookup on the contract address is performed. + * If the result is not cached or the cached value is out-of-date, the cache MUST be updated manually by calling + * {updateERC165Cache} with the contract address. + * @param account Address of the contract to check. + * @param interfaceId ERC165 interface to check. + * @return True if `account` implements `interfaceId`, false otherwise. + */ + function implementsERC165Interface(address account, bytes4 interfaceId) external view returns (bool); + + /** + * @notice Checks whether a contract implements an ERC165 interface or not without using or updating the cache. + * @param account Address of the contract to check. + * @param interfaceId ERC165 interface to check. + * @return True if `account` implements `interfaceId`, false otherwise. + */ + function implementsERC165InterfaceNoCache(address account, bytes4 interfaceId) external view returns (bool); +} diff --git a/contracts/interfaces/IERC777.sol b/contracts/interfaces/IERC777.sol index b97ba7b80..4d36da52c 100644 --- a/contracts/interfaces/IERC777.sol +++ b/contracts/interfaces/IERC777.sol @@ -1,6 +1,199 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC777.sol) pragma solidity ^0.8.0; -import "../token/ERC777/IERC777.sol"; +/** + * @dev Interface of the ERC777Token standard as defined in the EIP. + * + * This contract uses the + * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 registry standard] to let + * token holders and recipients react to token movements by using setting implementers + * for the associated interfaces in said registry. See {IERC1820Registry} and + * {IERC1820Implementer}. + */ +interface IERC777 { + /** + * @dev Emitted when `amount` tokens are created by `operator` and assigned to `to`. + * + * Note that some additional user `data` and `operatorData` can be logged in the event. + */ + event Minted(address indexed operator, address indexed to, uint256 amount, bytes data, bytes operatorData); + + /** + * @dev Emitted when `operator` destroys `amount` tokens from `account`. + * + * Note that some additional user `data` and `operatorData` can be logged in the event. + */ + event Burned(address indexed operator, address indexed from, uint256 amount, bytes data, bytes operatorData); + + /** + * @dev Emitted when `operator` is made operator for `tokenHolder`. + */ + event AuthorizedOperator(address indexed operator, address indexed tokenHolder); + + /** + * @dev Emitted when `operator` is revoked its operator status for `tokenHolder`. + */ + event RevokedOperator(address indexed operator, address indexed tokenHolder); + + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the smallest part of the token that is not divisible. This + * means all token operations (creation, movement and destruction) must have + * amounts that are a multiple of this number. + * + * For most token contracts, this value will equal 1. + */ + function granularity() external view returns (uint256); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by an account (`owner`). + */ + function balanceOf(address owner) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * If send or receive hooks are registered for the caller and `recipient`, + * the corresponding functions will be called with `data` and empty + * `operatorData`. See {IERC777Sender} and {IERC777Recipient}. + * + * Emits a {Sent} event. + * + * Requirements + * + * - the caller must have at least `amount` tokens. + * - `recipient` cannot be the zero address. + * - if `recipient` is a contract, it must implement the {IERC777Recipient} + * interface. + */ + function send(address recipient, uint256 amount, bytes calldata data) external; + + /** + * @dev Destroys `amount` tokens from the caller's account, reducing the + * total supply. + * + * If a send hook is registered for the caller, the corresponding function + * will be called with `data` and empty `operatorData`. See {IERC777Sender}. + * + * Emits a {Burned} event. + * + * Requirements + * + * - the caller must have at least `amount` tokens. + */ + function burn(uint256 amount, bytes calldata data) external; + + /** + * @dev Returns true if an account is an operator of `tokenHolder`. + * Operators can send and burn tokens on behalf of their owners. All + * accounts are their own operator. + * + * See {operatorSend} and {operatorBurn}. + */ + function isOperatorFor(address operator, address tokenHolder) external view returns (bool); + + /** + * @dev Make an account an operator of the caller. + * + * See {isOperatorFor}. + * + * Emits an {AuthorizedOperator} event. + * + * Requirements + * + * - `operator` cannot be calling address. + */ + function authorizeOperator(address operator) external; + + /** + * @dev Revoke an account's operator status for the caller. + * + * See {isOperatorFor} and {defaultOperators}. + * + * Emits a {RevokedOperator} event. + * + * Requirements + * + * - `operator` cannot be calling address. + */ + function revokeOperator(address operator) external; + + /** + * @dev Returns the list of default operators. These accounts are operators + * for all token holders, even if {authorizeOperator} was never called on + * them. + * + * This list is immutable, but individual holders may revoke these via + * {revokeOperator}, in which case {isOperatorFor} will return false. + */ + function defaultOperators() external view returns (address[] memory); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient`. The caller must + * be an operator of `sender`. + * + * If send or receive hooks are registered for `sender` and `recipient`, + * the corresponding functions will be called with `data` and + * `operatorData`. See {IERC777Sender} and {IERC777Recipient}. + * + * Emits a {Sent} event. + * + * Requirements + * + * - `sender` cannot be the zero address. + * - `sender` must have at least `amount` tokens. + * - the caller must be an operator for `sender`. + * - `recipient` cannot be the zero address. + * - if `recipient` is a contract, it must implement the {IERC777Recipient} + * interface. + */ + function operatorSend( + address sender, + address recipient, + uint256 amount, + bytes calldata data, + bytes calldata operatorData + ) external; + + /** + * @dev Destroys `amount` tokens from `account`, reducing the total supply. + * The caller must be an operator of `account`. + * + * If a send hook is registered for `account`, the corresponding function + * will be called with `data` and `operatorData`. See {IERC777Sender}. + * + * Emits a {Burned} event. + * + * Requirements + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + * - the caller must be an operator for `account`. + */ + function operatorBurn(address account, uint256 amount, bytes calldata data, bytes calldata operatorData) external; + + event Sent( + address indexed operator, + address indexed from, + address indexed to, + uint256 amount, + bytes data, + bytes operatorData + ); +} diff --git a/contracts/interfaces/IERC777Recipient.sol b/contracts/interfaces/IERC777Recipient.sol index 0ce2704a8..069904855 100644 --- a/contracts/interfaces/IERC777Recipient.sol +++ b/contracts/interfaces/IERC777Recipient.sol @@ -1,6 +1,34 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC777Recipient.sol) pragma solidity ^0.8.0; -import "../token/ERC777/IERC777Recipient.sol"; +/** + * @dev Interface of the ERC777TokensRecipient standard as defined in the EIP. + * + * Accounts can be notified of {IERC777} tokens being sent to them by having a + * contract implement this interface (contract holders can be their own + * implementer) and registering it on the + * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 global registry]. + * + * See {IERC1820Registry} and {IERC1820Implementer}. + */ +interface IERC777Recipient { + /** + * @dev Called by an {IERC777} token contract whenever tokens are being + * moved or created into a registered account (`to`). The type of operation + * is conveyed by `from` being the zero address or not. + * + * This call occurs _after_ the token contract's state is updated, so + * {IERC777-balanceOf}, etc., can be used to query the post-operation state. + * + * This function may revert to prevent the operation from being executed. + */ + function tokensReceived( + address operator, + address from, + address to, + uint256 amount, + bytes calldata userData, + bytes calldata operatorData + ) external; +} diff --git a/contracts/interfaces/IERC777Sender.sol b/contracts/interfaces/IERC777Sender.sol index f1f17a22e..c45477fcc 100644 --- a/contracts/interfaces/IERC777Sender.sol +++ b/contracts/interfaces/IERC777Sender.sol @@ -1,6 +1,34 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC777Sender.sol) pragma solidity ^0.8.0; -import "../token/ERC777/IERC777Sender.sol"; +/** + * @dev Interface of the ERC777TokensSender standard as defined in the EIP. + * + * {IERC777} Token holders can be notified of operations performed on their + * tokens by having a contract implement this interface (contract holders can be + * their own implementer) and registering it on the + * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 global registry]. + * + * See {IERC1820Registry} and {IERC1820Implementer}. + */ +interface IERC777Sender { + /** + * @dev Called by an {IERC777} token contract whenever a registered holder's + * (`from`) tokens are about to be moved or destroyed. The type of operation + * is conveyed by `to` being the zero address or not. + * + * This call occurs _before_ the token contract's state is updated, so + * {IERC777-balanceOf}, etc., can be used to query the pre-operation state. + * + * This function may revert to prevent the operation from being executed. + */ + function tokensToSend( + address operator, + address from, + address to, + uint256 amount, + bytes calldata userData, + bytes calldata operatorData + ) external; +} diff --git a/contracts/interfaces/draft-IERC2612.sol b/contracts/interfaces/draft-IERC2612.sol deleted file mode 100644 index 1ea7bf1c0..000000000 --- a/contracts/interfaces/draft-IERC2612.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -// EIP-2612 is Final as of 2022-11-01. This file is deprecated. - -import "./IERC2612.sol"; diff --git a/contracts/mocks/AccessControlCrossChainMock.sol b/contracts/mocks/AccessControlCrossChainMock.sol deleted file mode 100644 index cd4c3a5d9..000000000 --- a/contracts/mocks/AccessControlCrossChainMock.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.4; - -import "../access/AccessControlCrossChain.sol"; -import "../crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol"; - -contract AccessControlCrossChainMock is AccessControlCrossChain, CrossChainEnabledArbitrumL2 {} diff --git a/contracts/mocks/ConditionalEscrowMock.sol b/contracts/mocks/ConditionalEscrowMock.sol deleted file mode 100644 index ececf0521..000000000 --- a/contracts/mocks/ConditionalEscrowMock.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../utils/escrow/ConditionalEscrow.sol"; - -// mock class using ConditionalEscrow -contract ConditionalEscrowMock is ConditionalEscrow { - mapping(address => bool) private _allowed; - - function setAllowed(address payee, bool allowed) public { - _allowed[payee] = allowed; - } - - function withdrawalAllowed(address payee) public view override returns (bool) { - return _allowed[payee]; - } -} diff --git a/contracts/mocks/PullPaymentMock.sol b/contracts/mocks/PullPaymentMock.sol deleted file mode 100644 index 8a708e30c..000000000 --- a/contracts/mocks/PullPaymentMock.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../security/PullPayment.sol"; - -// mock class using PullPayment -contract PullPaymentMock is PullPayment { - constructor() payable {} - - // test helper function to call asyncTransfer - function callTransfer(address dest, uint256 amount) public { - _asyncTransfer(dest, amount); - } -} diff --git a/contracts/mocks/SafeMathMemoryCheck.sol b/contracts/mocks/SafeMathMemoryCheck.sol deleted file mode 100644 index 96946881a..000000000 --- a/contracts/mocks/SafeMathMemoryCheck.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../utils/math/SafeMath.sol"; - -library SafeMathMemoryCheck { - function addMemoryCheck() internal pure returns (uint256 mem) { - uint256 length = 32; - assembly { - mem := mload(0x40) - } - for (uint256 i = 0; i < length; ++i) { - SafeMath.add(1, 1); - } - assembly { - mem := sub(mload(0x40), mem) - } - } - - function subMemoryCheck() internal pure returns (uint256 mem) { - uint256 length = 32; - assembly { - mem := mload(0x40) - } - for (uint256 i = 0; i < length; ++i) { - SafeMath.sub(1, 1); - } - assembly { - mem := sub(mload(0x40), mem) - } - } - - function mulMemoryCheck() internal pure returns (uint256 mem) { - uint256 length = 32; - assembly { - mem := mload(0x40) - } - for (uint256 i = 0; i < length; ++i) { - SafeMath.mul(1, 1); - } - assembly { - mem := sub(mload(0x40), mem) - } - } - - function divMemoryCheck() internal pure returns (uint256 mem) { - uint256 length = 32; - assembly { - mem := mload(0x40) - } - for (uint256 i = 0; i < length; ++i) { - SafeMath.div(1, 1); - } - assembly { - mem := sub(mload(0x40), mem) - } - } - - function modMemoryCheck() internal pure returns (uint256 mem) { - uint256 length = 32; - assembly { - mem := mload(0x40) - } - for (uint256 i = 0; i < length; ++i) { - SafeMath.mod(1, 1); - } - assembly { - mem := sub(mload(0x40), mem) - } - } -} diff --git a/contracts/mocks/TimersBlockNumberImpl.sol b/contracts/mocks/TimersBlockNumberImpl.sol deleted file mode 100644 index 84633e6f8..000000000 --- a/contracts/mocks/TimersBlockNumberImpl.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../utils/Timers.sol"; - -contract TimersBlockNumberImpl { - using Timers for Timers.BlockNumber; - - Timers.BlockNumber private _timer; - - function getDeadline() public view returns (uint64) { - return _timer.getDeadline(); - } - - function setDeadline(uint64 timestamp) public { - _timer.setDeadline(timestamp); - } - - function reset() public { - _timer.reset(); - } - - function isUnset() public view returns (bool) { - return _timer.isUnset(); - } - - function isStarted() public view returns (bool) { - return _timer.isStarted(); - } - - function isPending() public view returns (bool) { - return _timer.isPending(); - } - - function isExpired() public view returns (bool) { - return _timer.isExpired(); - } -} diff --git a/contracts/mocks/TimersTimestampImpl.sol b/contracts/mocks/TimersTimestampImpl.sol deleted file mode 100644 index 07f9a1b3f..000000000 --- a/contracts/mocks/TimersTimestampImpl.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../utils/Timers.sol"; - -contract TimersTimestampImpl { - using Timers for Timers.Timestamp; - - Timers.Timestamp private _timer; - - function getDeadline() public view returns (uint64) { - return _timer.getDeadline(); - } - - function setDeadline(uint64 timestamp) public { - _timer.setDeadline(timestamp); - } - - function reset() public { - _timer.reset(); - } - - function isUnset() public view returns (bool) { - return _timer.isUnset(); - } - - function isStarted() public view returns (bool) { - return _timer.isStarted(); - } - - function isPending() public view returns (bool) { - return _timer.isPending(); - } - - function isExpired() public view returns (bool) { - return _timer.isExpired(); - } -} diff --git a/contracts/mocks/crosschain/bridges.sol b/contracts/mocks/crosschain/bridges.sol deleted file mode 100644 index 41baffed8..000000000 --- a/contracts/mocks/crosschain/bridges.sol +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../../utils/Address.sol"; -import "../../vendor/polygon/IFxMessageProcessor.sol"; - -abstract contract BaseRelayMock { - // needed to parse custom errors - error NotCrossChainCall(); - error InvalidCrossChainSender(address sender, address expected); - - address internal _currentSender; - - function relayAs(address target, bytes calldata data, address sender) external virtual { - address previousSender = _currentSender; - - _currentSender = sender; - - (bool success, bytes memory returndata) = target.call(data); - Address.verifyCallResultFromTarget(target, success, returndata, "low-level call reverted"); - - _currentSender = previousSender; - } -} - -/** - * AMB - */ -contract BridgeAMBMock is BaseRelayMock { - function messageSender() public view returns (address) { - return _currentSender; - } -} - -/** - * Arbitrum - */ -contract BridgeArbitrumL1Mock is BaseRelayMock { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment - address public immutable inbox = address(new BridgeArbitrumL1Inbox()); - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment - address public immutable outbox = address(new BridgeArbitrumL1Outbox()); - - function activeOutbox() public view returns (address) { - return outbox; - } - - function currentSender() public view returns (address) { - return _currentSender; - } -} - -contract BridgeArbitrumL1Inbox { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment - address public immutable bridge = msg.sender; -} - -contract BridgeArbitrumL1Outbox { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment - address public immutable bridge = msg.sender; - - function l2ToL1Sender() public view returns (address) { - return BridgeArbitrumL1Mock(bridge).currentSender(); - } -} - -contract BridgeArbitrumL2Mock is BaseRelayMock { - function wasMyCallersAddressAliased() public view returns (bool) { - return _currentSender != address(0); - } - - function myCallersAddressWithoutAliasing() public view returns (address) { - return _currentSender; - } -} - -/** - * Optimism - */ -contract BridgeOptimismMock is BaseRelayMock { - function xDomainMessageSender() public view returns (address) { - return _currentSender; - } -} - -/** - * Polygon - */ -contract BridgePolygonChildMock is BaseRelayMock { - function relayAs(address target, bytes calldata data, address sender) external override { - IFxMessageProcessor(target).processMessageFromRoot(0, sender, data); - } -} diff --git a/contracts/mocks/crosschain/receivers.sol b/contracts/mocks/crosschain/receivers.sol deleted file mode 100644 index 601a2ac10..000000000 --- a/contracts/mocks/crosschain/receivers.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.4; - -import "../../access/Ownable.sol"; -import "../../crosschain/amb/CrossChainEnabledAMB.sol"; -import "../../crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol"; -import "../../crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol"; -import "../../crosschain/optimism/CrossChainEnabledOptimism.sol"; -import "../../crosschain/polygon/CrossChainEnabledPolygonChild.sol"; - -abstract contract Receiver is CrossChainEnabled { - // we don't use Ownable because it messes up testing for the upgradeable contracts - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment - address public immutable owner = msg.sender; - - function crossChainRestricted() external onlyCrossChain {} - - function crossChainOwnerRestricted() external onlyCrossChainSender(owner) {} -} - -/** - * AMB - */ -contract CrossChainEnabledAMBMock is Receiver, CrossChainEnabledAMB { - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address bridge) CrossChainEnabledAMB(bridge) {} -} - -/** - * Arbitrum - */ -contract CrossChainEnabledArbitrumL1Mock is Receiver, CrossChainEnabledArbitrumL1 { - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address bridge) CrossChainEnabledArbitrumL1(bridge) {} -} - -contract CrossChainEnabledArbitrumL2Mock is Receiver, CrossChainEnabledArbitrumL2 {} - -/** - * Optimism - */ -contract CrossChainEnabledOptimismMock is Receiver, CrossChainEnabledOptimism { - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address bridge) CrossChainEnabledOptimism(bridge) {} -} - -/** - * Polygon - */ -contract CrossChainEnabledPolygonChildMock is Receiver, CrossChainEnabledPolygonChild { - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address bridge) CrossChainEnabledPolygonChild(bridge) {} -} diff --git a/contracts/mocks/governance/GovernorMock.sol b/contracts/mocks/governance/GovernorMock.sol index 8a1db4704..9284a24fe 100644 --- a/contracts/mocks/governance/GovernorMock.sol +++ b/contracts/mocks/governance/GovernorMock.sol @@ -2,27 +2,12 @@ pragma solidity ^0.8.0; -import "../../governance/extensions/GovernorProposalThreshold.sol"; import "../../governance/extensions/GovernorSettings.sol"; import "../../governance/extensions/GovernorCountingSimple.sol"; import "../../governance/extensions/GovernorVotesQuorumFraction.sol"; -abstract contract GovernorMock is - GovernorProposalThreshold, - GovernorSettings, - GovernorVotesQuorumFraction, - GovernorCountingSimple -{ +abstract contract GovernorMock is GovernorSettings, GovernorVotesQuorumFraction, GovernorCountingSimple { function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) { return super.proposalThreshold(); } - - function propose( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - string memory description - ) public override(Governor, GovernorProposalThreshold) returns (uint256) { - return super.propose(targets, values, calldatas, description); - } } diff --git a/contracts/mocks/token/ERC20PermitNoRevertMock.sol b/contracts/mocks/token/ERC20PermitNoRevertMock.sol index 2176c51ae..f2562d3cc 100644 --- a/contracts/mocks/token/ERC20PermitNoRevertMock.sol +++ b/contracts/mocks/token/ERC20PermitNoRevertMock.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import "../../token/ERC20/ERC20.sol"; -import "../../token/ERC20/extensions/draft-ERC20Permit.sol"; +import "../../token/ERC20/extensions/ERC20Permit.sol"; abstract contract ERC20PermitNoRevertMock is ERC20Permit { function permitThatMayRevert( diff --git a/contracts/mocks/token/ERC721ConsecutiveMock.sol b/contracts/mocks/token/ERC721ConsecutiveMock.sol index 427f44a19..8bfa0cb9e 100644 --- a/contracts/mocks/token/ERC721ConsecutiveMock.sol +++ b/contracts/mocks/token/ERC721ConsecutiveMock.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import "../../token/ERC721/extensions/ERC721Consecutive.sol"; import "../../token/ERC721/extensions/ERC721Enumerable.sol"; import "../../token/ERC721/extensions/ERC721Pausable.sol"; -import "../../token/ERC721/extensions/draft-ERC721Votes.sol"; +import "../../token/ERC721/extensions/ERC721Votes.sol"; /** * @title ERC721ConsecutiveMock diff --git a/contracts/mocks/token/ERC777Mock.sol b/contracts/mocks/token/ERC777Mock.sol deleted file mode 100644 index 685277e8b..000000000 --- a/contracts/mocks/token/ERC777Mock.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../../token/ERC777/ERC777.sol"; - -abstract contract ERC777Mock is ERC777 { - event BeforeTokenTransfer(); - - function _beforeTokenTransfer(address, address, address, uint256) internal override { - emit BeforeTokenTransfer(); - } -} diff --git a/contracts/mocks/token/ERC777SenderRecipientMock.sol b/contracts/mocks/token/ERC777SenderRecipientMock.sol deleted file mode 100644 index 3bec6d94b..000000000 --- a/contracts/mocks/token/ERC777SenderRecipientMock.sol +++ /dev/null @@ -1,152 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../../token/ERC777/IERC777.sol"; -import "../../token/ERC777/IERC777Sender.sol"; -import "../../token/ERC777/IERC777Recipient.sol"; -import "../../utils/Context.sol"; -import "../../utils/introspection/IERC1820Registry.sol"; -import "../../utils/introspection/ERC1820Implementer.sol"; - -contract ERC777SenderRecipientMock is Context, IERC777Sender, IERC777Recipient, ERC1820Implementer { - event TokensToSendCalled( - address operator, - address from, - address to, - uint256 amount, - bytes data, - bytes operatorData, - address token, - uint256 fromBalance, - uint256 toBalance - ); - - event TokensReceivedCalled( - address operator, - address from, - address to, - uint256 amount, - bytes data, - bytes operatorData, - address token, - uint256 fromBalance, - uint256 toBalance - ); - - // Emitted in ERC777Mock. Here for easier decoding - event BeforeTokenTransfer(); - - bool private _shouldRevertSend; - bool private _shouldRevertReceive; - - IERC1820Registry private _erc1820 = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24); - - bytes32 private constant _TOKENS_SENDER_INTERFACE_HASH = keccak256("ERC777TokensSender"); - bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient"); - - function tokensToSend( - address operator, - address from, - address to, - uint256 amount, - bytes calldata userData, - bytes calldata operatorData - ) external override { - if (_shouldRevertSend) { - revert(); - } - - IERC777 token = IERC777(_msgSender()); - - uint256 fromBalance = token.balanceOf(from); - // when called due to burn, to will be the zero address, which will have a balance of 0 - uint256 toBalance = token.balanceOf(to); - - emit TokensToSendCalled( - operator, - from, - to, - amount, - userData, - operatorData, - address(token), - fromBalance, - toBalance - ); - } - - function tokensReceived( - address operator, - address from, - address to, - uint256 amount, - bytes calldata userData, - bytes calldata operatorData - ) external override { - if (_shouldRevertReceive) { - revert(); - } - - IERC777 token = IERC777(_msgSender()); - - uint256 fromBalance = token.balanceOf(from); - // when called due to burn, to will be the zero address, which will have a balance of 0 - uint256 toBalance = token.balanceOf(to); - - emit TokensReceivedCalled( - operator, - from, - to, - amount, - userData, - operatorData, - address(token), - fromBalance, - toBalance - ); - } - - function senderFor(address account) public { - _registerInterfaceForAddress(_TOKENS_SENDER_INTERFACE_HASH, account); - - address self = address(this); - if (account == self) { - registerSender(self); - } - } - - function registerSender(address sender) public { - _erc1820.setInterfaceImplementer(address(this), _TOKENS_SENDER_INTERFACE_HASH, sender); - } - - function recipientFor(address account) public { - _registerInterfaceForAddress(_TOKENS_RECIPIENT_INTERFACE_HASH, account); - - address self = address(this); - if (account == self) { - registerRecipient(self); - } - } - - function registerRecipient(address recipient) public { - _erc1820.setInterfaceImplementer(address(this), _TOKENS_RECIPIENT_INTERFACE_HASH, recipient); - } - - function setShouldRevertSend(bool shouldRevert) public { - _shouldRevertSend = shouldRevert; - } - - function setShouldRevertReceive(bool shouldRevert) public { - _shouldRevertReceive = shouldRevert; - } - - function send(IERC777 token, address to, uint256 amount, bytes memory data) public { - // This is 777's send function, not the Solidity send function - token.send(to, amount, data); // solhint-disable-line check-send-result - } - - function burn(IERC777 token, uint256 amount, bytes memory data) public { - token.burn(amount, data); - } -} diff --git a/contracts/mocks/wizard/MyGovernor1.sol b/contracts/mocks/wizard/MyGovernor1.sol deleted file mode 100644 index 37ecfd57d..000000000 --- a/contracts/mocks/wizard/MyGovernor1.sol +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "../../governance/Governor.sol"; -import "../../governance/extensions/GovernorCountingSimple.sol"; -import "../../governance/extensions/GovernorVotes.sol"; -import "../../governance/extensions/GovernorVotesQuorumFraction.sol"; -import "../../governance/extensions/GovernorTimelockControl.sol"; - -contract MyGovernor1 is - Governor, - GovernorTimelockControl, - GovernorVotes, - GovernorVotesQuorumFraction, - GovernorCountingSimple -{ - constructor( - IVotes _token, - TimelockController _timelock - ) Governor("MyGovernor") GovernorVotes(_token) GovernorVotesQuorumFraction(4) GovernorTimelockControl(_timelock) {} - - function votingDelay() public pure override returns (uint256) { - return 1; // 1 block - } - - function votingPeriod() public pure override returns (uint256) { - return 45818; // 1 week - } - - // The following functions are overrides required by Solidity. - - function quorum( - uint256 blockNumber - ) public view override(IGovernor, GovernorVotesQuorumFraction) returns (uint256) { - return super.quorum(blockNumber); - } - - function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) { - return super.state(proposalId); - } - - function propose( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - string memory description - ) public override(Governor, IGovernor) returns (uint256) { - return super.propose(targets, values, calldatas, description); - } - - function _execute( - uint256 proposalId, - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 descriptionHash - ) internal override(Governor, GovernorTimelockControl) { - super._execute(proposalId, targets, values, calldatas, descriptionHash); - } - - function _cancel( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 descriptionHash - ) internal override(Governor, GovernorTimelockControl) returns (uint256) { - return super._cancel(targets, values, calldatas, descriptionHash); - } - - function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) { - return super._executor(); - } - - function supportsInterface( - bytes4 interfaceId - ) public view override(Governor, GovernorTimelockControl) returns (bool) { - return super.supportsInterface(interfaceId); - } -} diff --git a/contracts/mocks/wizard/MyGovernor2.sol b/contracts/mocks/wizard/MyGovernor2.sol deleted file mode 100644 index 1472b67d5..000000000 --- a/contracts/mocks/wizard/MyGovernor2.sol +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "../../governance/Governor.sol"; -import "../../governance/extensions/GovernorProposalThreshold.sol"; -import "../../governance/extensions/GovernorCountingSimple.sol"; -import "../../governance/extensions/GovernorVotes.sol"; -import "../../governance/extensions/GovernorVotesQuorumFraction.sol"; -import "../../governance/extensions/GovernorTimelockControl.sol"; - -contract MyGovernor2 is - Governor, - GovernorTimelockControl, - GovernorProposalThreshold, - GovernorVotes, - GovernorVotesQuorumFraction, - GovernorCountingSimple -{ - constructor( - IVotes _token, - TimelockController _timelock - ) Governor("MyGovernor") GovernorVotes(_token) GovernorVotesQuorumFraction(4) GovernorTimelockControl(_timelock) {} - - function votingDelay() public pure override returns (uint256) { - return 1; // 1 block - } - - function votingPeriod() public pure override returns (uint256) { - return 45818; // 1 week - } - - function proposalThreshold() public pure override returns (uint256) { - return 1000e18; - } - - // The following functions are overrides required by Solidity. - - function quorum( - uint256 blockNumber - ) public view override(IGovernor, GovernorVotesQuorumFraction) returns (uint256) { - return super.quorum(blockNumber); - } - - function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) { - return super.state(proposalId); - } - - function propose( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - string memory description - ) public override(Governor, GovernorProposalThreshold, IGovernor) returns (uint256) { - return super.propose(targets, values, calldatas, description); - } - - function _execute( - uint256 proposalId, - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 descriptionHash - ) internal override(Governor, GovernorTimelockControl) { - super._execute(proposalId, targets, values, calldatas, descriptionHash); - } - - function _cancel( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 descriptionHash - ) internal override(Governor, GovernorTimelockControl) returns (uint256) { - return super._cancel(targets, values, calldatas, descriptionHash); - } - - function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) { - return super._executor(); - } - - function supportsInterface( - bytes4 interfaceId - ) public view override(Governor, GovernorTimelockControl) returns (bool) { - return super.supportsInterface(interfaceId); - } -} diff --git a/contracts/mocks/wizard/MyGovernor3.sol b/contracts/mocks/wizard/MyGovernor3.sol deleted file mode 100644 index f4d295156..000000000 --- a/contracts/mocks/wizard/MyGovernor3.sol +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "../../governance/Governor.sol"; -import "../../governance/compatibility/GovernorCompatibilityBravo.sol"; -import "../../governance/extensions/GovernorVotes.sol"; -import "../../governance/extensions/GovernorVotesQuorumFraction.sol"; -import "../../governance/extensions/GovernorTimelockControl.sol"; - -contract MyGovernor is - Governor, - GovernorTimelockControl, - GovernorCompatibilityBravo, - GovernorVotes, - GovernorVotesQuorumFraction -{ - constructor( - IVotes _token, - TimelockController _timelock - ) Governor("MyGovernor") GovernorVotes(_token) GovernorVotesQuorumFraction(4) GovernorTimelockControl(_timelock) {} - - function votingDelay() public pure override returns (uint256) { - return 1; // 1 block - } - - function votingPeriod() public pure override returns (uint256) { - return 45818; // 1 week - } - - function proposalThreshold() public pure override returns (uint256) { - return 1000e18; - } - - // The following functions are overrides required by Solidity. - - function quorum( - uint256 blockNumber - ) public view override(IGovernor, GovernorVotesQuorumFraction) returns (uint256) { - return super.quorum(blockNumber); - } - - function state( - uint256 proposalId - ) public view override(Governor, IGovernor, GovernorTimelockControl) returns (ProposalState) { - return super.state(proposalId); - } - - function propose( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - string memory description - ) public override(Governor, GovernorCompatibilityBravo, IGovernor) returns (uint256) { - return super.propose(targets, values, calldatas, description); - } - - function cancel( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 descriptionHash - ) public override(Governor, GovernorCompatibilityBravo, IGovernor) returns (uint256) { - return super.cancel(targets, values, calldatas, descriptionHash); - } - - function _execute( - uint256 proposalId, - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 descriptionHash - ) internal override(Governor, GovernorTimelockControl) { - super._execute(proposalId, targets, values, calldatas, descriptionHash); - } - - function _cancel( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 descriptionHash - ) internal override(Governor, GovernorTimelockControl) returns (uint256) { - return super._cancel(targets, values, calldatas, descriptionHash); - } - - function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) { - return super._executor(); - } - - function supportsInterface( - bytes4 interfaceId - ) public view override(Governor, IERC165, GovernorTimelockControl) returns (bool) { - return super.supportsInterface(interfaceId); - } -} diff --git a/contracts/proxy/beacon/BeaconProxy.sol b/contracts/proxy/beacon/BeaconProxy.sol index d217b15ea..a278884f4 100644 --- a/contracts/proxy/beacon/BeaconProxy.sol +++ b/contracts/proxy/beacon/BeaconProxy.sol @@ -31,31 +31,10 @@ contract BeaconProxy is Proxy, ERC1967Upgrade { _upgradeBeaconToAndCall(beacon, data, false); } - /** - * @dev Returns the current beacon address. - */ - function _beacon() internal view virtual returns (address) { - return _getBeacon(); - } - /** * @dev Returns the current implementation address of the associated beacon. */ function _implementation() internal view virtual override returns (address) { return IBeacon(_getBeacon()).implementation(); } - - /** - * @dev Changes the proxy to use a new beacon. Deprecated: see {_upgradeBeaconToAndCall}. - * - * If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon. - * - * Requirements: - * - * - `beacon` must be a contract. - * - The implementation returned by `beacon` must be a contract. - */ - function _setBeacon(address beacon, bytes memory data) internal virtual { - _upgradeBeaconToAndCall(beacon, data, false); - } } diff --git a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol index fbef40619..7bdef7c28 100644 --- a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol @@ -60,20 +60,6 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { _changeAdmin(admin_); } - /** - * @dev Modifier used internally that will delegate the call to the implementation unless the sender is the admin. - * - * CAUTION: This modifier is deprecated, as it could cause issues if the modified function has arguments, and the - * implementation provides a function with the same selector. - */ - modifier ifAdmin() { - if (msg.sender == _getAdmin()) { - _; - } else { - _fallback(); - } - } - /** * @dev If caller is the admin process the call internally, otherwise transparently fallback to the proxy behavior */ @@ -137,17 +123,8 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { } /** - * @dev Returns the current admin. - * - * CAUTION: This function is deprecated. Use {ERC1967Upgrade-_getAdmin} instead. - */ - function _admin() internal view virtual returns (address) { - return _getAdmin(); - } - - /** - * @dev To keep this contract fully transparent, all `ifAdmin` functions must be payable. This helper is here to - * emulate some proxy functions being non-payable while still allowing value to pass through. + * @dev To keep this contract fully transparent, the fallback is payable. This helper is here to enforce + * non-payability of function implemented through dispatchers while still allowing value to pass through. */ function _requireZeroValue() private { require(msg.value == 0); diff --git a/contracts/security/PullPayment.sol b/contracts/security/PullPayment.sol deleted file mode 100644 index 65b4980f6..000000000 --- a/contracts/security/PullPayment.sol +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (security/PullPayment.sol) - -pragma solidity ^0.8.0; - -import "../utils/escrow/Escrow.sol"; - -/** - * @dev Simple implementation of a - * https://consensys.github.io/smart-contract-best-practices/development-recommendations/general/external-calls/#favor-pull-over-push-for-external-calls[pull-payment] - * strategy, where the paying contract doesn't interact directly with the - * receiver account, which must withdraw its payments itself. - * - * Pull-payments are often considered the best practice when it comes to sending - * Ether, security-wise. It prevents recipients from blocking execution, and - * eliminates reentrancy concerns. - * - * TIP: If you would like to learn more about reentrancy and alternative ways - * to protect against it, check out our blog post - * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. - * - * To use, derive from the `PullPayment` contract, and use {_asyncTransfer} - * instead of Solidity's `transfer` function. Payees can query their due - * payments with {payments}, and retrieve them with {withdrawPayments}. - */ -abstract contract PullPayment { - Escrow private immutable _escrow; - - constructor() { - _escrow = new Escrow(); - } - - /** - * @dev Withdraw accumulated payments, forwarding all gas to the recipient. - * - * Note that _any_ account can call this function, not just the `payee`. - * This means that contracts unaware of the `PullPayment` protocol can still - * receive funds this way, by having a separate account call - * {withdrawPayments}. - * - * WARNING: Forwarding all gas opens the door to reentrancy vulnerabilities. - * Make sure you trust the recipient, or are either following the - * checks-effects-interactions pattern or using {ReentrancyGuard}. - * - * @param payee Whose payments will be withdrawn. - * - * Causes the `escrow` to emit a {Withdrawn} event. - */ - function withdrawPayments(address payable payee) public virtual { - _escrow.withdraw(payee); - } - - /** - * @dev Returns the payments owed to an address. - * @param dest The creditor's address. - */ - function payments(address dest) public view returns (uint256) { - return _escrow.depositsOf(dest); - } - - /** - * @dev Called by the payer to store the sent amount as credit to be pulled. - * Funds sent in this way are stored in an intermediate {Escrow} contract, so - * there is no danger of them being spent before withdrawal. - * - * @param dest The destination address of the funds. - * @param amount The amount to transfer. - * - * Causes the `escrow` to emit a {Deposited} event. - */ - function _asyncTransfer(address dest, uint256 amount) internal virtual { - _escrow.deposit{value: amount}(dest); - } -} diff --git a/contracts/security/README.adoc b/contracts/security/README.adoc index 66f398fec..7f4799eb8 100644 --- a/contracts/security/README.adoc +++ b/contracts/security/README.adoc @@ -5,7 +5,6 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/ These contracts aim to cover common security practices. -* {PullPayment}: A pattern that can be used to avoid reentrancy attacks. * {ReentrancyGuard}: A modifier that can prevent reentrancy during certain functions. * {Pausable}: A common emergency response mechanism that can pause functionality while a remediation is pending. @@ -13,8 +12,6 @@ TIP: For an overview on reentrancy and the possible mechanisms to prevent it, re == Contracts -{{PullPayment}} - {{ReentrancyGuard}} {{Pausable}} diff --git a/contracts/token/ERC20/extensions/ERC20Permit.sol b/contracts/token/ERC20/extensions/ERC20Permit.sol index 70b48a8d4..8f476b662 100644 --- a/contracts/token/ERC20/extensions/ERC20Permit.sol +++ b/contracts/token/ERC20/extensions/ERC20Permit.sol @@ -23,14 +23,6 @@ abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { // solhint-disable-next-line var-name-mixedcase bytes32 private constant _PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - /** - * @dev In previous versions `_PERMIT_TYPEHASH` was declared as `immutable`. - * However, to ensure consistency with the upgradeable transpiler, we will continue - * to reserve a slot. - * @custom:oz-renamed-from _PERMIT_TYPEHASH - */ - // solhint-disable-next-line var-name-mixedcase - bytes32 private _PERMIT_TYPEHASH_DEPRECATED_SLOT; /** * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. diff --git a/contracts/token/ERC20/extensions/draft-ERC20Permit.sol b/contracts/token/ERC20/extensions/draft-ERC20Permit.sol deleted file mode 100644 index 6579ef33f..000000000 --- a/contracts/token/ERC20/extensions/draft-ERC20Permit.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/extensions/draft-ERC20Permit.sol) - -pragma solidity ^0.8.0; - -// EIP-2612 is Final as of 2022-11-01. This file is deprecated. - -import "./ERC20Permit.sol"; diff --git a/contracts/token/ERC20/extensions/draft-IERC20Permit.sol b/contracts/token/ERC20/extensions/draft-IERC20Permit.sol deleted file mode 100644 index 1df6c537d..000000000 --- a/contracts/token/ERC20/extensions/draft-IERC20Permit.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -// EIP-2612 is Final as of 2022-11-01. This file is deprecated. - -import "./IERC20Permit.sol"; diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index 805a1565f..5eb25932e 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -35,24 +35,6 @@ library SafeERC20 { _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); } - /** - * @dev Deprecated. This function has issues similar to the ones found in - * {IERC20-approve}, and its usage is discouraged. - * - * Whenever possible, use {safeIncreaseAllowance} and - * {safeDecreaseAllowance} instead. - */ - function safeApprove(IERC20 token, address spender, uint256 value) internal { - // safeApprove should only be called when setting an initial allowance, - // or when resetting it to zero. To increase and decrease it, use - // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' - require( - (value == 0) || (token.allowance(address(this), spender) == 0), - "SafeERC20: approve from non-zero to non-zero allowance" - ); - _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); - } - /** * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value, * non-reverting calls are assumed to be successful. diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol deleted file mode 100644 index c6aa7c564..000000000 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/extensions/draft-ERC721Votes.sol) - -pragma solidity ^0.8.0; - -// ERC721Votes was marked as draft due to the EIP-712 dependency. -// EIP-712 is Final as of 2022-08-11. This file is deprecated. - -import "./ERC721Votes.sol"; diff --git a/contracts/token/ERC777/ERC777.sol b/contracts/token/ERC777/ERC777.sol deleted file mode 100644 index 063337d7e..000000000 --- a/contracts/token/ERC777/ERC777.sol +++ /dev/null @@ -1,517 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC777/ERC777.sol) - -pragma solidity ^0.8.1; - -import "./IERC777.sol"; -import "./IERC777Recipient.sol"; -import "./IERC777Sender.sol"; -import "../ERC20/IERC20.sol"; -import "../../utils/Address.sol"; -import "../../utils/Context.sol"; -import "../../utils/introspection/IERC1820Registry.sol"; - -/** - * @dev Implementation of the {IERC777} interface. - * - * This implementation is agnostic to the way tokens are created. This means - * that a supply mechanism has to be added in a derived contract using {_mint}. - * - * Support for ERC20 is included in this contract, as specified by the EIP: both - * the ERC777 and ERC20 interfaces can be safely used when interacting with it. - * Both {IERC777-Sent} and {IERC20-Transfer} events are emitted on token - * movements. - * - * Additionally, the {IERC777-granularity} value is hard-coded to `1`, meaning that there - * are no special restrictions in the amount of tokens that created, moved, or - * destroyed. This makes integration with ERC20 applications seamless. - * - * CAUTION: This file is deprecated as of v4.9 and will be removed in the next major release. - */ -contract ERC777 is Context, IERC777, IERC20 { - using Address for address; - - IERC1820Registry internal constant _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24); - - mapping(address => uint256) private _balances; - - uint256 private _totalSupply; - - string private _name; - string private _symbol; - - bytes32 private constant _TOKENS_SENDER_INTERFACE_HASH = keccak256("ERC777TokensSender"); - bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient"); - - // This isn't ever read from - it's only used to respond to the defaultOperators query. - address[] private _defaultOperatorsArray; - - // Immutable, but accounts may revoke them (tracked in __revokedDefaultOperators). - mapping(address => bool) private _defaultOperators; - - // For each account, a mapping of its operators and revoked default operators. - mapping(address => mapping(address => bool)) private _operators; - mapping(address => mapping(address => bool)) private _revokedDefaultOperators; - - // ERC20-allowances - mapping(address => mapping(address => uint256)) private _allowances; - - /** - * @dev `defaultOperators` may be an empty array. - */ - constructor(string memory name_, string memory symbol_, address[] memory defaultOperators_) { - _name = name_; - _symbol = symbol_; - - _defaultOperatorsArray = defaultOperators_; - for (uint256 i = 0; i < defaultOperators_.length; i++) { - _defaultOperators[defaultOperators_[i]] = true; - } - - // register interfaces - _ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC777Token"), address(this)); - _ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC20Token"), address(this)); - } - - /** - * @dev See {IERC777-name}. - */ - function name() public view virtual override returns (string memory) { - return _name; - } - - /** - * @dev See {IERC777-symbol}. - */ - function symbol() public view virtual override returns (string memory) { - return _symbol; - } - - /** - * @dev See {ERC20-decimals}. - * - * Always returns 18, as per the - * [ERC777 EIP](https://eips.ethereum.org/EIPS/eip-777#backward-compatibility). - */ - function decimals() public pure virtual returns (uint8) { - return 18; - } - - /** - * @dev See {IERC777-granularity}. - * - * This implementation always returns `1`. - */ - function granularity() public view virtual override returns (uint256) { - return 1; - } - - /** - * @dev See {IERC777-totalSupply}. - */ - function totalSupply() public view virtual override(IERC20, IERC777) returns (uint256) { - return _totalSupply; - } - - /** - * @dev Returns the amount of tokens owned by an account (`tokenHolder`). - */ - function balanceOf(address tokenHolder) public view virtual override(IERC20, IERC777) returns (uint256) { - return _balances[tokenHolder]; - } - - /** - * @dev See {IERC777-send}. - * - * Also emits a {IERC20-Transfer} event for ERC20 compatibility. - */ - function send(address recipient, uint256 amount, bytes memory data) public virtual override { - _send(_msgSender(), recipient, amount, data, "", true); - } - - /** - * @dev See {IERC20-transfer}. - * - * Unlike `send`, `recipient` is _not_ required to implement the {IERC777Recipient} - * interface if it is a contract. - * - * Also emits a {Sent} event. - */ - function transfer(address recipient, uint256 amount) public virtual override returns (bool) { - _send(_msgSender(), recipient, amount, "", "", false); - return true; - } - - /** - * @dev See {IERC777-burn}. - * - * Also emits a {IERC20-Transfer} event for ERC20 compatibility. - */ - function burn(uint256 amount, bytes memory data) public virtual override { - _burn(_msgSender(), amount, data, ""); - } - - /** - * @dev See {IERC777-isOperatorFor}. - */ - function isOperatorFor(address operator, address tokenHolder) public view virtual override returns (bool) { - return - operator == tokenHolder || - (_defaultOperators[operator] && !_revokedDefaultOperators[tokenHolder][operator]) || - _operators[tokenHolder][operator]; - } - - /** - * @dev See {IERC777-authorizeOperator}. - */ - function authorizeOperator(address operator) public virtual override { - require(_msgSender() != operator, "ERC777: authorizing self as operator"); - - if (_defaultOperators[operator]) { - delete _revokedDefaultOperators[_msgSender()][operator]; - } else { - _operators[_msgSender()][operator] = true; - } - - emit AuthorizedOperator(operator, _msgSender()); - } - - /** - * @dev See {IERC777-revokeOperator}. - */ - function revokeOperator(address operator) public virtual override { - require(operator != _msgSender(), "ERC777: revoking self as operator"); - - if (_defaultOperators[operator]) { - _revokedDefaultOperators[_msgSender()][operator] = true; - } else { - delete _operators[_msgSender()][operator]; - } - - emit RevokedOperator(operator, _msgSender()); - } - - /** - * @dev See {IERC777-defaultOperators}. - */ - function defaultOperators() public view virtual override returns (address[] memory) { - return _defaultOperatorsArray; - } - - /** - * @dev See {IERC777-operatorSend}. - * - * Emits {Sent} and {IERC20-Transfer} events. - */ - function operatorSend( - address sender, - address recipient, - uint256 amount, - bytes memory data, - bytes memory operatorData - ) public virtual override { - require(isOperatorFor(_msgSender(), sender), "ERC777: caller is not an operator for holder"); - _send(sender, recipient, amount, data, operatorData, true); - } - - /** - * @dev See {IERC777-operatorBurn}. - * - * Emits {Burned} and {IERC20-Transfer} events. - */ - function operatorBurn( - address account, - uint256 amount, - bytes memory data, - bytes memory operatorData - ) public virtual override { - require(isOperatorFor(_msgSender(), account), "ERC777: caller is not an operator for holder"); - _burn(account, amount, data, operatorData); - } - - /** - * @dev See {IERC20-allowance}. - * - * Note that operator and allowance concepts are orthogonal: operators may - * not have allowance, and accounts with allowance may not be operators - * themselves. - */ - function allowance(address holder, address spender) public view virtual override returns (uint256) { - return _allowances[holder][spender]; - } - - /** - * @dev See {IERC20-approve}. - * - * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on - * `transferFrom`. This is semantically equivalent to an infinite approval. - * - * Note that accounts cannot have allowance issued by their operators. - */ - function approve(address spender, uint256 value) public virtual override returns (bool) { - address holder = _msgSender(); - _approve(holder, spender, value); - return true; - } - - /** - * @dev See {IERC20-transferFrom}. - * - * NOTE: Does not update the allowance if the current allowance - * is the maximum `uint256`. - * - * Note that operator and allowance concepts are orthogonal: operators cannot - * call `transferFrom` (unless they have allowance), and accounts with - * allowance cannot call `operatorSend` (unless they are operators). - * - * Emits {Sent}, {IERC20-Transfer} and {IERC20-Approval} events. - */ - function transferFrom(address holder, address recipient, uint256 amount) public virtual override returns (bool) { - address spender = _msgSender(); - _spendAllowance(holder, spender, amount); - _send(holder, recipient, amount, "", "", false); - return true; - } - - /** - * @dev Creates `amount` tokens and assigns them to `account`, increasing - * the total supply. - * - * If a send hook is registered for `account`, the corresponding function - * will be called with the caller address as the `operator` and with - * `userData` and `operatorData`. - * - * See {IERC777Sender} and {IERC777Recipient}. - * - * Emits {Minted} and {IERC20-Transfer} events. - * - * Requirements - * - * - `account` cannot be the zero address. - * - if `account` is a contract, it must implement the {IERC777Recipient} - * interface. - */ - function _mint(address account, uint256 amount, bytes memory userData, bytes memory operatorData) internal virtual { - _mint(account, amount, userData, operatorData, true); - } - - /** - * @dev Creates `amount` tokens and assigns them to `account`, increasing - * the total supply. - * - * If `requireReceptionAck` is set to true, and if a send hook is - * registered for `account`, the corresponding function will be called with - * `operator`, `data` and `operatorData`. - * - * See {IERC777Sender} and {IERC777Recipient}. - * - * Emits {Minted} and {IERC20-Transfer} events. - * - * Requirements - * - * - `account` cannot be the zero address. - * - if `account` is a contract, it must implement the {IERC777Recipient} - * interface. - */ - function _mint( - address account, - uint256 amount, - bytes memory userData, - bytes memory operatorData, - bool requireReceptionAck - ) internal virtual { - require(account != address(0), "ERC777: mint to the zero address"); - - address operator = _msgSender(); - - _beforeTokenTransfer(operator, address(0), account, amount); - - // Update state variables - _totalSupply += amount; - _balances[account] += amount; - - _callTokensReceived(operator, address(0), account, amount, userData, operatorData, requireReceptionAck); - - emit Minted(operator, account, amount, userData, operatorData); - emit Transfer(address(0), account, amount); - } - - /** - * @dev Send tokens - * @param from address token holder address - * @param to address recipient address - * @param amount uint256 amount of tokens to transfer - * @param userData bytes extra information provided by the token holder (if any) - * @param operatorData bytes extra information provided by the operator (if any) - * @param requireReceptionAck if true, contract recipients are required to implement ERC777TokensRecipient - */ - function _send( - address from, - address to, - uint256 amount, - bytes memory userData, - bytes memory operatorData, - bool requireReceptionAck - ) internal virtual { - require(from != address(0), "ERC777: transfer from the zero address"); - require(to != address(0), "ERC777: transfer to the zero address"); - - address operator = _msgSender(); - - _callTokensToSend(operator, from, to, amount, userData, operatorData); - - _move(operator, from, to, amount, userData, operatorData); - - _callTokensReceived(operator, from, to, amount, userData, operatorData, requireReceptionAck); - } - - /** - * @dev Burn tokens - * @param from address token holder address - * @param amount uint256 amount of tokens to burn - * @param data bytes extra information provided by the token holder - * @param operatorData bytes extra information provided by the operator (if any) - */ - function _burn(address from, uint256 amount, bytes memory data, bytes memory operatorData) internal virtual { - require(from != address(0), "ERC777: burn from the zero address"); - - address operator = _msgSender(); - - _callTokensToSend(operator, from, address(0), amount, data, operatorData); - - _beforeTokenTransfer(operator, from, address(0), amount); - - // Update state variables - uint256 fromBalance = _balances[from]; - require(fromBalance >= amount, "ERC777: burn amount exceeds balance"); - unchecked { - _balances[from] = fromBalance - amount; - } - _totalSupply -= amount; - - emit Burned(operator, from, amount, data, operatorData); - emit Transfer(from, address(0), amount); - } - - function _move( - address operator, - address from, - address to, - uint256 amount, - bytes memory userData, - bytes memory operatorData - ) private { - _beforeTokenTransfer(operator, from, to, amount); - - uint256 fromBalance = _balances[from]; - require(fromBalance >= amount, "ERC777: transfer amount exceeds balance"); - unchecked { - _balances[from] = fromBalance - amount; - } - _balances[to] += amount; - - emit Sent(operator, from, to, amount, userData, operatorData); - emit Transfer(from, to, amount); - } - - /** - * @dev See {ERC20-_approve}. - * - * Note that accounts cannot have allowance issued by their operators. - */ - function _approve(address holder, address spender, uint256 value) internal virtual { - require(holder != address(0), "ERC777: approve from the zero address"); - require(spender != address(0), "ERC777: approve to the zero address"); - - _allowances[holder][spender] = value; - emit Approval(holder, spender, value); - } - - /** - * @dev Call from.tokensToSend() if the interface is registered - * @param operator address operator requesting the transfer - * @param from address token holder address - * @param to address recipient address - * @param amount uint256 amount of tokens to transfer - * @param userData bytes extra information provided by the token holder (if any) - * @param operatorData bytes extra information provided by the operator (if any) - */ - function _callTokensToSend( - address operator, - address from, - address to, - uint256 amount, - bytes memory userData, - bytes memory operatorData - ) private { - address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(from, _TOKENS_SENDER_INTERFACE_HASH); - if (implementer != address(0)) { - IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData); - } - } - - /** - * @dev Call to.tokensReceived() if the interface is registered. Reverts if the recipient is a contract but - * tokensReceived() was not registered for the recipient - * @param operator address operator requesting the transfer - * @param from address token holder address - * @param to address recipient address - * @param amount uint256 amount of tokens to transfer - * @param userData bytes extra information provided by the token holder (if any) - * @param operatorData bytes extra information provided by the operator (if any) - * @param requireReceptionAck if true, contract recipients are required to implement ERC777TokensRecipient - */ - function _callTokensReceived( - address operator, - address from, - address to, - uint256 amount, - bytes memory userData, - bytes memory operatorData, - bool requireReceptionAck - ) private { - address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(to, _TOKENS_RECIPIENT_INTERFACE_HASH); - if (implementer != address(0)) { - IERC777Recipient(implementer).tokensReceived(operator, from, to, amount, userData, operatorData); - } else if (requireReceptionAck) { - require( - to.code.length == 0, - "ERC777: token recipient contract has no implementer for ERC777TokensRecipient" - ); - } - } - - /** - * @dev Updates `owner` s allowance for `spender` based on spent `amount`. - * - * Does not update the allowance amount in case of infinite allowance. - * Revert if not enough allowance is available. - * - * Might emit an {IERC20-Approval} event. - */ - function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { - uint256 currentAllowance = allowance(owner, spender); - if (currentAllowance != type(uint256).max) { - require(currentAllowance >= amount, "ERC777: insufficient allowance"); - unchecked { - _approve(owner, spender, currentAllowance - amount); - } - } - } - - /** - * @dev Hook that is called before any token transfer. This includes - * calls to {send}, {transfer}, {operatorSend}, {transferFrom}, minting and burning. - * - * Calling conditions: - * - * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens - * will be to transferred to `to`. - * - when `from` is zero, `amount` tokens will be minted for `to`. - * - when `to` is zero, `amount` of ``from``'s tokens will be burned. - * - `from` and `to` are never both zero. - * - * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. - */ - function _beforeTokenTransfer(address operator, address from, address to, uint256 amount) internal virtual {} -} diff --git a/contracts/token/ERC777/IERC777.sol b/contracts/token/ERC777/IERC777.sol deleted file mode 100644 index d3bede626..000000000 --- a/contracts/token/ERC777/IERC777.sol +++ /dev/null @@ -1,200 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC777/IERC777.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC777Token standard as defined in the EIP. - * - * This contract uses the - * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 registry standard] to let - * token holders and recipients react to token movements by using setting implementers - * for the associated interfaces in said registry. See {IERC1820Registry} and - * {ERC1820Implementer}. - */ -interface IERC777 { - /** - * @dev Emitted when `amount` tokens are created by `operator` and assigned to `to`. - * - * Note that some additional user `data` and `operatorData` can be logged in the event. - */ - event Minted(address indexed operator, address indexed to, uint256 amount, bytes data, bytes operatorData); - - /** - * @dev Emitted when `operator` destroys `amount` tokens from `account`. - * - * Note that some additional user `data` and `operatorData` can be logged in the event. - */ - event Burned(address indexed operator, address indexed from, uint256 amount, bytes data, bytes operatorData); - - /** - * @dev Emitted when `operator` is made operator for `tokenHolder`. - */ - event AuthorizedOperator(address indexed operator, address indexed tokenHolder); - - /** - * @dev Emitted when `operator` is revoked its operator status for `tokenHolder`. - */ - event RevokedOperator(address indexed operator, address indexed tokenHolder); - - /** - * @dev Returns the name of the token. - */ - function name() external view returns (string memory); - - /** - * @dev Returns the symbol of the token, usually a shorter version of the - * name. - */ - function symbol() external view returns (string memory); - - /** - * @dev Returns the smallest part of the token that is not divisible. This - * means all token operations (creation, movement and destruction) must have - * amounts that are a multiple of this number. - * - * For most token contracts, this value will equal 1. - */ - function granularity() external view returns (uint256); - - /** - * @dev Returns the amount of tokens in existence. - */ - function totalSupply() external view returns (uint256); - - /** - * @dev Returns the amount of tokens owned by an account (`owner`). - */ - function balanceOf(address owner) external view returns (uint256); - - /** - * @dev Moves `amount` tokens from the caller's account to `recipient`. - * - * If send or receive hooks are registered for the caller and `recipient`, - * the corresponding functions will be called with `data` and empty - * `operatorData`. See {IERC777Sender} and {IERC777Recipient}. - * - * Emits a {Sent} event. - * - * Requirements - * - * - the caller must have at least `amount` tokens. - * - `recipient` cannot be the zero address. - * - if `recipient` is a contract, it must implement the {IERC777Recipient} - * interface. - */ - function send(address recipient, uint256 amount, bytes calldata data) external; - - /** - * @dev Destroys `amount` tokens from the caller's account, reducing the - * total supply. - * - * If a send hook is registered for the caller, the corresponding function - * will be called with `data` and empty `operatorData`. See {IERC777Sender}. - * - * Emits a {Burned} event. - * - * Requirements - * - * - the caller must have at least `amount` tokens. - */ - function burn(uint256 amount, bytes calldata data) external; - - /** - * @dev Returns true if an account is an operator of `tokenHolder`. - * Operators can send and burn tokens on behalf of their owners. All - * accounts are their own operator. - * - * See {operatorSend} and {operatorBurn}. - */ - function isOperatorFor(address operator, address tokenHolder) external view returns (bool); - - /** - * @dev Make an account an operator of the caller. - * - * See {isOperatorFor}. - * - * Emits an {AuthorizedOperator} event. - * - * Requirements - * - * - `operator` cannot be calling address. - */ - function authorizeOperator(address operator) external; - - /** - * @dev Revoke an account's operator status for the caller. - * - * See {isOperatorFor} and {defaultOperators}. - * - * Emits a {RevokedOperator} event. - * - * Requirements - * - * - `operator` cannot be calling address. - */ - function revokeOperator(address operator) external; - - /** - * @dev Returns the list of default operators. These accounts are operators - * for all token holders, even if {authorizeOperator} was never called on - * them. - * - * This list is immutable, but individual holders may revoke these via - * {revokeOperator}, in which case {isOperatorFor} will return false. - */ - function defaultOperators() external view returns (address[] memory); - - /** - * @dev Moves `amount` tokens from `sender` to `recipient`. The caller must - * be an operator of `sender`. - * - * If send or receive hooks are registered for `sender` and `recipient`, - * the corresponding functions will be called with `data` and - * `operatorData`. See {IERC777Sender} and {IERC777Recipient}. - * - * Emits a {Sent} event. - * - * Requirements - * - * - `sender` cannot be the zero address. - * - `sender` must have at least `amount` tokens. - * - the caller must be an operator for `sender`. - * - `recipient` cannot be the zero address. - * - if `recipient` is a contract, it must implement the {IERC777Recipient} - * interface. - */ - function operatorSend( - address sender, - address recipient, - uint256 amount, - bytes calldata data, - bytes calldata operatorData - ) external; - - /** - * @dev Destroys `amount` tokens from `account`, reducing the total supply. - * The caller must be an operator of `account`. - * - * If a send hook is registered for `account`, the corresponding function - * will be called with `data` and `operatorData`. See {IERC777Sender}. - * - * Emits a {Burned} event. - * - * Requirements - * - * - `account` cannot be the zero address. - * - `account` must have at least `amount` tokens. - * - the caller must be an operator for `account`. - */ - function operatorBurn(address account, uint256 amount, bytes calldata data, bytes calldata operatorData) external; - - event Sent( - address indexed operator, - address indexed from, - address indexed to, - uint256 amount, - bytes data, - bytes operatorData - ); -} diff --git a/contracts/token/ERC777/IERC777Recipient.sol b/contracts/token/ERC777/IERC777Recipient.sol deleted file mode 100644 index 717dd8f8c..000000000 --- a/contracts/token/ERC777/IERC777Recipient.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (token/ERC777/IERC777Recipient.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC777TokensRecipient standard as defined in the EIP. - * - * Accounts can be notified of {IERC777} tokens being sent to them by having a - * contract implement this interface (contract holders can be their own - * implementer) and registering it on the - * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 global registry]. - * - * See {IERC1820Registry} and {ERC1820Implementer}. - */ -interface IERC777Recipient { - /** - * @dev Called by an {IERC777} token contract whenever tokens are being - * moved or created into a registered account (`to`). The type of operation - * is conveyed by `from` being the zero address or not. - * - * This call occurs _after_ the token contract's state is updated, so - * {IERC777-balanceOf}, etc., can be used to query the post-operation state. - * - * This function may revert to prevent the operation from being executed. - */ - function tokensReceived( - address operator, - address from, - address to, - uint256 amount, - bytes calldata userData, - bytes calldata operatorData - ) external; -} diff --git a/contracts/token/ERC777/IERC777Sender.sol b/contracts/token/ERC777/IERC777Sender.sol deleted file mode 100644 index 969e3e367..000000000 --- a/contracts/token/ERC777/IERC777Sender.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (token/ERC777/IERC777Sender.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC777TokensSender standard as defined in the EIP. - * - * {IERC777} Token holders can be notified of operations performed on their - * tokens by having a contract implement this interface (contract holders can be - * their own implementer) and registering it on the - * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 global registry]. - * - * See {IERC1820Registry} and {ERC1820Implementer}. - */ -interface IERC777Sender { - /** - * @dev Called by an {IERC777} token contract whenever a registered holder's - * (`from`) tokens are about to be moved or destroyed. The type of operation - * is conveyed by `to` being the zero address or not. - * - * This call occurs _before_ the token contract's state is updated, so - * {IERC777-balanceOf}, etc., can be used to query the pre-operation state. - * - * This function may revert to prevent the operation from being executed. - */ - function tokensToSend( - address operator, - address from, - address to, - uint256 amount, - bytes calldata userData, - bytes calldata operatorData - ) external; -} diff --git a/contracts/token/ERC777/README.adoc b/contracts/token/ERC777/README.adoc deleted file mode 100644 index cc096f74c..000000000 --- a/contracts/token/ERC777/README.adoc +++ /dev/null @@ -1,26 +0,0 @@ -= ERC 777 - -[.readme-notice] -NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/token/erc777 - -CAUTION: As of v4.9, OpenZeppelin's implementation of ERC-777 is deprecated and will be removed in the next major release. - -This set of interfaces and contracts are all related to the https://eips.ethereum.org/EIPS/eip-777[ERC777 token standard]. - -TIP: For an overview of ERC777 tokens and a walk through on how to create a token contract read our xref:ROOT:erc777.adoc[ERC777 guide]. - -The token behavior itself is implemented in the core contracts: {IERC777}, {ERC777}. - -Additionally there are interfaces used to develop contracts that react to token movements: {IERC777Sender}, {IERC777Recipient}. - -== Core - -{{IERC777}} - -{{ERC777}} - -== Hooks - -{{IERC777Sender}} - -{{IERC777Recipient}} diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index 55ff42fd1..aff542585 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -17,202 +17,6 @@ import "./math/SafeCast.sol"; * _Available since v4.5._ */ library Checkpoints { - struct History { - Checkpoint[] _checkpoints; - } - - struct Checkpoint { - uint32 _blockNumber; - uint224 _value; - } - - /** - * @dev Returns the value at a given block number. If a checkpoint is not available at that block, the closest one - * before it is returned, or zero otherwise. Because the number returned corresponds to that at the end of the - * block, the requested block number must be in the past, excluding the current block. - */ - function getAtBlock(History storage self, uint256 blockNumber) internal view returns (uint256) { - require(blockNumber < block.number, "Checkpoints: block not yet mined"); - uint32 key = SafeCast.toUint32(blockNumber); - - uint256 len = self._checkpoints.length; - uint256 pos = _upperBinaryLookup(self._checkpoints, key, 0, len); - return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; - } - - /** - * @dev Returns the value at a given block number. If a checkpoint is not available at that block, the closest one - * before it is returned, or zero otherwise. Similar to {upperLookup} but optimized for the case when the searched - * checkpoint is probably "recent", defined as being among the last sqrt(N) checkpoints where N is the number of - * checkpoints. - */ - function getAtProbablyRecentBlock(History storage self, uint256 blockNumber) internal view returns (uint256) { - require(blockNumber < block.number, "Checkpoints: block not yet mined"); - uint32 key = SafeCast.toUint32(blockNumber); - - uint256 len = self._checkpoints.length; - - uint256 low = 0; - uint256 high = len; - - if (len > 5) { - uint256 mid = len - Math.sqrt(len); - if (key < _unsafeAccess(self._checkpoints, mid)._blockNumber) { - high = mid; - } else { - low = mid + 1; - } - } - - uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high); - - return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; - } - - /** - * @dev Pushes a value onto a History so that it is stored as the checkpoint for the current block. - * - * Returns previous value and new value. - */ - function push(History storage self, uint256 value) internal returns (uint256, uint256) { - return _insert(self._checkpoints, SafeCast.toUint32(block.number), SafeCast.toUint224(value)); - } - - /** - * @dev Pushes a value onto a History, by updating the latest value using binary operation `op`. The new value will - * be set to `op(latest, delta)`. - * - * Returns previous value and new value. - */ - function push( - History storage self, - function(uint256, uint256) view returns (uint256) op, - uint256 delta - ) internal returns (uint256, uint256) { - return push(self, op(latest(self), delta)); - } - - /** - * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. - */ - function latest(History storage self) internal view returns (uint224) { - uint256 pos = self._checkpoints.length; - return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; - } - - /** - * @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value - * in the most recent checkpoint. - */ - function latestCheckpoint( - History storage self - ) internal view returns (bool exists, uint32 _blockNumber, uint224 _value) { - uint256 pos = self._checkpoints.length; - if (pos == 0) { - return (false, 0, 0); - } else { - Checkpoint memory ckpt = _unsafeAccess(self._checkpoints, pos - 1); - return (true, ckpt._blockNumber, ckpt._value); - } - } - - /** - * @dev Returns the number of checkpoint. - */ - function length(History storage self) internal view returns (uint256) { - return self._checkpoints.length; - } - - /** - * @dev Returns checkpoint at given position. - */ - function at(History storage self, uint32 pos) internal view returns (Checkpoint memory) { - return self._checkpoints[pos]; - } - - /** - * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, - * or by updating the last one. - */ - function _insert(Checkpoint[] storage self, uint32 key, uint224 value) private returns (uint224, uint224) { - uint256 pos = self.length; - - if (pos > 0) { - // Copying to memory is important here. - Checkpoint memory last = _unsafeAccess(self, pos - 1); - - // Checkpoint keys must be non-decreasing. - require(last._blockNumber <= key, "Checkpoint: decreasing keys"); - - // Update or push new checkpoint - if (last._blockNumber == key) { - _unsafeAccess(self, pos - 1)._value = value; - } else { - self.push(Checkpoint({_blockNumber: key, _value: value})); - } - return (last._value, value); - } else { - self.push(Checkpoint({_blockNumber: key, _value: value})); - return (0, value); - } - } - - /** - * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or `high` if there is none. - * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`. - * - * WARNING: `high` should not be greater than the array's length. - */ - function _upperBinaryLookup( - Checkpoint[] storage self, - uint32 key, - uint256 low, - uint256 high - ) private view returns (uint256) { - while (low < high) { - uint256 mid = Math.average(low, high); - if (_unsafeAccess(self, mid)._blockNumber > key) { - high = mid; - } else { - low = mid + 1; - } - } - return high; - } - - /** - * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or `high` if there is none. - * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`. - * - * WARNING: `high` should not be greater than the array's length. - */ - function _lowerBinaryLookup( - Checkpoint[] storage self, - uint32 key, - uint256 low, - uint256 high - ) private view returns (uint256) { - while (low < high) { - uint256 mid = Math.average(low, high); - if (_unsafeAccess(self, mid)._blockNumber < key) { - low = mid + 1; - } else { - high = mid; - } - } - return high; - } - - /** - * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds. - */ - function _unsafeAccess(Checkpoint[] storage self, uint256 pos) private pure returns (Checkpoint storage result) { - assembly { - mstore(0, self.slot) - result.slot := add(keccak256(0, 0x20), pos) - } - } - struct Trace224 { Checkpoint224[] _checkpoints; } diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 994b4b956..07cee6427 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -31,10 +31,6 @@ Finally, {Create2} contains all necessary utilities to safely use the https://bl {{SafeCast}} -{{SafeMath}} - -{{SignedSafeMath}} - == Cryptography {{ECDSA}} @@ -45,39 +41,18 @@ Finally, {Create2} contains all necessary utilities to safely use the https://bl {{EIP712}} -== Escrow - -{{ConditionalEscrow}} - -{{Escrow}} - -{{RefundEscrow}} - == Introspection This set of interfaces and contracts deal with https://en.wikipedia.org/wiki/Type_introspection[type introspection] of contracts, that is, examining which functions can be called on them. This is usually referred to as a contract's _interface_. Ethereum contracts have no native concept of an interface, so applications must usually simply trust they are not making an incorrect call. For trusted setups this is a non-issue, but often unknown and untrusted third-party addresses need to be interacted with. There may even not be any direct calls to them! (e.g. `ERC20` tokens may be sent to a contract that lacks a way to transfer them out of it, locking them forever). In these cases, a contract _declaring_ its interface can be very helpful in preventing errors. -There are two main ways to approach this. - -* Locally, where a contract implements `IERC165` and declares an interface, and a second one queries it directly via `ERC165Checker`. -* Globally, where a global and unique registry (`IERC1820Registry`) is used to register implementers of a certain interface (`IERC1820Implementer`). It is then the registry that is queried, which allows for more complex setups, like contracts implementing interfaces for externally-owned accounts. - -Note that, in all cases, accounts simply _declare_ their interfaces, but they are not required to actually implement them. This mechanism can therefore be used to both prevent errors and allow for complex interactions (see `ERC777`), but it must not be relied on for security. - {{IERC165}} {{ERC165}} {{ERC165Checker}} -{{IERC1820Registry}} - -{{IERC1820Implementer}} - -{{ERC1820Implementer}} - == Data Structures {{BitMaps}} diff --git a/contracts/utils/Timers.sol b/contracts/utils/Timers.sol deleted file mode 100644 index 1c92b029b..000000000 --- a/contracts/utils/Timers.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/Timers.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Tooling for timepoints, timers and delays - * - * CAUTION: This file is deprecated as of 4.9 and will be removed in the next major release. - */ -library Timers { - struct Timestamp { - uint64 _deadline; - } - - function getDeadline(Timestamp memory timer) internal pure returns (uint64) { - return timer._deadline; - } - - function setDeadline(Timestamp storage timer, uint64 timestamp) internal { - timer._deadline = timestamp; - } - - function reset(Timestamp storage timer) internal { - timer._deadline = 0; - } - - function isUnset(Timestamp memory timer) internal pure returns (bool) { - return timer._deadline == 0; - } - - function isStarted(Timestamp memory timer) internal pure returns (bool) { - return timer._deadline > 0; - } - - function isPending(Timestamp memory timer) internal view returns (bool) { - return timer._deadline > block.timestamp; - } - - function isExpired(Timestamp memory timer) internal view returns (bool) { - return isStarted(timer) && timer._deadline <= block.timestamp; - } - - struct BlockNumber { - uint64 _deadline; - } - - function getDeadline(BlockNumber memory timer) internal pure returns (uint64) { - return timer._deadline; - } - - function setDeadline(BlockNumber storage timer, uint64 timestamp) internal { - timer._deadline = timestamp; - } - - function reset(BlockNumber storage timer) internal { - timer._deadline = 0; - } - - function isUnset(BlockNumber memory timer) internal pure returns (bool) { - return timer._deadline == 0; - } - - function isStarted(BlockNumber memory timer) internal pure returns (bool) { - return timer._deadline > 0; - } - - function isPending(BlockNumber memory timer) internal view returns (bool) { - return timer._deadline > block.number; - } - - function isExpired(BlockNumber memory timer) internal view returns (bool) { - return isStarted(timer) && timer._deadline <= block.number; - } -} diff --git a/contracts/utils/cryptography/ECDSA.sol b/contracts/utils/cryptography/ECDSA.sol index 77279eb4f..03d2b0eef 100644 --- a/contracts/utils/cryptography/ECDSA.sol +++ b/contracts/utils/cryptography/ECDSA.sol @@ -16,8 +16,7 @@ library ECDSA { NoError, InvalidSignature, InvalidSignatureLength, - InvalidSignatureS, - InvalidSignatureV // Deprecated in v4.8 + InvalidSignatureS } function _throwError(RecoverError error) private pure { diff --git a/contracts/utils/cryptography/draft-EIP712.sol b/contracts/utils/cryptography/draft-EIP712.sol deleted file mode 100644 index fdae3ba3e..000000000 --- a/contracts/utils/cryptography/draft-EIP712.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/draft-EIP712.sol) - -pragma solidity ^0.8.0; - -// EIP-712 is Final as of 2022-08-11. This file is deprecated. - -import "./EIP712.sol"; diff --git a/contracts/utils/escrow/ConditionalEscrow.sol b/contracts/utils/escrow/ConditionalEscrow.sol deleted file mode 100644 index 87f53815b..000000000 --- a/contracts/utils/escrow/ConditionalEscrow.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/escrow/ConditionalEscrow.sol) - -pragma solidity ^0.8.0; - -import "./Escrow.sol"; - -/** - * @title ConditionalEscrow - * @dev Base abstract escrow to only allow withdrawal if a condition is met. - * @dev Intended usage: See {Escrow}. Same usage guidelines apply here. - */ -abstract contract ConditionalEscrow is Escrow { - /** - * @dev Returns whether an address is allowed to withdraw their funds. To be - * implemented by derived contracts. - * @param payee The destination address of the funds. - */ - function withdrawalAllowed(address payee) public view virtual returns (bool); - - function withdraw(address payable payee) public virtual override { - require(withdrawalAllowed(payee), "ConditionalEscrow: payee is not allowed to withdraw"); - super.withdraw(payee); - } -} diff --git a/contracts/utils/escrow/Escrow.sol b/contracts/utils/escrow/Escrow.sol deleted file mode 100644 index 48dd51ab7..000000000 --- a/contracts/utils/escrow/Escrow.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (utils/escrow/Escrow.sol) - -pragma solidity ^0.8.0; - -import "../../access/Ownable.sol"; -import "../Address.sol"; - -/** - * @title Escrow - * @dev Base escrow contract, holds funds designated for a payee until they - * withdraw them. - * - * Intended usage: This contract (and derived escrow contracts) should be a - * standalone contract, that only interacts with the contract that instantiated - * it. That way, it is guaranteed that all Ether will be handled according to - * the `Escrow` rules, and there is no need to check for payable functions or - * transfers in the inheritance tree. The contract that uses the escrow as its - * payment method should be its owner, and provide public methods redirecting - * to the escrow's deposit and withdraw. - */ -contract Escrow is Ownable { - using Address for address payable; - - event Deposited(address indexed payee, uint256 weiAmount); - event Withdrawn(address indexed payee, uint256 weiAmount); - - mapping(address => uint256) private _deposits; - - function depositsOf(address payee) public view returns (uint256) { - return _deposits[payee]; - } - - /** - * @dev Stores the sent amount as credit to be withdrawn. - * @param payee The destination address of the funds. - * - * Emits a {Deposited} event. - */ - function deposit(address payee) public payable virtual onlyOwner { - uint256 amount = msg.value; - _deposits[payee] += amount; - emit Deposited(payee, amount); - } - - /** - * @dev Withdraw accumulated balance for a payee, forwarding all gas to the - * recipient. - * - * WARNING: Forwarding all gas opens the door to reentrancy vulnerabilities. - * Make sure you trust the recipient, or are either following the - * checks-effects-interactions pattern or using {ReentrancyGuard}. - * - * @param payee The address whose funds will be withdrawn and transferred to. - * - * Emits a {Withdrawn} event. - */ - function withdraw(address payable payee) public virtual onlyOwner { - uint256 payment = _deposits[payee]; - - _deposits[payee] = 0; - - payee.sendValue(payment); - - emit Withdrawn(payee, payment); - } -} diff --git a/contracts/utils/escrow/RefundEscrow.sol b/contracts/utils/escrow/RefundEscrow.sol deleted file mode 100644 index 0e9621fee..000000000 --- a/contracts/utils/escrow/RefundEscrow.sol +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/escrow/RefundEscrow.sol) - -pragma solidity ^0.8.0; - -import "./ConditionalEscrow.sol"; - -/** - * @title RefundEscrow - * @dev Escrow that holds funds for a beneficiary, deposited from multiple - * parties. - * @dev Intended usage: See {Escrow}. Same usage guidelines apply here. - * @dev The owner account (that is, the contract that instantiates this - * contract) may deposit, close the deposit period, and allow for either - * withdrawal by the beneficiary, or refunds to the depositors. All interactions - * with `RefundEscrow` will be made through the owner contract. - */ -contract RefundEscrow is ConditionalEscrow { - using Address for address payable; - - enum State { - Active, - Refunding, - Closed - } - - event RefundsClosed(); - event RefundsEnabled(); - - State private _state; - address payable private immutable _beneficiary; - - /** - * @dev Constructor. - * @param beneficiary_ The beneficiary of the deposits. - */ - constructor(address payable beneficiary_) { - require(beneficiary_ != address(0), "RefundEscrow: beneficiary is the zero address"); - _beneficiary = beneficiary_; - _state = State.Active; - } - - /** - * @return The current state of the escrow. - */ - function state() public view virtual returns (State) { - return _state; - } - - /** - * @return The beneficiary of the escrow. - */ - function beneficiary() public view virtual returns (address payable) { - return _beneficiary; - } - - /** - * @dev Stores funds that may later be refunded. - * @param refundee The address funds will be sent to if a refund occurs. - */ - function deposit(address refundee) public payable virtual override { - require(state() == State.Active, "RefundEscrow: can only deposit while active"); - super.deposit(refundee); - } - - /** - * @dev Allows for the beneficiary to withdraw their funds, rejecting - * further deposits. - */ - function close() public virtual onlyOwner { - require(state() == State.Active, "RefundEscrow: can only close while active"); - _state = State.Closed; - emit RefundsClosed(); - } - - /** - * @dev Allows for refunds to take place, rejecting further deposits. - */ - function enableRefunds() public virtual onlyOwner { - require(state() == State.Active, "RefundEscrow: can only enable refunds while active"); - _state = State.Refunding; - emit RefundsEnabled(); - } - - /** - * @dev Withdraws the beneficiary's funds. - */ - function beneficiaryWithdraw() public virtual { - require(state() == State.Closed, "RefundEscrow: beneficiary can only withdraw while closed"); - beneficiary().sendValue(address(this).balance); - } - - /** - * @dev Returns whether refundees can withdraw their deposits (be refunded). The overridden function receives a - * 'payee' argument, but we ignore it here since the condition is global, not per-payee. - */ - function withdrawalAllowed(address) public view override returns (bool) { - return state() == State.Refunding; - } -} diff --git a/contracts/utils/introspection/ERC1820Implementer.sol b/contracts/utils/introspection/ERC1820Implementer.sol deleted file mode 100644 index cf4b50498..000000000 --- a/contracts/utils/introspection/ERC1820Implementer.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC1820Implementer.sol) - -pragma solidity ^0.8.0; - -import "./IERC1820Implementer.sol"; - -/** - * @dev Implementation of the {IERC1820Implementer} interface. - * - * Contracts may inherit from this and call {_registerInterfaceForAddress} to - * declare their willingness to be implementers. - * {IERC1820Registry-setInterfaceImplementer} should then be called for the - * registration to be complete. - * - * CAUTION: This file is deprecated as of v4.9 and will be removed in the next major release. - */ -contract ERC1820Implementer is IERC1820Implementer { - bytes32 private constant _ERC1820_ACCEPT_MAGIC = keccak256("ERC1820_ACCEPT_MAGIC"); - - mapping(bytes32 => mapping(address => bool)) private _supportedInterfaces; - - /** - * @dev See {IERC1820Implementer-canImplementInterfaceForAddress}. - */ - function canImplementInterfaceForAddress( - bytes32 interfaceHash, - address account - ) public view virtual override returns (bytes32) { - return _supportedInterfaces[interfaceHash][account] ? _ERC1820_ACCEPT_MAGIC : bytes32(0x00); - } - - /** - * @dev Declares the contract as willing to be an implementer of - * `interfaceHash` for `account`. - * - * See {IERC1820Registry-setInterfaceImplementer} and - * {IERC1820Registry-interfaceHash}. - */ - function _registerInterfaceForAddress(bytes32 interfaceHash, address account) internal virtual { - _supportedInterfaces[interfaceHash][account] = true; - } -} diff --git a/contracts/utils/introspection/IERC1820Implementer.sol b/contracts/utils/introspection/IERC1820Implementer.sol deleted file mode 100644 index c4d0b3028..000000000 --- a/contracts/utils/introspection/IERC1820Implementer.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC1820Implementer.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface for an ERC1820 implementer, as defined in the - * https://eips.ethereum.org/EIPS/eip-1820#interface-implementation-erc1820implementerinterface[EIP]. - * Used by contracts that will be registered as implementers in the - * {IERC1820Registry}. - */ -interface IERC1820Implementer { - /** - * @dev Returns a special value (`ERC1820_ACCEPT_MAGIC`) if this contract - * implements `interfaceHash` for `account`. - * - * See {IERC1820Registry-setInterfaceImplementer}. - */ - function canImplementInterfaceForAddress(bytes32 interfaceHash, address account) external view returns (bytes32); -} diff --git a/contracts/utils/introspection/IERC1820Registry.sol b/contracts/utils/introspection/IERC1820Registry.sol deleted file mode 100644 index a146bc2a6..000000000 --- a/contracts/utils/introspection/IERC1820Registry.sol +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/introspection/IERC1820Registry.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the global ERC1820 Registry, as defined in the - * https://eips.ethereum.org/EIPS/eip-1820[EIP]. Accounts may register - * implementers for interfaces in this registry, as well as query support. - * - * Implementers may be shared by multiple accounts, and can also implement more - * than a single interface for each account. Contracts can implement interfaces - * for themselves, but externally-owned accounts (EOA) must delegate this to a - * contract. - * - * {IERC165} interfaces can also be queried via the registry. - * - * For an in-depth explanation and source code analysis, see the EIP text. - */ -interface IERC1820Registry { - event InterfaceImplementerSet(address indexed account, bytes32 indexed interfaceHash, address indexed implementer); - - event ManagerChanged(address indexed account, address indexed newManager); - - /** - * @dev Sets `newManager` as the manager for `account`. A manager of an - * account is able to set interface implementers for it. - * - * By default, each account is its own manager. Passing a value of `0x0` in - * `newManager` will reset the manager to this initial state. - * - * Emits a {ManagerChanged} event. - * - * Requirements: - * - * - the caller must be the current manager for `account`. - */ - function setManager(address account, address newManager) external; - - /** - * @dev Returns the manager for `account`. - * - * See {setManager}. - */ - function getManager(address account) external view returns (address); - - /** - * @dev Sets the `implementer` contract as ``account``'s implementer for - * `interfaceHash`. - * - * `account` being the zero address is an alias for the caller's address. - * The zero address can also be used in `implementer` to remove an old one. - * - * See {interfaceHash} to learn how these are created. - * - * Emits an {InterfaceImplementerSet} event. - * - * Requirements: - * - * - the caller must be the current manager for `account`. - * - `interfaceHash` must not be an {IERC165} interface id (i.e. it must not - * end in 28 zeroes). - * - `implementer` must implement {IERC1820Implementer} and return true when - * queried for support, unless `implementer` is the caller. See - * {IERC1820Implementer-canImplementInterfaceForAddress}. - */ - function setInterfaceImplementer(address account, bytes32 _interfaceHash, address implementer) external; - - /** - * @dev Returns the implementer of `interfaceHash` for `account`. If no such - * implementer is registered, returns the zero address. - * - * If `interfaceHash` is an {IERC165} interface id (i.e. it ends with 28 - * zeroes), `account` will be queried for support of it. - * - * `account` being the zero address is an alias for the caller's address. - */ - function getInterfaceImplementer(address account, bytes32 _interfaceHash) external view returns (address); - - /** - * @dev Returns the interface hash for an `interfaceName`, as defined in the - * corresponding - * https://eips.ethereum.org/EIPS/eip-1820#interface-name[section of the EIP]. - */ - function interfaceHash(string calldata interfaceName) external pure returns (bytes32); - - /** - * @notice Updates the cache with whether the contract implements an ERC165 interface or not. - * @param account Address of the contract for which to update the cache. - * @param interfaceId ERC165 interface for which to update the cache. - */ - function updateERC165Cache(address account, bytes4 interfaceId) external; - - /** - * @notice Checks whether a contract implements an ERC165 interface or not. - * If the result is not cached a direct lookup on the contract address is performed. - * If the result is not cached or the cached value is out-of-date, the cache MUST be updated manually by calling - * {updateERC165Cache} with the contract address. - * @param account Address of the contract to check. - * @param interfaceId ERC165 interface to check. - * @return True if `account` implements `interfaceId`, false otherwise. - */ - function implementsERC165Interface(address account, bytes4 interfaceId) external view returns (bool); - - /** - * @notice Checks whether a contract implements an ERC165 interface or not without using or updating the cache. - * @param account Address of the contract to check. - * @param interfaceId ERC165 interface to check. - * @return True if `account` implements `interfaceId`, false otherwise. - */ - function implementsERC165InterfaceNoCache(address account, bytes4 interfaceId) external view returns (bool); -} diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index f8e7ca0a9..430d43a6d 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -13,6 +13,72 @@ library Math { Zero // Toward zero } + /** + * @dev Returns the addition of two unsigned integers, with an overflow flag. + * + * _Available since v5.0._ + */ + function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + uint256 c = a + b; + if (c < a) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the subtraction of two unsigned integers, with an overflow flag. + * + * _Available since v5.0._ + */ + function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b > a) return (false, 0); + return (true, a - b); + } + } + + /** + * @dev Returns the multiplication of two unsigned integers, with an overflow flag. + * + * _Available since v5.0._ + */ + function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) return (true, 0); + uint256 c = a * b; + if (c / a != b) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the division of two unsigned integers, with a division by zero flag. + * + * _Available since v5.0._ + */ + function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a / b); + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag. + * + * _Available since v5.0._ + */ + function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a % b); + } + } + /** * @dev Returns the largest of two numbers. */ diff --git a/contracts/utils/math/SafeCast.sol b/contracts/utils/math/SafeCast.sol index 435a5f945..0744b3711 100644 --- a/contracts/utils/math/SafeCast.sol +++ b/contracts/utils/math/SafeCast.sol @@ -15,9 +15,6 @@ pragma solidity ^0.8.0; * * Using this library instead of the unchecked operations eliminates an entire * class of bugs, so it's recommended to use it always. - * - * Can be combined with {SafeMath} and {SignedSafeMath} to extend it to smaller types, by performing - * all math on `uint256` and `int256` and then downcasting. */ library SafeCast { /** diff --git a/contracts/utils/math/SafeMath.sol b/contracts/utils/math/SafeMath.sol deleted file mode 100644 index 2f48fb736..000000000 --- a/contracts/utils/math/SafeMath.sol +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (utils/math/SafeMath.sol) - -pragma solidity ^0.8.0; - -// CAUTION -// This version of SafeMath should only be used with Solidity 0.8 or later, -// because it relies on the compiler's built in overflow checks. - -/** - * @dev Wrappers over Solidity's arithmetic operations. - * - * NOTE: `SafeMath` is generally not needed starting with Solidity 0.8, since the compiler - * now has built in overflow checking. - */ -library SafeMath { - /** - * @dev Returns the addition of two unsigned integers, with an overflow flag. - * - * _Available since v3.4._ - */ - function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { - unchecked { - uint256 c = a + b; - if (c < a) return (false, 0); - return (true, c); - } - } - - /** - * @dev Returns the subtraction of two unsigned integers, with an overflow flag. - * - * _Available since v3.4._ - */ - function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { - unchecked { - if (b > a) return (false, 0); - return (true, a - b); - } - } - - /** - * @dev Returns the multiplication of two unsigned integers, with an overflow flag. - * - * _Available since v3.4._ - */ - function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { - unchecked { - // Gas optimization: this is cheaper than requiring 'a' not being zero, but the - // benefit is lost if 'b' is also tested. - // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 - if (a == 0) return (true, 0); - uint256 c = a * b; - if (c / a != b) return (false, 0); - return (true, c); - } - } - - /** - * @dev Returns the division of two unsigned integers, with a division by zero flag. - * - * _Available since v3.4._ - */ - function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { - unchecked { - if (b == 0) return (false, 0); - return (true, a / b); - } - } - - /** - * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag. - * - * _Available since v3.4._ - */ - function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { - unchecked { - if (b == 0) return (false, 0); - return (true, a % b); - } - } - - /** - * @dev Returns the addition of two unsigned integers, reverting on - * overflow. - * - * Counterpart to Solidity's `+` operator. - * - * Requirements: - * - * - Addition cannot overflow. - */ - function add(uint256 a, uint256 b) internal pure returns (uint256) { - return a + b; - } - - /** - * @dev Returns the subtraction of two unsigned integers, reverting on - * overflow (when the result is negative). - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - * - Subtraction cannot overflow. - */ - function sub(uint256 a, uint256 b) internal pure returns (uint256) { - return a - b; - } - - /** - * @dev Returns the multiplication of two unsigned integers, reverting on - * overflow. - * - * Counterpart to Solidity's `*` operator. - * - * Requirements: - * - * - Multiplication cannot overflow. - */ - function mul(uint256 a, uint256 b) internal pure returns (uint256) { - return a * b; - } - - /** - * @dev Returns the integer division of two unsigned integers, reverting on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function div(uint256 a, uint256 b) internal pure returns (uint256) { - return a / b; - } - - /** - * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), - * reverting when dividing by zero. - * - * Counterpart to Solidity's `%` operator. This function uses a `revert` - * opcode (which leaves remaining gas untouched) while Solidity uses an - * invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function mod(uint256 a, uint256 b) internal pure returns (uint256) { - return a % b; - } - - /** - * @dev Returns the subtraction of two unsigned integers, reverting with custom message on - * overflow (when the result is negative). - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {trySub}. - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - * - Subtraction cannot overflow. - */ - function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - unchecked { - require(b <= a, errorMessage); - return a - b; - } - } - - /** - * @dev Returns the integer division of two unsigned integers, reverting with custom message on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. Note: this function uses a - * `revert` opcode (which leaves remaining gas untouched) while Solidity - * uses an invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - unchecked { - require(b > 0, errorMessage); - return a / b; - } - } - - /** - * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), - * reverting with custom message when dividing by zero. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryMod}. - * - * Counterpart to Solidity's `%` operator. This function uses a `revert` - * opcode (which leaves remaining gas untouched) while Solidity uses an - * invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - unchecked { - require(b > 0, errorMessage); - return a % b; - } - } -} diff --git a/contracts/utils/math/SignedSafeMath.sol b/contracts/utils/math/SignedSafeMath.sol deleted file mode 100644 index 6704d4ce2..000000000 --- a/contracts/utils/math/SignedSafeMath.sol +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/math/SignedSafeMath.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Wrappers over Solidity's arithmetic operations. - * - * NOTE: `SignedSafeMath` is no longer needed starting with Solidity 0.8. The compiler - * now has built in overflow checking. - */ -library SignedSafeMath { - /** - * @dev Returns the multiplication of two signed integers, reverting on - * overflow. - * - * Counterpart to Solidity's `*` operator. - * - * Requirements: - * - * - Multiplication cannot overflow. - */ - function mul(int256 a, int256 b) internal pure returns (int256) { - return a * b; - } - - /** - * @dev Returns the integer division of two signed integers. Reverts on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function div(int256 a, int256 b) internal pure returns (int256) { - return a / b; - } - - /** - * @dev Returns the subtraction of two signed integers, reverting on - * overflow. - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - * - Subtraction cannot overflow. - */ - function sub(int256 a, int256 b) internal pure returns (int256) { - return a - b; - } - - /** - * @dev Returns the addition of two signed integers, reverting on - * overflow. - * - * Counterpart to Solidity's `+` operator. - * - * Requirements: - * - * - Addition cannot overflow. - */ - function add(int256 a, int256 b) internal pure returns (int256) { - return a + b; - } -} diff --git a/contracts/utils/structs/EnumerableMap.sol b/contracts/utils/structs/EnumerableMap.sol index fb21f02cf..0320ee9a0 100644 --- a/contracts/utils/structs/EnumerableMap.sol +++ b/contracts/utils/structs/EnumerableMap.sol @@ -140,22 +140,6 @@ library EnumerableMap { return value; } - /** - * @dev Same as {get}, with a custom error message when `key` is not in the map. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryGet}. - */ - function get( - Bytes32ToBytes32Map storage map, - bytes32 key, - string memory errorMessage - ) internal view returns (bytes32) { - bytes32 value = map._values[key]; - require(value != 0 || contains(map, key), errorMessage); - return value; - } - /** * @dev Return the an array containing all the keys * @@ -242,16 +226,6 @@ library EnumerableMap { return uint256(get(map._inner, bytes32(key))); } - /** - * @dev Same as {get}, with a custom error message when `key` is not in the map. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryGet}. - */ - function get(UintToUintMap storage map, uint256 key, string memory errorMessage) internal view returns (uint256) { - return uint256(get(map._inner, bytes32(key), errorMessage)); - } - /** * @dev Return the an array containing all the keys * @@ -346,20 +320,6 @@ library EnumerableMap { return address(uint160(uint256(get(map._inner, bytes32(key))))); } - /** - * @dev Same as {get}, with a custom error message when `key` is not in the map. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryGet}. - */ - function get( - UintToAddressMap storage map, - uint256 key, - string memory errorMessage - ) internal view returns (address) { - return address(uint160(uint256(get(map._inner, bytes32(key), errorMessage)))); - } - /** * @dev Return the an array containing all the keys * @@ -454,20 +414,6 @@ library EnumerableMap { return uint256(get(map._inner, bytes32(uint256(uint160(key))))); } - /** - * @dev Same as {get}, with a custom error message when `key` is not in the map. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryGet}. - */ - function get( - AddressToUintMap storage map, - address key, - string memory errorMessage - ) internal view returns (uint256) { - return uint256(get(map._inner, bytes32(uint256(uint160(key))), errorMessage)); - } - /** * @dev Return the an array containing all the keys * @@ -562,20 +508,6 @@ library EnumerableMap { return uint256(get(map._inner, key)); } - /** - * @dev Same as {get}, with a custom error message when `key` is not in the map. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryGet}. - */ - function get( - Bytes32ToUintMap storage map, - bytes32 key, - string memory errorMessage - ) internal view returns (uint256) { - return uint256(get(map._inner, key, errorMessage)); - } - /** * @dev Return the an array containing all the keys * diff --git a/contracts/vendor/amb/IAMB.sol b/contracts/vendor/amb/IAMB.sol deleted file mode 100644 index 73a2bd24b..000000000 --- a/contracts/vendor/amb/IAMB.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (vendor/amb/IAMB.sol) -pragma solidity ^0.8.0; - -interface IAMB { - event UserRequestForAffirmation(bytes32 indexed messageId, bytes encodedData); - event UserRequestForSignature(bytes32 indexed messageId, bytes encodedData); - event AffirmationCompleted( - address indexed sender, - address indexed executor, - bytes32 indexed messageId, - bool status - ); - event RelayedMessage(address indexed sender, address indexed executor, bytes32 indexed messageId, bool status); - - function messageSender() external view returns (address); - - function maxGasPerTx() external view returns (uint256); - - function transactionHash() external view returns (bytes32); - - function messageId() external view returns (bytes32); - - function messageSourceChainId() external view returns (bytes32); - - function messageCallStatus(bytes32 _messageId) external view returns (bool); - - function failedMessageDataHash(bytes32 _messageId) external view returns (bytes32); - - function failedMessageReceiver(bytes32 _messageId) external view returns (address); - - function failedMessageSender(bytes32 _messageId) external view returns (address); - - function requireToPassMessage(address _contract, bytes calldata _data, uint256 _gas) external returns (bytes32); - - function requireToConfirmMessage(address _contract, bytes calldata _data, uint256 _gas) external returns (bytes32); - - function sourceChainId() external view returns (uint256); - - function destinationChainId() external view returns (uint256); -} diff --git a/contracts/vendor/arbitrum/IArbSys.sol b/contracts/vendor/arbitrum/IArbSys.sol deleted file mode 100644 index 9b79d5c16..000000000 --- a/contracts/vendor/arbitrum/IArbSys.sol +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2021-2022, Offchain Labs, Inc. -// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE -// SPDX-License-Identifier: BUSL-1.1 -// OpenZeppelin Contracts (last updated v4.8.0) (vendor/arbitrum/IArbSys.sol) - -pragma solidity >=0.4.21 <0.9.0; - -/** - * @title System level functionality - * @notice For use by contracts to interact with core L2-specific functionality. - * Precompiled contract that exists in every Arbitrum chain at address(100), 0x0000000000000000000000000000000000000064. - */ -interface IArbSys { - /** - * @notice Get Arbitrum block number (distinct from L1 block number; Arbitrum genesis block has block number 0) - * @return block number as int - */ - function arbBlockNumber() external view returns (uint256); - - /** - * @notice Get Arbitrum block hash (reverts unless currentBlockNum-256 <= arbBlockNum < currentBlockNum) - * @return block hash - */ - function arbBlockHash(uint256 arbBlockNum) external view returns (bytes32); - - /** - * @notice Gets the rollup's unique chain identifier - * @return Chain identifier as int - */ - function arbChainID() external view returns (uint256); - - /** - * @notice Get internal version number identifying an ArbOS build - * @return version number as int - */ - function arbOSVersion() external view returns (uint256); - - /** - * @notice Returns 0 since Nitro has no concept of storage gas - * @return uint 0 - */ - function getStorageGasAvailable() external view returns (uint256); - - /** - * @notice (deprecated) check if current call is top level (meaning it was triggered by an EoA or a L1 contract) - * @dev this call has been deprecated and may be removed in a future release - * @return true if current execution frame is not a call by another L2 contract - */ - function isTopLevelCall() external view returns (bool); - - /** - * @notice map L1 sender contract address to its L2 alias - * @param sender sender address - * @param unused argument no longer used - * @return aliased sender address - */ - function mapL1SenderContractAddressToL2Alias(address sender, address unused) external pure returns (address); - - /** - * @notice check if the caller (of this caller of this) is an aliased L1 contract address - * @return true iff the caller's address is an alias for an L1 contract address - */ - function wasMyCallersAddressAliased() external view returns (bool); - - /** - * @notice return the address of the caller (of this caller of this), without applying L1 contract address aliasing - * @return address of the caller's caller, without applying L1 contract address aliasing - */ - function myCallersAddressWithoutAliasing() external view returns (address); - - /** - * @notice Send given amount of Eth to dest from sender. - * This is a convenience function, which is equivalent to calling sendTxToL1 with empty data. - * @param destination recipient address on L1 - * @return unique identifier for this L2-to-L1 transaction. - */ - function withdrawEth(address destination) external payable returns (uint256); - - /** - * @notice Send a transaction to L1 - * @dev it is not possible to execute on the L1 any L2-to-L1 transaction which contains data - * to a contract address without any code (as enforced by the Bridge contract). - * @param destination recipient address on L1 - * @param data (optional) calldata for L1 contract call - * @return a unique identifier for this L2-to-L1 transaction. - */ - function sendTxToL1(address destination, bytes calldata data) external payable returns (uint256); - - /** - * @notice Get send Merkle tree state - * @return size number of sends in the history - * @return root root hash of the send history - * @return partials hashes of partial subtrees in the send history tree - */ - function sendMerkleTreeState() external view returns (uint256 size, bytes32 root, bytes32[] memory partials); - - /** - * @notice creates a send txn from L2 to L1 - * @param position = (level << 192) + leaf = (0 << 192) + leaf = leaf - */ - event L2ToL1Tx( - address caller, - address indexed destination, - uint256 indexed hash, - uint256 indexed position, - uint256 arbBlockNum, - uint256 ethBlockNum, - uint256 timestamp, - uint256 callvalue, - bytes data - ); - - /// @dev DEPRECATED in favour of the new L2ToL1Tx event above after the nitro upgrade - event L2ToL1Transaction( - address caller, - address indexed destination, - uint256 indexed uniqueId, - uint256 indexed batchNumber, - uint256 indexInBatch, - uint256 arbBlockNum, - uint256 ethBlockNum, - uint256 timestamp, - uint256 callvalue, - bytes data - ); - - /** - * @notice logs a merkle branch for proof synthesis - * @param reserved an index meant only to align the 4th index with L2ToL1Transaction's 4th event - * @param hash the merkle hash - * @param position = (level << 192) + leaf - */ - event SendMerkleUpdate(uint256 indexed reserved, bytes32 indexed hash, uint256 indexed position); -} diff --git a/contracts/vendor/arbitrum/IBridge.sol b/contracts/vendor/arbitrum/IBridge.sol deleted file mode 100644 index e71bedce0..000000000 --- a/contracts/vendor/arbitrum/IBridge.sol +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2021-2022, Offchain Labs, Inc. -// For license information, see https://github.com/nitro/blob/master/LICENSE -// SPDX-License-Identifier: BUSL-1.1 -// OpenZeppelin Contracts (last updated v4.8.0) (vendor/arbitrum/IBridge.sol) - -// solhint-disable-next-line compiler-version -pragma solidity >=0.6.9 <0.9.0; - -interface IBridge { - event MessageDelivered( - uint256 indexed messageIndex, - bytes32 indexed beforeInboxAcc, - address inbox, - uint8 kind, - address sender, - bytes32 messageDataHash, - uint256 baseFeeL1, - uint64 timestamp - ); - - event BridgeCallTriggered(address indexed outbox, address indexed to, uint256 value, bytes data); - - event InboxToggle(address indexed inbox, bool enabled); - - event OutboxToggle(address indexed outbox, bool enabled); - - event SequencerInboxUpdated(address newSequencerInbox); - - function allowedDelayedInboxList(uint256) external returns (address); - - function allowedOutboxList(uint256) external returns (address); - - /// @dev Accumulator for delayed inbox messages; tail represents hash of the current state; each element represents the inclusion of a new message. - function delayedInboxAccs(uint256) external view returns (bytes32); - - /// @dev Accumulator for sequencer inbox messages; tail represents hash of the current state; each element represents the inclusion of a new message. - function sequencerInboxAccs(uint256) external view returns (bytes32); - - // OpenZeppelin: changed return type from IOwnable - function rollup() external view returns (address); - - function sequencerInbox() external view returns (address); - - function activeOutbox() external view returns (address); - - function allowedDelayedInboxes(address inbox) external view returns (bool); - - function allowedOutboxes(address outbox) external view returns (bool); - - function sequencerReportedSubMessageCount() external view returns (uint256); - - /** - * @dev Enqueue a message in the delayed inbox accumulator. - * These messages are later sequenced in the SequencerInbox, either - * by the sequencer as part of a normal batch, or by force inclusion. - */ - function enqueueDelayedMessage( - uint8 kind, - address sender, - bytes32 messageDataHash - ) external payable returns (uint256); - - function executeCall( - address to, - uint256 value, - bytes calldata data - ) external returns (bool success, bytes memory returnData); - - function delayedMessageCount() external view returns (uint256); - - function sequencerMessageCount() external view returns (uint256); - - // ---------- onlySequencerInbox functions ---------- - - function enqueueSequencerMessage( - bytes32 dataHash, - uint256 afterDelayedMessagesRead, - uint256 prevMessageCount, - uint256 newMessageCount - ) external returns (uint256 seqMessageIndex, bytes32 beforeAcc, bytes32 delayedAcc, bytes32 acc); - - /** - * @dev Allows the sequencer inbox to submit a delayed message of the batchPostingReport type - * This is done through a separate function entrypoint instead of allowing the sequencer inbox - * to call `enqueueDelayedMessage` to avoid the gas overhead of an extra SLOAD in either - * every delayed inbox or every sequencer inbox call. - */ - function submitBatchSpendingReport(address batchPoster, bytes32 dataHash) external returns (uint256 msgNum); - - // ---------- onlyRollupOrOwner functions ---------- - - function setSequencerInbox(address _sequencerInbox) external; - - function setDelayedInbox(address inbox, bool enabled) external; - - function setOutbox(address inbox, bool enabled) external; - - // ---------- initializer ---------- - - // OpenZeppelin: changed rollup_ type from IOwnable - function initialize(address rollup_) external; -} diff --git a/contracts/vendor/arbitrum/IDelayedMessageProvider.sol b/contracts/vendor/arbitrum/IDelayedMessageProvider.sol deleted file mode 100644 index 914c25fb7..000000000 --- a/contracts/vendor/arbitrum/IDelayedMessageProvider.sol +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2021-2022, Offchain Labs, Inc. -// For license information, see https://github.com/nitro/blob/master/LICENSE -// SPDX-License-Identifier: BUSL-1.1 -// OpenZeppelin Contracts (last updated v4.8.0) (vendor/arbitrum/IDelayedMessageProvider.sol) - -// solhint-disable-next-line compiler-version -pragma solidity >=0.6.9 <0.9.0; - -interface IDelayedMessageProvider { - /// @dev event emitted when a inbox message is added to the Bridge's delayed accumulator - event InboxMessageDelivered(uint256 indexed messageNum, bytes data); - - /// @dev event emitted when a inbox message is added to the Bridge's delayed accumulator - /// same as InboxMessageDelivered but the batch data is available in tx.input - event InboxMessageDeliveredFromOrigin(uint256 indexed messageNum); -} diff --git a/contracts/vendor/arbitrum/IInbox.sol b/contracts/vendor/arbitrum/IInbox.sol deleted file mode 100644 index a8b67511c..000000000 --- a/contracts/vendor/arbitrum/IInbox.sol +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2021-2022, Offchain Labs, Inc. -// For license information, see https://github.com/nitro/blob/master/LICENSE -// SPDX-License-Identifier: BUSL-1.1 -// OpenZeppelin Contracts (last updated v4.8.0) (vendor/arbitrum/IInbox.sol) - -// solhint-disable-next-line compiler-version -pragma solidity >=0.6.9 <0.9.0; - -import "./IBridge.sol"; -import "./IDelayedMessageProvider.sol"; - -interface IInbox is IDelayedMessageProvider { - function bridge() external view returns (IBridge); - - // OpenZeppelin: changed return type from ISequencerInbox - function sequencerInbox() external view returns (address); - - /** - * @notice Send a generic L2 message to the chain - * @dev This method is an optimization to avoid having to emit the entirety of the messageData in a log. Instead validators are expected to be able to parse the data from the transaction's input - * @param messageData Data of the message being sent - */ - function sendL2MessageFromOrigin(bytes calldata messageData) external returns (uint256); - - /** - * @notice Send a generic L2 message to the chain - * @dev This method can be used to send any type of message that doesn't require L1 validation - * @param messageData Data of the message being sent - */ - function sendL2Message(bytes calldata messageData) external returns (uint256); - - function sendL1FundedUnsignedTransaction( - uint256 gasLimit, - uint256 maxFeePerGas, - uint256 nonce, - address to, - bytes calldata data - ) external payable returns (uint256); - - function sendL1FundedContractTransaction( - uint256 gasLimit, - uint256 maxFeePerGas, - address to, - bytes calldata data - ) external payable returns (uint256); - - function sendUnsignedTransaction( - uint256 gasLimit, - uint256 maxFeePerGas, - uint256 nonce, - address to, - uint256 value, - bytes calldata data - ) external returns (uint256); - - function sendContractTransaction( - uint256 gasLimit, - uint256 maxFeePerGas, - address to, - uint256 value, - bytes calldata data - ) external returns (uint256); - - /** - * @notice Get the L1 fee for submitting a retryable - * @dev This fee can be paid by funds already in the L2 aliased address or by the current message value - * @dev This formula may change in the future, to future proof your code query this method instead of inlining!! - * @param dataLength The length of the retryable's calldata, in bytes - * @param baseFee The block basefee when the retryable is included in the chain, if 0 current block.basefee will be used - */ - function calculateRetryableSubmissionFee(uint256 dataLength, uint256 baseFee) external view returns (uint256); - - /** - * @notice Deposit eth from L1 to L2 to address of the sender if sender is an EOA, and to its aliased address if the sender is a contract - * @dev This does not trigger the fallback function when receiving in the L2 side. - * Look into retryable tickets if you are interested in this functionality. - * @dev This function should not be called inside contract constructors - */ - function depositEth() external payable returns (uint256); - - /** - * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts - * @dev all msg.value will deposited to callValueRefundAddress on L2 - * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error - * @param to destination L2 contract address - * @param l2CallValue call value for retryable L2 message - * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee - * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on L2 balance - * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled - * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param maxFeePerGas price bid for L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param data ABI encoded data of L2 message - * @return unique message number of the retryable transaction - */ - function createRetryableTicket( - address to, - uint256 l2CallValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - bytes calldata data - ) external payable returns (uint256); - - /** - * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts - * @dev Same as createRetryableTicket, but does not guarantee that submission will succeed by requiring the needed funds - * come from the deposit alone, rather than falling back on the user's L2 balance - * @dev Advanced usage only (does not rewrite aliases for excessFeeRefundAddress and callValueRefundAddress). - * createRetryableTicket method is the recommended standard. - * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error - * @param to destination L2 contract address - * @param l2CallValue call value for retryable L2 message - * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee - * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on L2 balance - * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled - * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param maxFeePerGas price bid for L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param data ABI encoded data of L2 message - * @return unique message number of the retryable transaction - */ - function unsafeCreateRetryableTicket( - address to, - uint256 l2CallValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - bytes calldata data - ) external payable returns (uint256); - - // ---------- onlyRollupOrOwner functions ---------- - - /// @notice pauses all inbox functionality - function pause() external; - - /// @notice unpauses all inbox functionality - function unpause() external; - - // ---------- initializer ---------- - - /** - * @dev function to be called one time during the inbox upgrade process - * this is used to fix the storage slots - */ - function postUpgradeInit(IBridge _bridge) external; - - // OpenZeppelin: changed _sequencerInbox type from ISequencerInbox - function initialize(IBridge _bridge, address _sequencerInbox) external; -} diff --git a/contracts/vendor/arbitrum/IOutbox.sol b/contracts/vendor/arbitrum/IOutbox.sol deleted file mode 100644 index 22fa58f40..000000000 --- a/contracts/vendor/arbitrum/IOutbox.sol +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2021-2022, Offchain Labs, Inc. -// For license information, see https://github.com/nitro/blob/master/LICENSE -// SPDX-License-Identifier: BUSL-1.1 -// OpenZeppelin Contracts (last updated v4.8.0) (vendor/arbitrum/IOutbox.sol) - -// solhint-disable-next-line compiler-version -pragma solidity >=0.6.9 <0.9.0; - -import "./IBridge.sol"; - -interface IOutbox { - event SendRootUpdated(bytes32 indexed blockHash, bytes32 indexed outputRoot); - event OutBoxTransactionExecuted( - address indexed to, - address indexed l2Sender, - uint256 indexed zero, - uint256 transactionIndex - ); - - function rollup() external view returns (address); // the rollup contract - - function bridge() external view returns (IBridge); // the bridge contract - - function spent(uint256) external view returns (bytes32); // packed spent bitmap - - function roots(bytes32) external view returns (bytes32); // maps root hashes => L2 block hash - - // solhint-disable-next-line func-name-mixedcase - function OUTBOX_VERSION() external view returns (uint128); // the outbox version - - function updateSendRoot(bytes32 sendRoot, bytes32 l2BlockHash) external; - - /// @notice When l2ToL1Sender returns a nonzero address, the message was originated by an L2 account - /// When the return value is zero, that means this is a system message - /// @dev the l2ToL1Sender behaves as the tx.origin, the msg.sender should be validated to protect against reentrancies - function l2ToL1Sender() external view returns (address); - - /// @return l2Block return L2 block when the L2 tx was initiated or 0 if no L2 to L1 transaction is active - function l2ToL1Block() external view returns (uint256); - - /// @return l1Block return L1 block when the L2 tx was initiated or 0 if no L2 to L1 transaction is active - function l2ToL1EthBlock() external view returns (uint256); - - /// @return timestamp return L2 timestamp when the L2 tx was initiated or 0 if no L2 to L1 transaction is active - function l2ToL1Timestamp() external view returns (uint256); - - /// @return outputId returns the unique output identifier of the L2 to L1 tx or 0 if no L2 to L1 transaction is active - function l2ToL1OutputId() external view returns (bytes32); - - /** - * @notice Executes a messages in an Outbox entry. - * @dev Reverts if dispute period hasn't expired, since the outbox entry - * is only created once the rollup confirms the respective assertion. - * @dev it is not possible to execute any L2-to-L1 transaction which contains data - * to a contract address without any code (as enforced by the Bridge contract). - * @param proof Merkle proof of message inclusion in send root - * @param index Merkle path to message - * @param l2Sender sender if original message (i.e., caller of ArbSys.sendTxToL1) - * @param to destination address for L1 contract call - * @param l2Block l2 block number at which sendTxToL1 call was made - * @param l1Block l1 block number at which sendTxToL1 call was made - * @param l2Timestamp l2 Timestamp at which sendTxToL1 call was made - * @param value wei in L1 message - * @param data abi-encoded L1 message data - */ - function executeTransaction( - bytes32[] calldata proof, - uint256 index, - address l2Sender, - address to, - uint256 l2Block, - uint256 l1Block, - uint256 l2Timestamp, - uint256 value, - bytes calldata data - ) external; - - /** - * @dev function used to simulate the result of a particular function call from the outbox - * it is useful for things such as gas estimates. This function includes all costs except for - * proof validation (which can be considered offchain as a somewhat of a fixed cost - it's - * not really a fixed cost, but can be treated as so with a fixed overhead for gas estimation). - * We can't include the cost of proof validation since this is intended to be used to simulate txs - * that are included in yet-to-be confirmed merkle roots. The simulation entrypoint could instead pretend - * to confirm a pending merkle root, but that would be less practical for integrating with tooling. - * It is only possible to trigger it when the msg sender is address zero, which should be impossible - * unless under simulation in an eth_call or eth_estimateGas - */ - function executeTransactionSimulation( - uint256 index, - address l2Sender, - address to, - uint256 l2Block, - uint256 l1Block, - uint256 l2Timestamp, - uint256 value, - bytes calldata data - ) external; - - /** - * @param index Merkle path to message - * @return true if the message has been spent - */ - function isSpent(uint256 index) external view returns (bool); - - function calculateItemHash( - address l2Sender, - address to, - uint256 l2Block, - uint256 l1Block, - uint256 l2Timestamp, - uint256 value, - bytes calldata data - ) external pure returns (bytes32); - - function calculateMerkleRoot(bytes32[] memory proof, uint256 path, bytes32 item) external pure returns (bytes32); -} diff --git a/contracts/vendor/optimism/ICrossDomainMessenger.sol b/contracts/vendor/optimism/ICrossDomainMessenger.sol deleted file mode 100644 index cc01a48ab..000000000 --- a/contracts/vendor/optimism/ICrossDomainMessenger.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (vendor/optimism/ICrossDomainMessenger.sol) -pragma solidity >0.5.0 <0.9.0; - -/** - * @title ICrossDomainMessenger - */ -interface ICrossDomainMessenger { - /********** - * Events * - **********/ - - event SentMessage(address indexed target, address sender, bytes message, uint256 messageNonce, uint256 gasLimit); - event RelayedMessage(bytes32 indexed msgHash); - event FailedRelayedMessage(bytes32 indexed msgHash); - - /************* - * Variables * - *************/ - - function xDomainMessageSender() external view returns (address); - - /******************** - * Public Functions * - ********************/ - - /** - * Sends a cross domain message to the target messenger. - * @param _target Target contract address. - * @param _message Message to send to the target. - * @param _gasLimit Gas limit for the provided message. - */ - function sendMessage(address _target, bytes calldata _message, uint32 _gasLimit) external; -} diff --git a/contracts/vendor/optimism/LICENSE b/contracts/vendor/optimism/LICENSE deleted file mode 100644 index 6a7da5218..000000000 --- a/contracts/vendor/optimism/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -(The MIT License) - -Copyright 2020-2021 Optimism - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/contracts/vendor/polygon/IFxMessageProcessor.sol b/contracts/vendor/polygon/IFxMessageProcessor.sol deleted file mode 100644 index be73e6f53..000000000 --- a/contracts/vendor/polygon/IFxMessageProcessor.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (vendor/polygon/IFxMessageProcessor.sol) -pragma solidity ^0.8.0; - -interface IFxMessageProcessor { - function processMessageFromRoot(uint256 stateId, address rootMessageSender, bytes calldata data) external; -} diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 071045e68..fc38e8953 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -11,14 +11,11 @@ ** xref:erc20.adoc[ERC20] *** xref:erc20-supply.adoc[Creating Supply] ** xref:erc721.adoc[ERC721] -** xref:erc777.adoc[ERC777] ** xref:erc1155.adoc[ERC1155] ** xref:erc4626.adoc[ERC4626] * xref:governance.adoc[Governance] -* xref:crosschain.adoc[Crosschain] - * xref:utilities.adoc[Utilities] * xref:subgraphs::index.adoc[Subgraphs] diff --git a/docs/modules/ROOT/pages/crosschain.adoc b/docs/modules/ROOT/pages/crosschain.adoc deleted file mode 100644 index cbe24df77..000000000 --- a/docs/modules/ROOT/pages/crosschain.adoc +++ /dev/null @@ -1,210 +0,0 @@ -= Adding cross-chain support to contracts - -If your contract is targeting to be used in the context of multichain operations, you may need specific tools to identify and process these cross-chain operations. - -OpenZeppelin provides the xref:api:crosschain.adoc#CrossChainEnabled[`CrossChainEnabled`] abstract contract, that includes dedicated internal functions. - -In this guide, we will go through an example use case: _how to build an upgradeable & mintable ERC20 token controlled by a governor present on a foreign chain_. - -== Starting point, our ERC20 contract - -Let's start with a small ERC20 contract, that we bootstrapped using the https://wizard.openzeppelin.com/[OpenZeppelin Contracts Wizard], and extended with an owner that has the ability to mint. Note that for demonstration purposes we have not used the built-in `Ownable` contract. - -[source,solidity] ----- -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; - -contract MyToken is Initializable, ERC20Upgradeable, UUPSUpgradeable { - address public owner; - - modifier onlyOwner() { - require(owner == _msgSender(), "Not authorized"); - _; - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() initializer {} - - function initialize(address initialOwner) initializer public { - __ERC20_init("MyToken", "MTK"); - __UUPSUpgradeable_init(); - - owner = initialOwner; - } - - function mint(address to, uint256 amount) public onlyOwner { - _mint(to, amount); - } - - function _authorizeUpgrade(address newImplementation) internal override onlyOwner { - } -} ----- - -This token is mintable and upgradeable by the owner of the contract. - -== Preparing our contract for cross-chain operations. - -Let's now imagine that this contract is going to live on one chain, but we want the minting and the upgrading to be performed by a xref:governance.adoc[`governor`] contract on another chain. - -For example, we could have our token on xDai, with our governor on mainnet, or we could have our token on mainnet, with our governor on optimism - -In order to do that, we will start by adding xref:api:crosschain.adoc#CrossChainEnabled[`CrossChainEnabled`] to our contract. You will notice that the contract is now abstract. This is because `CrossChainEnabled` is an abstract contract: it is not tied to any particular chain and it deals with cross-chain interactions in an abstract way. This is what enables us to easily reuse the code for different chains. We will specialize it later by inheriting from a chain-specific implementation of the abstraction. - -```diff - import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -+import "@openzeppelin/contracts-upgradeable/crosschain/CrossChainEnabled.sol"; - --contract MyToken is Initializable, ERC20Upgradeable, UUPSUpgradeable { -+abstract contract MyTokenCrossChain is Initializable, ERC20Upgradeable, UUPSUpgradeable, CrossChainEnabled { -``` - -Once that is done, we can use the `onlyCrossChainSender` modifier, provided by `CrossChainEnabled` in order to protect the minting and upgrading operations. - -```diff -- function mint(address to, uint256 amount) public onlyOwner { -+ function mint(address to, uint256 amount) public onlyCrossChainSender(owner) { - -- function _authorizeUpgrade(address newImplementation) internal override onlyOwner { -+ function _authorizeUpgrade(address newImplementation) internal override onlyCrossChainSender(owner) { -``` - -This change will effectively restrict the mint and upgrade operations to the `owner` on the remote chain. - -== Specializing for a specific chain - -Once the abstract cross-chain version of our token is ready we can easily specialize it for the chain we want, or more precisely for the bridge system that we want to rely on. - -This is done using one of the many `CrossChainEnabled` implementations. - -For example, if our token is on xDai, and our governor on mainnet, we can use the https://docs.tokenbridge.net/amb-bridge/about-amb-bridge[AMB] bridge available on xDai at https://blockscout.com/xdai/mainnet/address/0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59[0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59] - -[source,solidity] ----- -[...] - -import "@openzeppelin/contracts-upgradeable/crosschain/amb/CrossChainEnabledAMB.sol"; - -contract MyTokenXDAI is - MyTokenCrossChain, - CrossChainEnabledAMB(0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59) -{} ----- - -If the token is on Ethereum mainnet, and our governor on Optimism, we use the Optimism https://community.optimism.io/docs/protocol/protocol-2.0/#l1crossdomainmessenger[CrossDomainMessenger] available on mainnet at https://etherscan.io/address/0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1[0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1]. - -[source,solidity] ----- -[...] - -import "@openzeppelin/contracts-upgradeable/crosschain/optimismCrossChainEnabledOptimism.sol"; - -contract MyTokenOptimism is - MyTokenCrossChain, - CrossChainEnabledOptimism(0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1) -{} ----- - -== Mixing cross domain addresses is dangerous - -When designing a contract with cross-chain support, it is essential to understand possible fallbacks and the security assumption that are being made. - -In this guide, we are particularly focusing on restricting access to a specific caller. This is usually done (as shown above) using `msg.sender` or `_msgSender()`. However, when going cross-chain, it is not just that simple. Even without considering possible bridge issues, it is important to keep in mind that the same address can correspond to very different entities when considering a multi-chain space. EOA wallets can only execute operations if the wallet's private-key signs the transaction. To our knowledge this is the case in all EVM chains, so a cross-chain message coming from such a wallet is arguably equivalent to a non-cross-chain message by the same wallet. The situation is however very different for smart contracts. - -Due to the way smart contract addresses are computed, and the fact that smart contracts on different chains live independent lives, you could have two very different contracts live at the same address on different chains. You could imagine two multisig wallets with different signers using the same address on different chains. You could also see a very basic smart wallet live on one chain at the same address as a full-fledged governor on another chain. Therefore, you should be careful that whenever you give permissions to a specific address, you control with chain this address can act from. - -== Going further with access control - -In the previous example, we have both an `onlyOwner()` modifier and the `onlyCrossChainSender(owner)` mechanism. We didn't use the xref:access-control.adoc#ownership-and-ownable[`Ownable`] pattern because the ownership transfer mechanism in includes is not designed to work with the owner being a cross-chain entity. Unlike xref:access-control.adoc#ownership-and-ownable[`Ownable`], xref:access-control.adoc#role-based-access-control[`AccessControl`] is more effective at capturing the nuances and can effectively be used to build cross-chain-aware contracts. - -Using xref:api:access.adoc#AccessControlCrossChain[`AccessControlCrossChain`] includes both the xref:api:access.adoc#AccessControl[`AccessControl`] core and the xref:api:crosschain.adoc#CrossChainEnabled[`CrossChainEnabled`] abstraction. It also includes some binding to make role management compatible with cross-chain operations. - -In the case of the `mint` function, the caller must have the `MINTER_ROLE` when the call originates from the same chain. If the caller is on a remote chain, then the caller should not have the `MINTER_ROLE`, but the "aliased" version (`MINTER_ROLE ^ CROSSCHAIN_ALIAS`). This mitigates the danger described in the previous section by strictly separating local accounts from remote accounts from a different chain. See the xref:api:access.adoc#AccessControlCrossChain[`AccessControlCrossChain`] documentation for more details. - - -```diff - import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; - import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -+import "@openzeppelin/contracts-upgradeable/access/AccessControlCrossChainUpgradeable.sol"; - --abstract contract MyTokenCrossChain is Initializable, ERC20Upgradeable, UUPSUpgradeable, CrossChainEnabled { -+abstract contract MyTokenCrossChain is Initializable, ERC20Upgradeable, UUPSUpgradeable, AccessControlCrossChainUpgradeable { - -- address public owner; -- modifier onlyOwner() { -- require(owner == _msgSender(), "Not authorized"); -- _; -- } - -+ bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); -+ bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); - - function initialize(address initialOwner) initializer public { - __ERC20_init("MyToken", "MTK"); - __UUPSUpgradeable_init(); -+ __AccessControl_init(); -+ _grantRole(_crossChainRoleAlias(DEFAULT_ADMIN_ROLE), initialOwner); // initialOwner is on a remote chain -- owner = initialOwner; - } - -- function mint(address to, uint256 amount) public onlyCrossChainSender(owner) { -+ function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { - -- function _authorizeUpgrade(address newImplementation) internal override onlyCrossChainSender(owner) { -+ function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) { -``` - -This results in the following, final, code: - -[source,solidity] ----- -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/AccessControlCrossChainUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; - -abstract contract MyTokenCrossChain is Initializable, ERC20Upgradeable, AccessControlCrossChainUpgradeable, UUPSUpgradeable { - bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); - bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() initializer {} - - function initialize(address initialOwner) initializer public { - __ERC20_init("MyToken", "MTK"); - __AccessControl_init(); - __UUPSUpgradeable_init(); - - _grantRole(_crossChainRoleAlias(DEFAULT_ADMIN_ROLE), initialOwner); // initialOwner is on a remote chain - } - - function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { - _mint(to, amount); - } - - function _authorizeUpgrade(address newImplementation) internal onlyRole(UPGRADER_ROLE) override { - } -} - -import "@openzeppelin/contracts-upgradeable/crosschain/amb/CrossChainEnabledAMB.sol"; - -contract MyTokenXDAI is - MyTokenCrossChain, - CrossChainEnabledAMB(0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59) -{} - -import "@openzeppelin/contracts-upgradeable/crosschain/optimismCrossChainEnabledOptimism.sol"; - -contract MyTokenOptimism is - MyTokenCrossChain, - CrossChainEnabledOptimism(0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1) -{} ----- diff --git a/docs/modules/ROOT/pages/erc1155.adoc b/docs/modules/ROOT/pages/erc1155.adoc index a01542c5a..d7976ca1f 100644 --- a/docs/modules/ROOT/pages/erc1155.adoc +++ b/docs/modules/ROOT/pages/erc1155.adoc @@ -2,7 +2,7 @@ ERC1155 is a novel token standard that aims to take the best from previous standards to create a xref:tokens.adoc#different-kinds-of-tokens[*fungibility-agnostic*] and *gas-efficient* xref:tokens.adoc#but_first_coffee_a_primer_on_token_contracts[token contract]. -TIP: ERC1155 draws ideas from all of xref:erc20.adoc[ERC20], xref:erc721.adoc[ERC721], and xref:erc777.adoc[ERC777]. If you're unfamiliar with those standards, head to their guides before moving on. +TIP: ERC1155 draws ideas from all of xref:erc20.adoc[ERC20], xref:erc721.adoc[ERC721], and https://eips.ethereum.org/EIPS/eip-777[ERC777]. If you're unfamiliar with those standards, head to their guides before moving on. [[multi-token-standard]] == Multi Token Standard @@ -22,9 +22,9 @@ In the spirit of the standard, we've also included batch operations in the non-s == Constructing an ERC1155 Token Contract -We'll use ERC1155 to track multiple items in our game, which will each have their own unique attributes. We mint all items to the deployer of the contract, which we can later transfer to players. Players are free to keep their tokens or trade them with other people as they see fit, as they would any other asset on the blockchain! +We'll use ERC1155 to track multiple items in our game, which will each have their own unique attributes. We mint all items to the deployer of the contract, which we can later transfer to players. Players are free to keep their tokens or trade them with other people as they see fit, as they would any other asset on the blockchain! -For simplicity, we will mint all items in the constructor, but you could add minting functionality to the contract to mint on demand to players. +For simplicity, we will mint all items in the constructor, but you could add minting functionality to the contract to mint on demand to players. TIP: For an overview of minting mechanisms, check out xref:erc20-supply.adoc[Creating ERC20 Supply]. @@ -112,7 +112,7 @@ The JSON document for token ID 2 might look something like: For more information about the metadata JSON Schema, check out the https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md#erc-1155-metadata-uri-json-schema[ERC-1155 Metadata URI JSON Schema]. -NOTE: You'll notice that the item's information is included in the metadata, but that information isn't on-chain! So a game developer could change the underlying metadata, changing the rules of the game! +NOTE: You'll notice that the item's information is included in the metadata, but that information isn't on-chain! So a game developer could change the underlying metadata, changing the rules of the game! TIP: If you'd like to put all item information on-chain, you can extend ERC721 to do so (though it will be rather costly) by providing a xref:utilities.adoc#base64[`Base64`] Data URI with the JSON schema encoded. You could also leverage IPFS to store the URI information, but these techniques are out of the scope of this overview guide diff --git a/docs/modules/ROOT/pages/erc777.adoc b/docs/modules/ROOT/pages/erc777.adoc deleted file mode 100644 index 4a0af16c3..000000000 --- a/docs/modules/ROOT/pages/erc777.adoc +++ /dev/null @@ -1,75 +0,0 @@ -= ERC777 - -CAUTION: As of v4.9, OpenZeppelin's implementation of ERC-777 is deprecated and will be removed in the next major release. - -Like xref:erc20.adoc[ERC20], ERC777 is a standard for xref:tokens.adoc#different-kinds-of-tokens[_fungible_ tokens], and is focused around allowing more complex interactions when trading tokens. More generally, it brings tokens and Ether closer together by providing the equivalent of a `msg.value` field, but for tokens. - -The standard also brings multiple quality-of-life improvements, such as getting rid of the confusion around `decimals`, minting and burning with proper events, among others, but its killer feature is *receive hooks*. A hook is simply a function in a contract that is called when tokens are sent to it, meaning *accounts and contracts can react to receiving tokens*. - -This enables a lot of interesting use cases, including atomic purchases using tokens (no need to do `approve` and `transferFrom` in two separate transactions), rejecting reception of tokens (by reverting on the hook call), redirecting the received tokens to other addresses (similarly to how xref:api:payment#PaymentSplitter[`PaymentSplitter`] does it), among many others. - -Furthermore, since contracts are required to implement these hooks in order to receive tokens, _no tokens can get stuck in a contract that is unaware of the ERC777 protocol_, as has happened countless times when using ERC20s. - -== What If I Already Use ERC20? - -The standard has you covered! The ERC777 standard is *backwards compatible with ERC20*, meaning you can interact with these tokens as if they were ERC20, using the standard functions, while still getting all of the niceties, including send hooks. See the https://eips.ethereum.org/EIPS/eip-777#backward-compatibility[EIP's Backwards Compatibility section] to learn more. - -== Constructing an ERC777 Token Contract - -We will replicate the `GLD` example of the xref:erc20.adoc#constructing-an-erc20-token-contract[ERC20 guide], this time using ERC777. As always, check out the xref:api:token/ERC777.adoc[`API reference`] to learn more about the details of each function. - -[source,solidity] ----- -// contracts/GLDToken.sol -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "@openzeppelin/contracts/token/ERC777/ERC777.sol"; - -contract GLDToken is ERC777 { - constructor(uint256 initialSupply, address[] memory defaultOperators) - ERC777("Gold", "GLD", defaultOperators) - { - _mint(msg.sender, initialSupply, "", ""); - } -} ----- - -In this case, we'll be extending from the xref:api:token/ERC777.adoc#ERC777[`ERC777`] contract, which provides an implementation with compatibility support for ERC20. The API is quite similar to that of xref:api:token/ERC777.adoc#ERC777[`ERC777`], and we'll once again make use of xref:api:token/ERC777.adoc#ERC777-_mint-address-address-uint256-bytes-bytes-[`_mint`] to assign the `initialSupply` to the deployer account. Unlike xref:api:token/ERC20.adoc#ERC20-_mint-address-uint256-[ERC20's `_mint`], this one includes some extra parameters, but you can safely ignore those for now. - -You'll notice both xref:api:token/ERC777.adoc#IERC777-name--[`name`] and xref:api:token/ERC777.adoc#IERC777-symbol--[`symbol`] are assigned, but not xref:api:token/ERC777.adoc#ERC777-decimals--[`decimals`]. The ERC777 specification makes it mandatory to include support for these functions (unlike ERC20, where it is optional and we had to include xref:api:token/ERC20.adoc#ERC20Detailed[`ERC20Detailed`]), but also mandates that `decimals` always returns a fixed value of `18`, so there's no need to set it ourselves. For a review of ``decimals``'s role and importance, refer back to our xref:erc20.adoc#a-note-on-decimals[ERC20 guide]. - -Finally, we'll need to set the xref:api:token/ERC777.adoc#IERC777-defaultOperators--[`defaultOperators`]: special accounts (usually other smart contracts) that will be able to transfer tokens on behalf of their holders. If you're not planning on using operators in your token, you can simply pass an empty array. _Stay tuned for an upcoming in-depth guide on ERC777 operators!_ - -That's it for a basic token contract! We can now deploy it, and use the same xref:api:token/ERC777.adoc#IERC777-balanceOf-address-[`balanceOf`] method to query the deployer's balance: - -[source,javascript] ----- -> GLDToken.balanceOf(deployerAddress) -1000 ----- - -To move tokens from one account to another, we can use both xref:api:token/ERC777.adoc#ERC777-transfer-address-uint256-[``ERC20``'s `transfer`] method, or the new xref:api:token/ERC777.adoc#ERC777-send-address-uint256-bytes-[``ERC777``'s `send`], which fulfills a very similar role, but adds an optional `data` field: - -[source,javascript] ----- -> GLDToken.transfer(otherAddress, 300) -> GLDToken.send(otherAddress, 300, "") -> GLDToken.balanceOf(otherAddress) -600 -> GLDToken.balanceOf(deployerAddress) -400 ----- - -== Sending Tokens to Contracts - -A key difference when using xref:api:token/ERC777.adoc#ERC777-send-address-uint256-bytes-[`send`] is that token transfers to other contracts may revert with the following message: - -[source,text] ----- -ERC777: token recipient contract has no implementer for ERC777TokensRecipient ----- - -This is a good thing! It means that the recipient contract has not registered itself as aware of the ERC777 protocol, so transfers to it are disabled to *prevent tokens from being locked forever*. As an example, https://etherscan.io/token/0xa74476443119A942dE498590Fe1f2454d7D4aC0d?a=0xa74476443119A942dE498590Fe1f2454d7D4aC0d[the Golem contract currently holds over 350k `GNT` tokens], worth multiple tens of thousands of dollars, and lacks methods to get them out of there. This has happened to virtually every ERC20-backed project, usually due to user error. - -_An upcoming guide will cover how a contract can register itself as a recipient, send and receive hooks, and other advanced features of ERC777!_ diff --git a/docs/modules/ROOT/pages/tokens.adoc b/docs/modules/ROOT/pages/tokens.adoc index b168756df..10626f548 100644 --- a/docs/modules/ROOT/pages/tokens.adoc +++ b/docs/modules/ROOT/pages/tokens.adoc @@ -28,5 +28,4 @@ You've probably heard of the ERC20 or ERC721 token standards, and that's why you * xref:erc20.adoc[ERC20]: the most widespread token standard for fungible assets, albeit somewhat limited by its simplicity. * xref:erc721.adoc[ERC721]: the de-facto solution for non-fungible tokens, often used for collectibles and games. - * xref:erc777.adoc[ERC777]: a richer standard for fungible tokens, enabling new use cases and building on past learnings. Backwards compatible with ERC20. * xref:erc1155.adoc[ERC1155]: a novel standard for multi-tokens, allowing for a single contract to represent multiple fungible and non-fungible tokens, along with batched operations for increased gas efficiency. diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 88207f0e1..eecb8293e 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -87,9 +87,7 @@ Easy! Want to split some payments between multiple people? Maybe you have an app that sends 30% of art purchases to the original creator and 70% of the profits to the current owner; you can build that with xref:api:finance.adoc#PaymentSplitter[`PaymentSplitter`]! -In Solidity, there are some security concerns with blindly sending money to accounts, since it allows them to execute arbitrary code. You can read up on these security concerns in the https://consensys.github.io/smart-contract-best-practices/[Ethereum Smart Contract Best Practices] website. One of the ways to fix reentrancy and stalling problems is, instead of immediately sending Ether to accounts that need it, you can use xref:api:security.adoc#PullPayment[`PullPayment`], which offers an xref:api:security.adoc#PullPayment-_asyncTransfer-address-uint256-[`_asyncTransfer`] function for sending money to something and requesting that they xref:api:security.adoc#PullPayment-withdrawPayments-address-payable-[`withdrawPayments()`] it later. - -If you want to Escrow some funds, check out xref:api:utils.adoc#Escrow[`Escrow`] and xref:api:utils.adoc#ConditionalEscrow[`ConditionalEscrow`] for governing the release of some escrowed Ether. +In Solidity, there are some security concerns with blindly sending money to accounts, since it allows them to execute arbitrary code. You can read up on these security concerns in the https://consensys.github.io/smart-contract-best-practices/[Ethereum Smart Contract Best Practices] website. [[collections]] == Collections @@ -103,7 +101,7 @@ Want to keep track of some numbers that increment by 1 every time you want anoth === Base64 -xref:api:utils.adoc#Base64[`Base64`] util allows you to transform `bytes32` data into its Base64 `string` representation. +xref:api:utils.adoc#Base64[`Base64`] util allows you to transform `bytes32` data into its Base64 `string` representation. This is especially useful for building URL-safe tokenURIs for both xref:api:token/ERC721.adoc#IERC721Metadata-tokenURI-uint256-[`ERC721`] or xref:api:token/ERC1155.adoc#IERC1155MetadataURI-uri-uint256-[`ERC1155`]. This library provides a clever way to serve URL-safe https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URIs/[Data URI] compliant strings to serve on-chain data structures. @@ -122,7 +120,7 @@ contract My721Token is ERC721 { using Strings for uint256; constructor() ERC721("My721Token", "MTK") {} - + ... function tokenURI(uint256 tokenId) @@ -140,7 +138,7 @@ contract My721Token is ERC721 { return string( abi.encodePacked( - "data:application/json;base64,", + "data:application/json;base64,", Base64.encode(dataURI) ) ); diff --git a/hardhat.config.js b/hardhat.config.js index 5fd703d06..17d2c47b2 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -93,11 +93,7 @@ module.exports = { }, exposed: { initializers: true, - exclude: [ - 'vendor/**/*', - // Exclude Timers from hardhat-exposed because its overloaded functions are not transformed correctly - 'utils/Timers{,Upgradeable}.sol', - ], + exclude: ['vendor/**/*'], }, docgen: require('./docs/config'), }; diff --git a/scripts/generate/templates/Checkpoints.js b/scripts/generate/templates/Checkpoints.js index f64973b31..de58ad19e 100644 --- a/scripts/generate/templates/Checkpoints.js +++ b/scripts/generate/templates/Checkpoints.js @@ -1,5 +1,5 @@ const format = require('../format-lines'); -const { OPTS, LEGACY_OPTS } = require('./Checkpoints.opts.js'); +const { OPTS } = require('./Checkpoints.opts.js'); // TEMPLATE const header = `\ @@ -19,7 +19,7 @@ import "./math/SafeCast.sol"; */ `; -const types = opts => `\ +const template = opts => `\ struct ${opts.historyTypeName} { ${opts.checkpointTypeName}[] ${opts.checkpointFieldName}; } @@ -28,10 +28,7 @@ struct ${opts.checkpointTypeName} { ${opts.keyTypeName} ${opts.keyFieldName}; ${opts.valueTypeName} ${opts.valueFieldName}; } -`; -/* eslint-disable max-len */ -const operations = opts => `\ /** * @dev Pushes a (\`key\`, \`value\`) pair into a ${opts.historyTypeName} so that it is stored as the checkpoint. * @@ -87,77 +84,7 @@ function upperLookupRecent(${opts.historyTypeName} storage self, ${opts.keyTypeN return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName}; } -`; -const legacyOperations = opts => `\ -/** - * @dev Returns the value at a given block number. If a checkpoint is not available at that block, the closest one - * before it is returned, or zero otherwise. Because the number returned corresponds to that at the end of the - * block, the requested block number must be in the past, excluding the current block. - */ -function getAtBlock(${opts.historyTypeName} storage self, uint256 blockNumber) internal view returns (uint256) { - require(blockNumber < block.number, "Checkpoints: block not yet mined"); - uint32 key = SafeCast.toUint32(blockNumber); - - uint256 len = self.${opts.checkpointFieldName}.length; - uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, 0, len); - return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName}; -} - -/** - * @dev Returns the value at a given block number. If a checkpoint is not available at that block, the closest one - * before it is returned, or zero otherwise. Similar to {upperLookup} but optimized for the case when the searched - * checkpoint is probably "recent", defined as being among the last sqrt(N) checkpoints where N is the number of - * checkpoints. - */ -function getAtProbablyRecentBlock(${opts.historyTypeName} storage self, uint256 blockNumber) internal view returns (uint256) { - require(blockNumber < block.number, "Checkpoints: block not yet mined"); - uint32 key = SafeCast.toUint32(blockNumber); - - uint256 len = self.${opts.checkpointFieldName}.length; - - uint256 low = 0; - uint256 high = len; - - if (len > 5) { - uint256 mid = len - Math.sqrt(len); - if (key < _unsafeAccess(self.${opts.checkpointFieldName}, mid)._blockNumber) { - high = mid; - } else { - low = mid + 1; - } - } - - uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, low, high); - - return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName}; -} - -/** - * @dev Pushes a value onto a History so that it is stored as the checkpoint for the current block. - * - * Returns previous value and new value. - */ -function push(${opts.historyTypeName} storage self, uint256 value) internal returns (uint256, uint256) { - return _insert(self.${opts.checkpointFieldName}, SafeCast.toUint32(block.number), SafeCast.toUint224(value)); -} - -/** - * @dev Pushes a value onto a History, by updating the latest value using binary operation \`op\`. The new value will - * be set to \`op(latest, delta)\`. - * - * Returns previous value and new value. - */ -function push( - ${opts.historyTypeName} storage self, - function(uint256, uint256) view returns (uint256) op, - uint256 delta -) internal returns (uint256, uint256) { - return push(self, op(latest(self), delta)); -} -`; - -const common = opts => `\ /** * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. */ @@ -299,13 +226,6 @@ function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos) module.exports = format( header.trimEnd(), 'library Checkpoints {', - [ - // Legacy types & functions - types(LEGACY_OPTS), - legacyOperations(LEGACY_OPTS), - common(LEGACY_OPTS), - // New flavors - ...OPTS.flatMap(opts => [types(opts), operations(opts), common(opts)]), - ], + OPTS.flatMap(opts => template(opts)), '}', ); diff --git a/scripts/generate/templates/Checkpoints.opts.js b/scripts/generate/templates/Checkpoints.opts.js index 03c5a9569..b8be23104 100644 --- a/scripts/generate/templates/Checkpoints.opts.js +++ b/scripts/generate/templates/Checkpoints.opts.js @@ -12,11 +12,6 @@ const defaultOpts = size => ({ }); module.exports = { + VALUE_SIZES, OPTS: VALUE_SIZES.map(size => defaultOpts(size)), - LEGACY_OPTS: { - ...defaultOpts(224), - historyTypeName: 'History', - checkpointTypeName: 'Checkpoint', - keyFieldName: '_blockNumber', - }, }; diff --git a/scripts/generate/templates/Checkpoints.t.js b/scripts/generate/templates/Checkpoints.t.js index b3da933a1..0451ecea4 100644 --- a/scripts/generate/templates/Checkpoints.t.js +++ b/scripts/generate/templates/Checkpoints.t.js @@ -1,6 +1,6 @@ const format = require('../format-lines'); const { capitalize } = require('../../helpers'); -const { OPTS, LEGACY_OPTS } = require('./Checkpoints.opts.js'); +const { OPTS } = require('./Checkpoints.opts.js'); // TEMPLATE const header = `\ @@ -12,7 +12,7 @@ import "../../contracts/utils/math/SafeCast.sol"; `; /* eslint-disable max-len */ -const common = opts => `\ +const template = opts => `\ using Checkpoints for Checkpoints.${opts.historyTypeName}; // Maximum gap between keys used during the fuzzing tests: the \`_prepareKeys\` function with make sure that @@ -52,9 +52,7 @@ function _assertLatestCheckpoint( assertEq(_key, key); assertEq(_value, value); } -`; -const testTrace = opts => `\ // tests function testPush( ${opts.keyTypeName}[] memory keys, @@ -88,7 +86,7 @@ function testPush( ${opts.keyTypeName} lastKey = keys[keys.length - 1]; if (lastKey > 0) { pastKey = _bound${capitalize(opts.keyTypeName)}(pastKey, 0, lastKey - 1); - + vm.expectRevert(); this.push(pastKey, values[keys.length % values.length]); } @@ -141,116 +139,8 @@ function testLookup( } `; -const testHistory = opts => `\ -// tests -function testPush( - ${opts.keyTypeName}[] memory keys, - ${opts.valueTypeName}[] memory values, - ${opts.keyTypeName} pastKey -) public { - vm.assume(values.length > 0 && values.length <= keys.length); - _prepareKeys(keys, _KEY_MAX_GAP); - - // initial state - assertEq(_ckpts.length(), 0); - assertEq(_ckpts.latest(), 0); - _assertLatestCheckpoint(false, 0, 0); - - uint256 duplicates = 0; - for (uint256 i = 0; i < keys.length; ++i) { - ${opts.keyTypeName} key = keys[i]; - ${opts.valueTypeName} value = values[i % values.length]; - if (i > 0 && key == keys[i - 1]) ++duplicates; - - // push - vm.roll(key); - _ckpts.push(value); - - // check length & latest - assertEq(_ckpts.length(), i + 1 - duplicates); - assertEq(_ckpts.latest(), value); - _assertLatestCheckpoint(true, key, value); - } - - // Can't push any key in the past - if (keys.length > 0) { - ${opts.keyTypeName} lastKey = keys[keys.length - 1]; - if (lastKey > 0) { - pastKey = _bound${capitalize(opts.keyTypeName)}(pastKey, 0, lastKey - 1); - - vm.roll(pastKey); - vm.expectRevert(); - this.push(values[keys.length % values.length]); - } - } -} - -// used to test reverts -function push(${opts.valueTypeName} value) external { - _ckpts.push(value); -} - -function testLookup( - ${opts.keyTypeName}[] memory keys, - ${opts.valueTypeName}[] memory values, - ${opts.keyTypeName} lookup -) public { - vm.assume(keys.length > 0); - vm.assume(values.length > 0 && values.length <= keys.length); - _prepareKeys(keys, _KEY_MAX_GAP); - - ${opts.keyTypeName} lastKey = keys[keys.length - 1]; - vm.assume(lastKey > 0); - lookup = _bound${capitalize(opts.keyTypeName)}(lookup, 0, lastKey - 1); - - ${opts.valueTypeName} upper = 0; - for (uint256 i = 0; i < keys.length; ++i) { - ${opts.keyTypeName} key = keys[i]; - ${opts.valueTypeName} value = values[i % values.length]; - - // push - vm.roll(key); - _ckpts.push(value); - - // track expected result of lookups - if (key <= lookup) { - upper = value; - } - } - - // check lookup - assertEq(_ckpts.getAtBlock(lookup), upper); - assertEq(_ckpts.getAtProbablyRecentBlock(lookup), upper); - - vm.expectRevert(); this.getAtBlock(lastKey); - vm.expectRevert(); this.getAtBlock(lastKey + 1); - vm.expectRevert(); this.getAtProbablyRecentBlock(lastKey); - vm.expectRevert(); this.getAtProbablyRecentBlock(lastKey + 1); -} - -// used to test reverts -function getAtBlock(${opts.keyTypeName} key) external view { - _ckpts.getAtBlock(key); -} - -// used to test reverts -function getAtProbablyRecentBlock(${opts.keyTypeName} key) external view { - _ckpts.getAtProbablyRecentBlock(key); -} -`; -/* eslint-enable max-len */ - // GENERATE module.exports = format( header, - // HISTORY - `contract Checkpoints${LEGACY_OPTS.historyTypeName}Test is Test {`, - [common(LEGACY_OPTS), testHistory(LEGACY_OPTS)], - '}', - // TRACEXXX - ...OPTS.flatMap(opts => [ - `contract Checkpoints${opts.historyTypeName}Test is Test {`, - [common(opts), testTrace(opts)], - '}', - ]), + ...OPTS.flatMap(opts => [`contract Checkpoints${opts.historyTypeName}Test is Test {`, [template(opts)], '}']), ); diff --git a/scripts/generate/templates/EnumerableMap.js b/scripts/generate/templates/EnumerableMap.js index 2bbb69e3e..61e23cfb4 100644 --- a/scripts/generate/templates/EnumerableMap.js +++ b/scripts/generate/templates/EnumerableMap.js @@ -153,22 +153,6 @@ function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns return value; } -/** - * @dev Same as {get}, with a custom error message when \`key\` is not in the map. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryGet}. - */ -function get( - Bytes32ToBytes32Map storage map, - bytes32 key, - string memory errorMessage -) internal view returns (bytes32) { - bytes32 value = map._values[key]; - require(value != 0 || contains(map, key), errorMessage); - return value; -} - /** * @dev Return the an array containing all the keys * @@ -261,20 +245,6 @@ function get(${name} storage map, ${keyType} key) internal view returns (${value return ${fromBytes32(valueType, `get(map._inner, ${toBytes32(keyType, 'key')})`)}; } -/** - * @dev Same as {get}, with a custom error message when \`key\` is not in the map. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryGet}. - */ -function get( - ${name} storage map, - ${keyType} key, - string memory errorMessage -) internal view returns (${valueType}) { - return ${fromBytes32(valueType, `get(map._inner, ${toBytes32(keyType, 'key')}, errorMessage)`)}; -} - /** * @dev Return the an array containing all the keys * diff --git a/scripts/generate/templates/SafeCast.js b/scripts/generate/templates/SafeCast.js index 3e2b148fd..b47193be6 100644 --- a/scripts/generate/templates/SafeCast.js +++ b/scripts/generate/templates/SafeCast.js @@ -74,9 +74,6 @@ pragma solidity ^0.8.0; * * Using this library instead of the unchecked operations eliminates an entire * class of bugs, so it's recommended to use it always. - * - * Can be combined with {SafeMath} and {SignedSafeMath} to extend it to smaller types, by performing - * all math on \`uint256\` and \`int256\` and then downcasting. */ `; diff --git a/scripts/upgradeable/upgradeable.patch b/scripts/upgradeable/upgradeable.patch index 6f022d7b5..bc675ab8a 100644 --- a/scripts/upgradeable/upgradeable.patch +++ b/scripts/upgradeable/upgradeable.patch @@ -159,19 +159,6 @@ index 55e70b17..ceefb984 100644 }, "keywords": [ "solidity", -diff --git a/contracts/security/PullPayment.sol b/contracts/security/PullPayment.sol -index 65b4980f..f336592e 100644 ---- a/contracts/security/PullPayment.sol -+++ b/contracts/security/PullPayment.sol -@@ -22,6 +22,8 @@ import "../utils/escrow/Escrow.sol"; - * To use, derive from the `PullPayment` contract, and use {_asyncTransfer} - * instead of Solidity's `transfer` function. Payees can query their due - * payments with {payments}, and retrieve them with {withdrawPayments}. -+ * -+ * @custom:storage-size 51 - */ - abstract contract PullPayment { - Escrow private immutable _escrow; diff --git a/contracts/token/ERC20/extensions/ERC20Capped.sol b/contracts/token/ERC20/extensions/ERC20Capped.sol index 16f830d1..9ef98148 100644 --- a/contracts/token/ERC20/extensions/ERC20Capped.sol diff --git a/test/access/AccessControlCrossChain.test.js b/test/access/AccessControlCrossChain.test.js deleted file mode 100644 index d5a41076b..000000000 --- a/test/access/AccessControlCrossChain.test.js +++ /dev/null @@ -1,49 +0,0 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); -const { BridgeHelper } = require('../helpers/crosschain'); - -const { DEFAULT_ADMIN_ROLE, shouldBehaveLikeAccessControl } = require('./AccessControl.behavior.js'); - -const crossChainRoleAlias = role => - web3.utils.leftPad( - web3.utils.toHex(web3.utils.toBN(role).xor(web3.utils.toBN(web3.utils.soliditySha3('CROSSCHAIN_ALIAS')))), - 64, - ); - -const AccessControlCrossChainMock = artifacts.require('$AccessControlCrossChainMock'); - -const ROLE = web3.utils.soliditySha3('ROLE'); - -contract('AccessControl', function (accounts) { - before(async function () { - this.bridge = await BridgeHelper.deploy(); - }); - - beforeEach(async function () { - this.accessControl = await AccessControlCrossChainMock.new({ from: accounts[0] }); - await this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, accounts[0]); - }); - - shouldBehaveLikeAccessControl('AccessControl', ...accounts); - - describe('CrossChain enabled', function () { - beforeEach(async function () { - await this.accessControl.grantRole(ROLE, accounts[0], { from: accounts[0] }); - await this.accessControl.grantRole(crossChainRoleAlias(ROLE), accounts[1], { from: accounts[0] }); - }); - - it('check alliassing', async function () { - expect(await this.accessControl.$_crossChainRoleAlias(ROLE)).to.be.bignumber.equal(crossChainRoleAlias(ROLE)); - }); - - it('Crosschain calls not authorized to non-aliased addresses', async function () { - await expectRevert( - this.bridge.call(accounts[0], this.accessControl, '$_checkRole(bytes32)', [ROLE]), - `AccessControl: account ${accounts[0].toLowerCase()} is missing role ${crossChainRoleAlias(ROLE)}`, - ); - }); - - it('Crosschain calls not authorized to non-aliased addresses', async function () { - await this.bridge.call(accounts[1], this.accessControl, '$_checkRole(bytes32)', [ROLE]); - }); - }); -}); diff --git a/test/crosschain/CrossChainEnabled.test.js b/test/crosschain/CrossChainEnabled.test.js deleted file mode 100644 index 9e7d26308..000000000 --- a/test/crosschain/CrossChainEnabled.test.js +++ /dev/null @@ -1,78 +0,0 @@ -const { BridgeHelper } = require('../helpers/crosschain'); -const { expectRevertCustomError } = require('../helpers/customError'); - -function randomAddress() { - return web3.utils.toChecksumAddress(web3.utils.randomHex(20)); -} - -const CrossChainEnabledAMBMock = artifacts.require('CrossChainEnabledAMBMock'); -const CrossChainEnabledArbitrumL1Mock = artifacts.require('CrossChainEnabledArbitrumL1Mock'); -const CrossChainEnabledArbitrumL2Mock = artifacts.require('CrossChainEnabledArbitrumL2Mock'); -const CrossChainEnabledOptimismMock = artifacts.require('CrossChainEnabledOptimismMock'); -const CrossChainEnabledPolygonChildMock = artifacts.require('CrossChainEnabledPolygonChildMock'); - -function shouldBehaveLikeReceiver(sender = randomAddress()) { - it('should reject same-chain calls', async function () { - await expectRevertCustomError(this.receiver.crossChainRestricted(), 'NotCrossChainCall()'); - - await expectRevertCustomError(this.receiver.crossChainOwnerRestricted(), 'NotCrossChainCall()'); - }); - - it('should restrict to cross-chain call from a invalid sender', async function () { - await expectRevertCustomError( - this.bridge.call(sender, this.receiver, 'crossChainOwnerRestricted()'), - `InvalidCrossChainSender("${sender}", "${await this.receiver.owner()}")`, - ); - }); - - it('should grant access to cross-chain call from the owner', async function () { - await this.bridge.call(await this.receiver.owner(), this.receiver, 'crossChainOwnerRestricted()'); - }); -} - -contract('CrossChainEnabled', function () { - describe('AMB', function () { - beforeEach(async function () { - this.bridge = await BridgeHelper.deploy('AMB'); - this.receiver = await CrossChainEnabledAMBMock.new(this.bridge.address); - }); - - shouldBehaveLikeReceiver(); - }); - - describe('Arbitrum-L1', function () { - beforeEach(async function () { - this.bridge = await BridgeHelper.deploy('Arbitrum-L1'); - this.receiver = await CrossChainEnabledArbitrumL1Mock.new(this.bridge.address); - }); - - shouldBehaveLikeReceiver(); - }); - - describe('Arbitrum-L2', function () { - beforeEach(async function () { - this.bridge = await BridgeHelper.deploy('Arbitrum-L2'); - this.receiver = await CrossChainEnabledArbitrumL2Mock.new(); - }); - - shouldBehaveLikeReceiver(); - }); - - describe('Optimism', function () { - beforeEach(async function () { - this.bridge = await BridgeHelper.deploy('Optimism'); - this.receiver = await CrossChainEnabledOptimismMock.new(this.bridge.address); - }); - - shouldBehaveLikeReceiver(); - }); - - describe('Polygon-Child', function () { - beforeEach(async function () { - this.bridge = await BridgeHelper.deploy('Polygon-Child'); - this.receiver = await CrossChainEnabledPolygonChildMock.new(this.bridge.address); - }); - - shouldBehaveLikeReceiver(); - }); -}); diff --git a/test/helpers/crosschain.js b/test/helpers/crosschain.js deleted file mode 100644 index 9e6ff9610..000000000 --- a/test/helpers/crosschain.js +++ /dev/null @@ -1,61 +0,0 @@ -const { promisify } = require('util'); - -const BridgeAMBMock = artifacts.require('BridgeAMBMock'); -const BridgeArbitrumL1Mock = artifacts.require('BridgeArbitrumL1Mock'); -const BridgeArbitrumL2Mock = artifacts.require('BridgeArbitrumL2Mock'); -const BridgeOptimismMock = artifacts.require('BridgeOptimismMock'); -const BridgePolygonChildMock = artifacts.require('BridgePolygonChildMock'); - -class BridgeHelper { - static async deploy(type) { - return new BridgeHelper(await deployBridge(type)); - } - - constructor(bridge) { - this.bridge = bridge; - this.address = bridge.address; - } - - call(from, target, selector = undefined, args = []) { - return this.bridge.relayAs( - target.address || target, - selector ? target.contract.methods[selector](...args).encodeABI() : '0x', - from, - ); - } -} - -async function deployBridge(type = 'Arbitrum-L2') { - switch (type) { - case 'AMB': - return BridgeAMBMock.new(); - - case 'Arbitrum-L1': - return BridgeArbitrumL1Mock.new(); - - case 'Arbitrum-L2': { - const instance = await BridgeArbitrumL2Mock.new(); - const code = await web3.eth.getCode(instance.address); - await promisify(web3.currentProvider.send.bind(web3.currentProvider))({ - jsonrpc: '2.0', - method: 'hardhat_setCode', - params: ['0x0000000000000000000000000000000000000064', code], - id: new Date().getTime(), - }); - return BridgeArbitrumL2Mock.at('0x0000000000000000000000000000000000000064'); - } - - case 'Optimism': - return BridgeOptimismMock.new(); - - case 'Polygon-Child': - return BridgePolygonChildMock.new(); - - default: - throw new Error(`CrossChain: ${type} is not supported`); - } -} - -module.exports = { - BridgeHelper, -}; diff --git a/test/security/PullPayment.test.js b/test/security/PullPayment.test.js deleted file mode 100644 index 5bf72bbe6..000000000 --- a/test/security/PullPayment.test.js +++ /dev/null @@ -1,51 +0,0 @@ -const { balance, ether } = require('@openzeppelin/test-helpers'); - -const { expect } = require('chai'); - -const PullPaymentMock = artifacts.require('PullPaymentMock'); - -contract('PullPayment', function (accounts) { - const [payer, payee1, payee2] = accounts; - - const amount = ether('17'); - - beforeEach(async function () { - this.contract = await PullPaymentMock.new({ value: amount }); - }); - - describe('payments', function () { - it('can record an async payment correctly', async function () { - await this.contract.callTransfer(payee1, 100, { from: payer }); - expect(await this.contract.payments(payee1)).to.be.bignumber.equal('100'); - }); - - it('can add multiple balances on one account', async function () { - await this.contract.callTransfer(payee1, 200, { from: payer }); - await this.contract.callTransfer(payee1, 300, { from: payer }); - expect(await this.contract.payments(payee1)).to.be.bignumber.equal('500'); - }); - - it('can add balances on multiple accounts', async function () { - await this.contract.callTransfer(payee1, 200, { from: payer }); - await this.contract.callTransfer(payee2, 300, { from: payer }); - - expect(await this.contract.payments(payee1)).to.be.bignumber.equal('200'); - - expect(await this.contract.payments(payee2)).to.be.bignumber.equal('300'); - }); - }); - - describe('withdrawPayments', function () { - it('can withdraw payment', async function () { - const balanceTracker = await balance.tracker(payee1); - - await this.contract.callTransfer(payee1, amount, { from: payer }); - expect(await this.contract.payments(payee1)).to.be.bignumber.equal(amount); - - await this.contract.withdrawPayments(payee1); - - expect(await balanceTracker.delta()).to.be.bignumber.equal(amount); - expect(await this.contract.payments(payee1)).to.be.bignumber.equal('0'); - }); - }); -}); diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index d4981dd30..04b3b5cb1 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -182,17 +182,6 @@ contract('SafeERC20', function (accounts) { await this.token.$_approve(this.mock.address, spender, 100); }); - it('safeApprove fails to update approval to non-zero', async function () { - await expectRevert( - this.mock.$safeApprove(this.token.address, spender, 200), - 'SafeERC20: approve from non-zero to non-zero allowance', - ); - }); - - it('safeApprove can update approval to zero', async function () { - await this.mock.$safeApprove(this.token.address, spender, 0); - }); - it('safeApprove can increase approval', async function () { await expectRevert(this.mock.$safeIncreaseAllowance(this.token.address, spender, 10), 'USDT approval failure'); }); @@ -217,10 +206,6 @@ function shouldRevertOnAllCalls([receiver, spender], reason) { await expectRevert(this.mock.$safeTransferFrom(this.token.address, this.mock.address, receiver, 0), reason); }); - it('reverts on approve', async function () { - await expectRevert(this.mock.$safeApprove(this.token.address, spender, 0), reason); - }); - it('reverts on increaseAllowance', async function () { // [TODO] make sure it's reverting for the right reason await expectRevert.unspecified(this.mock.$safeIncreaseAllowance(this.token.address, spender, 0)); @@ -269,16 +254,6 @@ function shouldOnlyRevertOnErrors([owner, receiver, spender]) { await this.token.$_approve(this.mock.address, spender, 0); }); - it("doesn't revert when approving a non-zero allowance", async function () { - await this.mock.$safeApprove(this.token.address, spender, 100); - expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('100'); - }); - - it("doesn't revert when approving a zero allowance", async function () { - await this.mock.$safeApprove(this.token.address, spender, 0); - expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0'); - }); - it("doesn't revert when force approving a non-zero allowance", async function () { await this.mock.$forceApprove(this.token.address, spender, 100); expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('100'); @@ -307,18 +282,6 @@ function shouldOnlyRevertOnErrors([owner, receiver, spender]) { await this.token.$_approve(this.mock.address, spender, 100); }); - it('reverts when approving a non-zero allowance', async function () { - await expectRevert( - this.mock.$safeApprove(this.token.address, spender, 20), - 'SafeERC20: approve from non-zero to non-zero allowance', - ); - }); - - it("doesn't revert when approving a zero allowance", async function () { - await this.mock.$safeApprove(this.token.address, spender, 0); - expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0'); - }); - it("doesn't revert when force approving a non-zero allowance", async function () { await this.mock.$forceApprove(this.token.address, spender, 20); expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('20'); diff --git a/test/token/ERC777/ERC777.behavior.js b/test/token/ERC777/ERC777.behavior.js deleted file mode 100644 index b1585bc91..000000000 --- a/test/token/ERC777/ERC777.behavior.js +++ /dev/null @@ -1,597 +0,0 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS } = constants; - -const { expect } = require('chai'); - -const ERC777SenderRecipientMock = artifacts.require('ERC777SenderRecipientMock'); - -function shouldBehaveLikeERC777DirectSendBurn(holder, recipient, data) { - shouldBehaveLikeERC777DirectSend(holder, recipient, data); - shouldBehaveLikeERC777DirectBurn(holder, data); -} - -function shouldBehaveLikeERC777OperatorSendBurn(holder, recipient, operator, data, operatorData) { - shouldBehaveLikeERC777OperatorSend(holder, recipient, operator, data, operatorData); - shouldBehaveLikeERC777OperatorBurn(holder, operator, data, operatorData); -} - -function shouldBehaveLikeERC777UnauthorizedOperatorSendBurn(holder, recipient, operator, data, operatorData) { - shouldBehaveLikeERC777UnauthorizedOperatorSend(holder, recipient, operator, data, operatorData); - shouldBehaveLikeERC777UnauthorizedOperatorBurn(holder, operator, data, operatorData); -} - -function shouldBehaveLikeERC777DirectSend(holder, recipient, data) { - describe('direct send', function () { - context('when the sender has tokens', function () { - shouldDirectSendTokens(holder, recipient, new BN('0'), data); - shouldDirectSendTokens(holder, recipient, new BN('1'), data); - - it('reverts when sending more than the balance', async function () { - const balance = await this.token.balanceOf(holder); - await expectRevert.unspecified(this.token.send(recipient, balance.addn(1), data, { from: holder })); - }); - - it('reverts when sending to the zero address', async function () { - await expectRevert.unspecified(this.token.send(ZERO_ADDRESS, new BN('1'), data, { from: holder })); - }); - }); - - context('when the sender has no tokens', function () { - removeBalance(holder); - - shouldDirectSendTokens(holder, recipient, new BN('0'), data); - - it('reverts when sending a non-zero amount', async function () { - await expectRevert.unspecified(this.token.send(recipient, new BN('1'), data, { from: holder })); - }); - }); - }); -} - -function shouldBehaveLikeERC777OperatorSend(holder, recipient, operator, data, operatorData) { - describe('operator send', function () { - context('when the sender has tokens', async function () { - shouldOperatorSendTokens(holder, operator, recipient, new BN('0'), data, operatorData); - shouldOperatorSendTokens(holder, operator, recipient, new BN('1'), data, operatorData); - - it('reverts when sending more than the balance', async function () { - const balance = await this.token.balanceOf(holder); - await expectRevert.unspecified( - this.token.operatorSend(holder, recipient, balance.addn(1), data, operatorData, { from: operator }), - ); - }); - - it('reverts when sending to the zero address', async function () { - await expectRevert.unspecified( - this.token.operatorSend(holder, ZERO_ADDRESS, new BN('1'), data, operatorData, { from: operator }), - ); - }); - }); - - context('when the sender has no tokens', function () { - removeBalance(holder); - - shouldOperatorSendTokens(holder, operator, recipient, new BN('0'), data, operatorData); - - it('reverts when sending a non-zero amount', async function () { - await expectRevert.unspecified( - this.token.operatorSend(holder, recipient, new BN('1'), data, operatorData, { from: operator }), - ); - }); - - it('reverts when sending from the zero address', async function () { - // This is not yet reflected in the spec - await expectRevert.unspecified( - this.token.operatorSend(ZERO_ADDRESS, recipient, new BN('0'), data, operatorData, { from: operator }), - ); - }); - }); - }); -} - -function shouldBehaveLikeERC777UnauthorizedOperatorSend(holder, recipient, operator, data, operatorData) { - describe('operator send', function () { - it('reverts', async function () { - await expectRevert.unspecified(this.token.operatorSend(holder, recipient, new BN('0'), data, operatorData)); - }); - }); -} - -function shouldBehaveLikeERC777DirectBurn(holder, data) { - describe('direct burn', function () { - context('when the sender has tokens', function () { - shouldDirectBurnTokens(holder, new BN('0'), data); - shouldDirectBurnTokens(holder, new BN('1'), data); - - it('reverts when burning more than the balance', async function () { - const balance = await this.token.balanceOf(holder); - await expectRevert.unspecified(this.token.burn(balance.addn(1), data, { from: holder })); - }); - }); - - context('when the sender has no tokens', function () { - removeBalance(holder); - - shouldDirectBurnTokens(holder, new BN('0'), data); - - it('reverts when burning a non-zero amount', async function () { - await expectRevert.unspecified(this.token.burn(new BN('1'), data, { from: holder })); - }); - }); - }); -} - -function shouldBehaveLikeERC777OperatorBurn(holder, operator, data, operatorData) { - describe('operator burn', function () { - context('when the sender has tokens', async function () { - shouldOperatorBurnTokens(holder, operator, new BN('0'), data, operatorData); - shouldOperatorBurnTokens(holder, operator, new BN('1'), data, operatorData); - - it('reverts when burning more than the balance', async function () { - const balance = await this.token.balanceOf(holder); - await expectRevert.unspecified( - this.token.operatorBurn(holder, balance.addn(1), data, operatorData, { from: operator }), - ); - }); - }); - - context('when the sender has no tokens', function () { - removeBalance(holder); - - shouldOperatorBurnTokens(holder, operator, new BN('0'), data, operatorData); - - it('reverts when burning a non-zero amount', async function () { - await expectRevert.unspecified( - this.token.operatorBurn(holder, new BN('1'), data, operatorData, { from: operator }), - ); - }); - - it('reverts when burning from the zero address', async function () { - // This is not yet reflected in the spec - await expectRevert.unspecified( - this.token.operatorBurn(ZERO_ADDRESS, new BN('0'), data, operatorData, { from: operator }), - ); - }); - }); - }); -} - -function shouldBehaveLikeERC777UnauthorizedOperatorBurn(holder, operator, data, operatorData) { - describe('operator burn', function () { - it('reverts', async function () { - await expectRevert.unspecified(this.token.operatorBurn(holder, new BN('0'), data, operatorData)); - }); - }); -} - -function shouldDirectSendTokens(from, to, amount, data) { - shouldSendTokens(from, null, to, amount, data, null); -} - -function shouldOperatorSendTokens(from, operator, to, amount, data, operatorData) { - shouldSendTokens(from, operator, to, amount, data, operatorData); -} - -function shouldSendTokens(from, operator, to, amount, data, operatorData) { - const operatorCall = operator !== null; - - it(`${operatorCall ? 'operator ' : ''}can send an amount of ${amount}`, async function () { - const initialTotalSupply = await this.token.totalSupply(); - const initialFromBalance = await this.token.balanceOf(from); - const initialToBalance = await this.token.balanceOf(to); - - let receipt; - if (!operatorCall) { - receipt = await this.token.send(to, amount, data, { from }); - expectEvent(receipt, 'Sent', { - operator: from, - from, - to, - amount, - data, - operatorData: null, - }); - } else { - receipt = await this.token.operatorSend(from, to, amount, data, operatorData, { from: operator }); - expectEvent(receipt, 'Sent', { - operator, - from, - to, - amount, - data, - operatorData, - }); - } - - expectEvent(receipt, 'Transfer', { - from, - to, - value: amount, - }); - - const finalTotalSupply = await this.token.totalSupply(); - const finalFromBalance = await this.token.balanceOf(from); - const finalToBalance = await this.token.balanceOf(to); - - expect(finalTotalSupply).to.be.bignumber.equal(initialTotalSupply); - expect(finalToBalance.sub(initialToBalance)).to.be.bignumber.equal(amount); - expect(finalFromBalance.sub(initialFromBalance)).to.be.bignumber.equal(amount.neg()); - }); -} - -function shouldDirectBurnTokens(from, amount, data) { - shouldBurnTokens(from, null, amount, data, null); -} - -function shouldOperatorBurnTokens(from, operator, amount, data, operatorData) { - shouldBurnTokens(from, operator, amount, data, operatorData); -} - -function shouldBurnTokens(from, operator, amount, data, operatorData) { - const operatorCall = operator !== null; - - it(`${operatorCall ? 'operator ' : ''}can burn an amount of ${amount}`, async function () { - const initialTotalSupply = await this.token.totalSupply(); - const initialFromBalance = await this.token.balanceOf(from); - - let receipt; - if (!operatorCall) { - receipt = await this.token.burn(amount, data, { from }); - expectEvent(receipt, 'Burned', { - operator: from, - from, - amount, - data, - operatorData: null, - }); - } else { - receipt = await this.token.operatorBurn(from, amount, data, operatorData, { from: operator }); - expectEvent(receipt, 'Burned', { - operator, - from, - amount, - data, - operatorData, - }); - } - - expectEvent(receipt, 'Transfer', { - from, - to: ZERO_ADDRESS, - value: amount, - }); - - const finalTotalSupply = await this.token.totalSupply(); - const finalFromBalance = await this.token.balanceOf(from); - - expect(finalTotalSupply.sub(initialTotalSupply)).to.be.bignumber.equal(amount.neg()); - expect(finalFromBalance.sub(initialFromBalance)).to.be.bignumber.equal(amount.neg()); - }); -} - -function shouldBehaveLikeERC777InternalMint(recipient, operator, amount, data, operatorData) { - shouldInternalMintTokens(operator, recipient, new BN('0'), data, operatorData); - shouldInternalMintTokens(operator, recipient, amount, data, operatorData); - - it('reverts when minting tokens for the zero address', async function () { - await expectRevert.unspecified( - this.token.$_mint(ZERO_ADDRESS, amount, data, operatorData, true, { from: operator }), - ); - }); -} - -function shouldInternalMintTokens(operator, to, amount, data, operatorData) { - it(`can (internal) mint an amount of ${amount}`, async function () { - const initialTotalSupply = await this.token.totalSupply(); - const initialToBalance = await this.token.balanceOf(to); - - const receipt = await this.token.$_mint(to, amount, data, operatorData, true, { from: operator }); - - expectEvent(receipt, 'Minted', { - operator, - to, - amount, - data, - operatorData, - }); - - expectEvent(receipt, 'Transfer', { - from: ZERO_ADDRESS, - to, - value: amount, - }); - - const finalTotalSupply = await this.token.totalSupply(); - const finalToBalance = await this.token.balanceOf(to); - - expect(finalTotalSupply.sub(initialTotalSupply)).to.be.bignumber.equal(amount); - expect(finalToBalance.sub(initialToBalance)).to.be.bignumber.equal(amount); - }); -} - -function shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData) { - context('when TokensRecipient reverts', function () { - beforeEach(async function () { - await this.tokensRecipientImplementer.setShouldRevertReceive(true); - }); - - it('send reverts', async function () { - await expectRevert.unspecified(sendFromHolder(this.token, this.sender, this.recipient, amount, data)); - }); - - it('operatorSend reverts', async function () { - await expectRevert.unspecified( - this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator }), - ); - }); - - it('mint (internal) reverts', async function () { - await expectRevert.unspecified( - this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }), - ); - }); - }); - - context('when TokensRecipient does not revert', function () { - beforeEach(async function () { - await this.tokensRecipientImplementer.setShouldRevertSend(false); - }); - - it('TokensRecipient receives send data and is called after state mutation', async function () { - const { tx } = await sendFromHolder(this.token, this.sender, this.recipient, amount, data); - - const postSenderBalance = await this.token.balanceOf(this.sender); - const postRecipientBalance = await this.token.balanceOf(this.recipient); - - await assertTokensReceivedCalled( - this.token, - tx, - this.sender, - this.sender, - this.recipient, - amount, - data, - null, - postSenderBalance, - postRecipientBalance, - ); - }); - - it('TokensRecipient receives operatorSend data and is called after state mutation', async function () { - const { tx } = await this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { - from: operator, - }); - - const postSenderBalance = await this.token.balanceOf(this.sender); - const postRecipientBalance = await this.token.balanceOf(this.recipient); - - await assertTokensReceivedCalled( - this.token, - tx, - operator, - this.sender, - this.recipient, - amount, - data, - operatorData, - postSenderBalance, - postRecipientBalance, - ); - }); - - it('TokensRecipient receives mint (internal) data and is called after state mutation', async function () { - const { tx } = await this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }); - - const postRecipientBalance = await this.token.balanceOf(this.recipient); - - await assertTokensReceivedCalled( - this.token, - tx, - operator, - ZERO_ADDRESS, - this.recipient, - amount, - data, - operatorData, - new BN('0'), - postRecipientBalance, - ); - }); - }); -} - -function shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData) { - context('when TokensSender reverts', function () { - beforeEach(async function () { - await this.tokensSenderImplementer.setShouldRevertSend(true); - }); - - it('send reverts', async function () { - await expectRevert.unspecified(sendFromHolder(this.token, this.sender, this.recipient, amount, data)); - }); - - it('operatorSend reverts', async function () { - await expectRevert.unspecified( - this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator }), - ); - }); - - it('burn reverts', async function () { - await expectRevert.unspecified(burnFromHolder(this.token, this.sender, amount, data)); - }); - - it('operatorBurn reverts', async function () { - await expectRevert.unspecified( - this.token.operatorBurn(this.sender, amount, data, operatorData, { from: operator }), - ); - }); - }); - - context('when TokensSender does not revert', function () { - beforeEach(async function () { - await this.tokensSenderImplementer.setShouldRevertSend(false); - }); - - it('TokensSender receives send data and is called before state mutation', async function () { - const preSenderBalance = await this.token.balanceOf(this.sender); - const preRecipientBalance = await this.token.balanceOf(this.recipient); - - const { tx } = await sendFromHolder(this.token, this.sender, this.recipient, amount, data); - - await assertTokensToSendCalled( - this.token, - tx, - this.sender, - this.sender, - this.recipient, - amount, - data, - null, - preSenderBalance, - preRecipientBalance, - ); - }); - - it('TokensSender receives operatorSend data and is called before state mutation', async function () { - const preSenderBalance = await this.token.balanceOf(this.sender); - const preRecipientBalance = await this.token.balanceOf(this.recipient); - - const { tx } = await this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { - from: operator, - }); - - await assertTokensToSendCalled( - this.token, - tx, - operator, - this.sender, - this.recipient, - amount, - data, - operatorData, - preSenderBalance, - preRecipientBalance, - ); - }); - - it('TokensSender receives burn data and is called before state mutation', async function () { - const preSenderBalance = await this.token.balanceOf(this.sender); - - const { tx } = await burnFromHolder(this.token, this.sender, amount, data, { from: this.sender }); - - await assertTokensToSendCalled( - this.token, - tx, - this.sender, - this.sender, - ZERO_ADDRESS, - amount, - data, - null, - preSenderBalance, - ); - }); - - it('TokensSender receives operatorBurn data and is called before state mutation', async function () { - const preSenderBalance = await this.token.balanceOf(this.sender); - - const { tx } = await this.token.operatorBurn(this.sender, amount, data, operatorData, { from: operator }); - - await assertTokensToSendCalled( - this.token, - tx, - operator, - this.sender, - ZERO_ADDRESS, - amount, - data, - operatorData, - preSenderBalance, - ); - }); - }); -} - -function removeBalance(holder) { - beforeEach(async function () { - await this.token.burn(await this.token.balanceOf(holder), '0x', { from: holder }); - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('0'); - }); -} - -async function assertTokensReceivedCalled( - token, - txHash, - operator, - from, - to, - amount, - data, - operatorData, - fromBalance, - toBalance = '0', -) { - await expectEvent.inTransaction(txHash, ERC777SenderRecipientMock, 'TokensReceivedCalled', { - operator, - from, - to, - amount, - data, - operatorData, - token: token.address, - fromBalance, - toBalance, - }); -} - -async function assertTokensToSendCalled( - token, - txHash, - operator, - from, - to, - amount, - data, - operatorData, - fromBalance, - toBalance = '0', -) { - await expectEvent.inTransaction(txHash, ERC777SenderRecipientMock, 'TokensToSendCalled', { - operator, - from, - to, - amount, - data, - operatorData, - token: token.address, - fromBalance, - toBalance, - }); -} - -async function sendFromHolder(token, holder, to, amount, data) { - if ((await web3.eth.getCode(holder)).length <= '0x'.length) { - return token.send(to, amount, data, { from: holder }); - } else { - // assume holder is ERC777SenderRecipientMock contract - return (await ERC777SenderRecipientMock.at(holder)).send(token.address, to, amount, data); - } -} - -async function burnFromHolder(token, holder, amount, data) { - if ((await web3.eth.getCode(holder)).length <= '0x'.length) { - return token.burn(amount, data, { from: holder }); - } else { - // assume holder is ERC777SenderRecipientMock contract - return (await ERC777SenderRecipientMock.at(holder)).burn(token.address, amount, data); - } -} - -module.exports = { - shouldBehaveLikeERC777DirectSendBurn, - shouldBehaveLikeERC777OperatorSendBurn, - shouldBehaveLikeERC777UnauthorizedOperatorSendBurn, - shouldBehaveLikeERC777InternalMint, - shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook, - shouldBehaveLikeERC777SendBurnWithSendHook, -}; diff --git a/test/token/ERC777/ERC777.test.js b/test/token/ERC777/ERC777.test.js deleted file mode 100644 index 44bc25351..000000000 --- a/test/token/ERC777/ERC777.test.js +++ /dev/null @@ -1,556 +0,0 @@ -const { BN, constants, expectEvent, expectRevert, singletons } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS } = constants; - -const { expect } = require('chai'); - -const { - shouldBehaveLikeERC777DirectSendBurn, - shouldBehaveLikeERC777OperatorSendBurn, - shouldBehaveLikeERC777UnauthorizedOperatorSendBurn, - shouldBehaveLikeERC777InternalMint, - shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook, - shouldBehaveLikeERC777SendBurnWithSendHook, -} = require('./ERC777.behavior'); - -const { shouldBehaveLikeERC20, shouldBehaveLikeERC20Approve } = require('../ERC20/ERC20.behavior'); - -const ERC777 = artifacts.require('$ERC777Mock'); -const ERC777SenderRecipientMock = artifacts.require('$ERC777SenderRecipientMock'); - -contract('ERC777', function (accounts) { - const [registryFunder, holder, defaultOperatorA, defaultOperatorB, newOperator, anyone] = accounts; - - const initialSupply = new BN('10000'); - const name = 'ERC777Test'; - const symbol = '777T'; - const data = web3.utils.sha3('OZ777TestData'); - const operatorData = web3.utils.sha3('OZ777TestOperatorData'); - - const defaultOperators = [defaultOperatorA, defaultOperatorB]; - - beforeEach(async function () { - this.erc1820 = await singletons.ERC1820Registry(registryFunder); - }); - - context('with default operators', function () { - beforeEach(async function () { - this.token = await ERC777.new(name, symbol, defaultOperators); - await this.token.$_mint(holder, initialSupply, '0x', '0x'); - }); - - describe('as an ERC20 token', function () { - shouldBehaveLikeERC20('ERC777', initialSupply, holder, anyone, defaultOperatorA); - - describe('_approve', function () { - shouldBehaveLikeERC20Approve('ERC777', holder, anyone, initialSupply, function (owner, spender, amount) { - return this.token.$_approve(owner, spender, amount); - }); - - describe('when the owner is the zero address', function () { - it('reverts', async function () { - await expectRevert( - this.token.$_approve(ZERO_ADDRESS, anyone, initialSupply), - 'ERC777: approve from the zero address', - ); - }); - }); - }); - }); - - it('does not emit AuthorizedOperator events for default operators', async function () { - await expectEvent.notEmitted.inConstruction(this.token, 'AuthorizedOperator'); - }); - - describe('basic information', function () { - it('returns the name', async function () { - expect(await this.token.name()).to.equal(name); - }); - - it('returns the symbol', async function () { - expect(await this.token.symbol()).to.equal(symbol); - }); - - it('returns a granularity of 1', async function () { - expect(await this.token.granularity()).to.be.bignumber.equal('1'); - }); - - it('returns the default operators', async function () { - expect(await this.token.defaultOperators()).to.deep.equal(defaultOperators); - }); - - it('default operators are operators for all accounts', async function () { - for (const operator of defaultOperators) { - expect(await this.token.isOperatorFor(operator, anyone)).to.equal(true); - } - }); - - it('returns the total supply', async function () { - expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply); - }); - - it('returns 18 when decimals is called', async function () { - expect(await this.token.decimals()).to.be.bignumber.equal('18'); - }); - - it('the ERC777Token interface is registered in the registry', async function () { - expect( - await this.erc1820.getInterfaceImplementer(this.token.address, web3.utils.soliditySha3('ERC777Token')), - ).to.equal(this.token.address); - }); - - it('the ERC20Token interface is registered in the registry', async function () { - expect( - await this.erc1820.getInterfaceImplementer(this.token.address, web3.utils.soliditySha3('ERC20Token')), - ).to.equal(this.token.address); - }); - }); - - describe('balanceOf', function () { - context('for an account with no tokens', function () { - it('returns zero', async function () { - expect(await this.token.balanceOf(anyone)).to.be.bignumber.equal('0'); - }); - }); - - context('for an account with tokens', function () { - it('returns their balance', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply); - }); - }); - }); - - context('with no ERC777TokensSender and no ERC777TokensRecipient implementers', function () { - describe('send/burn', function () { - shouldBehaveLikeERC777DirectSendBurn(holder, anyone, data); - - context('with self operator', function () { - shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, holder, data, operatorData); - }); - - context('with first default operator', function () { - shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, defaultOperatorA, data, operatorData); - }); - - context('with second default operator', function () { - shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, defaultOperatorB, data, operatorData); - }); - - context('before authorizing a new operator', function () { - shouldBehaveLikeERC777UnauthorizedOperatorSendBurn(holder, anyone, newOperator, data, operatorData); - }); - - context('with new authorized operator', function () { - beforeEach(async function () { - await this.token.authorizeOperator(newOperator, { from: holder }); - }); - - shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, newOperator, data, operatorData); - - context('with revoked operator', function () { - beforeEach(async function () { - await this.token.revokeOperator(newOperator, { from: holder }); - }); - - shouldBehaveLikeERC777UnauthorizedOperatorSendBurn(holder, anyone, newOperator, data, operatorData); - }); - }); - }); - - describe('mint (internal)', function () { - const to = anyone; - const amount = new BN('5'); - - context('with default operator', function () { - const operator = defaultOperatorA; - - shouldBehaveLikeERC777InternalMint(to, operator, amount, data, operatorData); - }); - - context('with non operator', function () { - const operator = newOperator; - - shouldBehaveLikeERC777InternalMint(to, operator, amount, data, operatorData); - }); - }); - - describe('mint (internal extended)', function () { - const amount = new BN('5'); - - context('to anyone', function () { - beforeEach(async function () { - this.recipient = anyone; - }); - - context('with default operator', function () { - const operator = defaultOperatorA; - - it('without requireReceptionAck', async function () { - await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator }); - }); - - it('with requireReceptionAck', async function () { - await this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }); - }); - }); - - context('with non operator', function () { - const operator = newOperator; - - it('without requireReceptionAck', async function () { - await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator }); - }); - - it('with requireReceptionAck', async function () { - await this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }); - }); - }); - }); - - context('to non ERC777TokensRecipient implementer', function () { - beforeEach(async function () { - this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new(); - this.recipient = this.tokensRecipientImplementer.address; - }); - - context('with default operator', function () { - const operator = defaultOperatorA; - - it('without requireReceptionAck', async function () { - await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator }); - }); - - it('with requireReceptionAck', async function () { - await expectRevert( - this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }), - 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient', - ); - }); - }); - - context('with non operator', function () { - const operator = newOperator; - - it('without requireReceptionAck', async function () { - await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator }); - }); - - it('with requireReceptionAck', async function () { - await expectRevert( - this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }), - 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient', - ); - }); - }); - }); - }); - }); - - describe('operator management', function () { - it('accounts are their own operator', async function () { - expect(await this.token.isOperatorFor(holder, holder)).to.equal(true); - }); - - it('reverts when self-authorizing', async function () { - await expectRevert( - this.token.authorizeOperator(holder, { from: holder }), - 'ERC777: authorizing self as operator', - ); - }); - - it('reverts when self-revoking', async function () { - await expectRevert(this.token.revokeOperator(holder, { from: holder }), 'ERC777: revoking self as operator'); - }); - - it('non-operators can be revoked', async function () { - expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false); - - const receipt = await this.token.revokeOperator(newOperator, { from: holder }); - expectEvent(receipt, 'RevokedOperator', { operator: newOperator, tokenHolder: holder }); - - expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false); - }); - - it('non-operators can be authorized', async function () { - expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false); - - const receipt = await this.token.authorizeOperator(newOperator, { from: holder }); - expectEvent(receipt, 'AuthorizedOperator', { operator: newOperator, tokenHolder: holder }); - - expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(true); - }); - - describe('new operators', function () { - beforeEach(async function () { - await this.token.authorizeOperator(newOperator, { from: holder }); - }); - - it('are not added to the default operators list', async function () { - expect(await this.token.defaultOperators()).to.deep.equal(defaultOperators); - }); - - it('can be re-authorized', async function () { - const receipt = await this.token.authorizeOperator(newOperator, { from: holder }); - expectEvent(receipt, 'AuthorizedOperator', { operator: newOperator, tokenHolder: holder }); - - expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(true); - }); - - it('can be revoked', async function () { - const receipt = await this.token.revokeOperator(newOperator, { from: holder }); - expectEvent(receipt, 'RevokedOperator', { operator: newOperator, tokenHolder: holder }); - - expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false); - }); - }); - - describe('default operators', function () { - it('can be re-authorized', async function () { - const receipt = await this.token.authorizeOperator(defaultOperatorA, { from: holder }); - expectEvent(receipt, 'AuthorizedOperator', { operator: defaultOperatorA, tokenHolder: holder }); - - expect(await this.token.isOperatorFor(defaultOperatorA, holder)).to.equal(true); - }); - - it('can be revoked', async function () { - const receipt = await this.token.revokeOperator(defaultOperatorA, { from: holder }); - expectEvent(receipt, 'RevokedOperator', { operator: defaultOperatorA, tokenHolder: holder }); - - expect(await this.token.isOperatorFor(defaultOperatorA, holder)).to.equal(false); - }); - - it('cannot be revoked for themselves', async function () { - await expectRevert( - this.token.revokeOperator(defaultOperatorA, { from: defaultOperatorA }), - 'ERC777: revoking self as operator', - ); - }); - - context('with revoked default operator', function () { - beforeEach(async function () { - await this.token.revokeOperator(defaultOperatorA, { from: holder }); - }); - - it('default operator is not revoked for other holders', async function () { - expect(await this.token.isOperatorFor(defaultOperatorA, anyone)).to.equal(true); - }); - - it('other default operators are not revoked', async function () { - expect(await this.token.isOperatorFor(defaultOperatorB, holder)).to.equal(true); - }); - - it('default operators list is not modified', async function () { - expect(await this.token.defaultOperators()).to.deep.equal(defaultOperators); - }); - - it('revoked default operator can be re-authorized', async function () { - const receipt = await this.token.authorizeOperator(defaultOperatorA, { from: holder }); - expectEvent(receipt, 'AuthorizedOperator', { operator: defaultOperatorA, tokenHolder: holder }); - - expect(await this.token.isOperatorFor(defaultOperatorA, holder)).to.equal(true); - }); - }); - }); - }); - - describe('send and receive hooks', function () { - const amount = new BN('1'); - const operator = defaultOperatorA; - // sender and recipient are stored inside 'this', since in some tests their addresses are determined dynamically - - describe('tokensReceived', function () { - beforeEach(function () { - this.sender = holder; - }); - - context('with no ERC777TokensRecipient implementer', function () { - context('with contract recipient', function () { - beforeEach(async function () { - this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new(); - this.recipient = this.tokensRecipientImplementer.address; - - // Note that tokensRecipientImplementer doesn't implement the recipient interface for the recipient - }); - - it('send reverts', async function () { - await expectRevert( - this.token.send(this.recipient, amount, data, { from: holder }), - 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient', - ); - }); - - it('operatorSend reverts', async function () { - await expectRevert( - this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator }), - 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient', - ); - }); - - it('mint (internal) reverts', async function () { - await expectRevert( - this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }), - 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient', - ); - }); - - it('(ERC20) transfer succeeds', async function () { - await this.token.transfer(this.recipient, amount, { from: holder }); - }); - - it('(ERC20) transferFrom succeeds', async function () { - const approved = anyone; - await this.token.approve(approved, amount, { from: this.sender }); - await this.token.transferFrom(this.sender, this.recipient, amount, { from: approved }); - }); - }); - }); - - context('with ERC777TokensRecipient implementer', function () { - context('with contract as implementer for an externally owned account', function () { - beforeEach(async function () { - this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new(); - this.recipient = anyone; - - await this.tokensRecipientImplementer.recipientFor(this.recipient); - - await this.erc1820.setInterfaceImplementer( - this.recipient, - web3.utils.soliditySha3('ERC777TokensRecipient'), - this.tokensRecipientImplementer.address, - { from: this.recipient }, - ); - }); - - shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData); - }); - - context('with contract as implementer for another contract', function () { - beforeEach(async function () { - this.recipientContract = await ERC777SenderRecipientMock.new(); - this.recipient = this.recipientContract.address; - - this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new(); - await this.tokensRecipientImplementer.recipientFor(this.recipient); - await this.recipientContract.registerRecipient(this.tokensRecipientImplementer.address); - }); - - shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData); - }); - - context('with contract as implementer for itself', function () { - beforeEach(async function () { - this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new(); - this.recipient = this.tokensRecipientImplementer.address; - - await this.tokensRecipientImplementer.recipientFor(this.recipient); - }); - - shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData); - }); - }); - }); - - describe('tokensToSend', function () { - beforeEach(function () { - this.recipient = anyone; - }); - - context('with a contract as implementer for an externally owned account', function () { - beforeEach(async function () { - this.tokensSenderImplementer = await ERC777SenderRecipientMock.new(); - this.sender = holder; - - await this.tokensSenderImplementer.senderFor(this.sender); - - await this.erc1820.setInterfaceImplementer( - this.sender, - web3.utils.soliditySha3('ERC777TokensSender'), - this.tokensSenderImplementer.address, - { from: this.sender }, - ); - }); - - shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData); - }); - - context('with contract as implementer for another contract', function () { - beforeEach(async function () { - this.senderContract = await ERC777SenderRecipientMock.new(); - this.sender = this.senderContract.address; - - this.tokensSenderImplementer = await ERC777SenderRecipientMock.new(); - await this.tokensSenderImplementer.senderFor(this.sender); - await this.senderContract.registerSender(this.tokensSenderImplementer.address); - - // For the contract to be able to receive tokens (that it can later send), it must also implement the - // recipient interface. - - await this.senderContract.recipientFor(this.sender); - await this.token.send(this.sender, amount, data, { from: holder }); - }); - - shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData); - }); - - context('with a contract as implementer for itself', function () { - beforeEach(async function () { - this.tokensSenderImplementer = await ERC777SenderRecipientMock.new(); - this.sender = this.tokensSenderImplementer.address; - - await this.tokensSenderImplementer.senderFor(this.sender); - - // For the contract to be able to receive tokens (that it can later send), it must also implement the - // recipient interface. - - await this.tokensSenderImplementer.recipientFor(this.sender); - await this.token.send(this.sender, amount, data, { from: holder }); - }); - - shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData); - }); - }); - }); - }); - - context('with no default operators', function () { - beforeEach(async function () { - this.token = await ERC777.new(name, symbol, []); - }); - - it('default operators list is empty', async function () { - expect(await this.token.defaultOperators()).to.deep.equal([]); - }); - }); - - describe('relative order of hooks', function () { - beforeEach(async function () { - await singletons.ERC1820Registry(registryFunder); - this.sender = await ERC777SenderRecipientMock.new(); - await this.sender.registerRecipient(this.sender.address); - await this.sender.registerSender(this.sender.address); - this.token = await ERC777.new(name, symbol, []); - await this.token.$_mint(this.sender.address, 1, '0x', '0x'); - }); - - it('send', async function () { - const { receipt } = await this.sender.send(this.token.address, anyone, 1, '0x'); - - const internalBeforeHook = receipt.logs.findIndex(l => l.event === 'BeforeTokenTransfer'); - expect(internalBeforeHook).to.be.gte(0); - const externalSendHook = receipt.logs.findIndex(l => l.event === 'TokensToSendCalled'); - expect(externalSendHook).to.be.gte(0); - - expect(externalSendHook).to.be.lt(internalBeforeHook); - }); - - it('burn', async function () { - const { receipt } = await this.sender.burn(this.token.address, 1, '0x'); - - const internalBeforeHook = receipt.logs.findIndex(l => l.event === 'BeforeTokenTransfer'); - expect(internalBeforeHook).to.be.gte(0); - const externalSendHook = receipt.logs.findIndex(l => l.event === 'TokensToSendCalled'); - expect(externalSendHook).to.be.gte(0); - - expect(externalSendHook).to.be.lt(internalBeforeHook); - }); - }); -}); diff --git a/test/utils/Checkpoints.t.sol b/test/utils/Checkpoints.t.sol index f2cb587d5..12598da46 100644 --- a/test/utils/Checkpoints.t.sol +++ b/test/utils/Checkpoints.t.sol @@ -7,129 +7,6 @@ import "forge-std/Test.sol"; import "../../contracts/utils/Checkpoints.sol"; import "../../contracts/utils/math/SafeCast.sol"; -contract CheckpointsHistoryTest is Test { - using Checkpoints for Checkpoints.History; - - // Maximum gap between keys used during the fuzzing tests: the `_prepareKeys` function with make sure that - // key#n+1 is in the [key#n, key#n + _KEY_MAX_GAP] range. - uint8 internal constant _KEY_MAX_GAP = 64; - - Checkpoints.History internal _ckpts; - - // helpers - function _boundUint32(uint32 x, uint32 min, uint32 max) internal view returns (uint32) { - return SafeCast.toUint32(bound(uint256(x), uint256(min), uint256(max))); - } - - function _prepareKeys(uint32[] memory keys, uint32 maxSpread) internal view { - uint32 lastKey = 0; - for (uint256 i = 0; i < keys.length; ++i) { - uint32 key = _boundUint32(keys[i], lastKey, lastKey + maxSpread); - keys[i] = key; - lastKey = key; - } - } - - function _assertLatestCheckpoint(bool exist, uint32 key, uint224 value) internal { - (bool _exist, uint32 _key, uint224 _value) = _ckpts.latestCheckpoint(); - assertEq(_exist, exist); - assertEq(_key, key); - assertEq(_value, value); - } - - // tests - function testPush(uint32[] memory keys, uint224[] memory values, uint32 pastKey) public { - vm.assume(values.length > 0 && values.length <= keys.length); - _prepareKeys(keys, _KEY_MAX_GAP); - - // initial state - assertEq(_ckpts.length(), 0); - assertEq(_ckpts.latest(), 0); - _assertLatestCheckpoint(false, 0, 0); - - uint256 duplicates = 0; - for (uint256 i = 0; i < keys.length; ++i) { - uint32 key = keys[i]; - uint224 value = values[i % values.length]; - if (i > 0 && key == keys[i - 1]) ++duplicates; - - // push - vm.roll(key); - _ckpts.push(value); - - // check length & latest - assertEq(_ckpts.length(), i + 1 - duplicates); - assertEq(_ckpts.latest(), value); - _assertLatestCheckpoint(true, key, value); - } - - // Can't push any key in the past - if (keys.length > 0) { - uint32 lastKey = keys[keys.length - 1]; - if (lastKey > 0) { - pastKey = _boundUint32(pastKey, 0, lastKey - 1); - - vm.roll(pastKey); - vm.expectRevert(); - this.push(values[keys.length % values.length]); - } - } - } - - // used to test reverts - function push(uint224 value) external { - _ckpts.push(value); - } - - function testLookup(uint32[] memory keys, uint224[] memory values, uint32 lookup) public { - vm.assume(keys.length > 0); - vm.assume(values.length > 0 && values.length <= keys.length); - _prepareKeys(keys, _KEY_MAX_GAP); - - uint32 lastKey = keys[keys.length - 1]; - vm.assume(lastKey > 0); - lookup = _boundUint32(lookup, 0, lastKey - 1); - - uint224 upper = 0; - for (uint256 i = 0; i < keys.length; ++i) { - uint32 key = keys[i]; - uint224 value = values[i % values.length]; - - // push - vm.roll(key); - _ckpts.push(value); - - // track expected result of lookups - if (key <= lookup) { - upper = value; - } - } - - // check lookup - assertEq(_ckpts.getAtBlock(lookup), upper); - assertEq(_ckpts.getAtProbablyRecentBlock(lookup), upper); - - vm.expectRevert(); - this.getAtBlock(lastKey); - vm.expectRevert(); - this.getAtBlock(lastKey + 1); - vm.expectRevert(); - this.getAtProbablyRecentBlock(lastKey); - vm.expectRevert(); - this.getAtProbablyRecentBlock(lastKey + 1); - } - - // used to test reverts - function getAtBlock(uint32 key) external view { - _ckpts.getAtBlock(key); - } - - // used to test reverts - function getAtProbablyRecentBlock(uint32 key) external view { - _ckpts.getAtProbablyRecentBlock(key); - } -} - contract CheckpointsTrace224Test is Test { using Checkpoints for Checkpoints.Trace224; diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js index f395663be..d9fd2c8ea 100644 --- a/test/utils/Checkpoints.test.js +++ b/test/utils/Checkpoints.test.js @@ -1,166 +1,51 @@ -const { expectRevert, time } = require('@openzeppelin/test-helpers'); - +const { expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); -const { batchInBlock } = require('../helpers/txpool'); +const { VALUE_SIZES } = require('../../scripts/generate/templates/Checkpoints.opts.js'); const $Checkpoints = artifacts.require('$Checkpoints'); // The library name may be 'Checkpoints' or 'CheckpointsUpgradeable' const libraryName = $Checkpoints._json.contractName.replace(/^\$/, ''); -const traceLengths = [160, 224]; - const first = array => (array.length ? array[0] : undefined); const last = array => (array.length ? array[array.length - 1] : undefined); contract('Checkpoints', function () { beforeEach(async function () { this.mock = await $Checkpoints.new(); - - this.methods = { trace: {} }; - this.methods.history = { - latest: (...args) => this.mock.methods[`$latest_${libraryName}_History(uint256)`](0, ...args), - latestCheckpoint: (...args) => this.mock.methods[`$latestCheckpoint_${libraryName}_History(uint256)`](0, ...args), - length: (...args) => this.mock.methods[`$length_${libraryName}_History(uint256)`](0, ...args), - push: (...args) => this.mock.methods['$push(uint256,uint256)'](0, ...args), - getAtBlock: (...args) => this.mock.$getAtBlock(0, ...args), - getAtRecentBlock: (...args) => this.mock.$getAtProbablyRecentBlock(0, ...args), - }; - for (const length of traceLengths) { - this.methods.trace[length] = { - latest: (...args) => this.mock.methods[`$latest_${libraryName}_Trace${length}(uint256)`](0, ...args), - latestCheckpoint: (...args) => - this.mock.methods[`$latestCheckpoint_${libraryName}_Trace${length}(uint256)`](0, ...args), - length: (...args) => this.mock.methods[`$length_${libraryName}_Trace${length}(uint256)`](0, ...args), - push: (...args) => this.mock.methods[`$push(uint256,uint${256 - length},uint${length})`](0, ...args), - lowerLookup: (...args) => this.mock.methods[`$lowerLookup(uint256,uint${256 - length})`](0, ...args), - upperLookup: (...args) => this.mock.methods[`$upperLookup(uint256,uint${256 - length})`](0, ...args), - upperLookupRecent: (...args) => - this.mock.methods[`$upperLookupRecent(uint256,uint${256 - length})`](0, ...args), - }; - } }); - describe('History checkpoints', function () { - describe('without checkpoints', function () { - it('returns zero as latest value', async function () { - expect(await this.methods.history.latest()).to.be.bignumber.equal('0'); - - const ckpt = await this.methods.history.latestCheckpoint(); - expect(ckpt[0]).to.be.equal(false); - expect(ckpt[1]).to.be.bignumber.equal('0'); - expect(ckpt[2]).to.be.bignumber.equal('0'); - }); - - it('returns zero as past value', async function () { - await time.advanceBlock(); - expect(await this.methods.history.getAtBlock((await web3.eth.getBlockNumber()) - 1)).to.be.bignumber.equal('0'); - expect( - await this.methods.history.getAtRecentBlock((await web3.eth.getBlockNumber()) - 1), - ).to.be.bignumber.equal('0'); - }); - }); - - describe('with checkpoints', function () { - beforeEach('pushing checkpoints', async function () { - this.tx1 = await this.methods.history.push(1); - this.tx2 = await this.methods.history.push(2); - await time.advanceBlock(); - this.tx3 = await this.methods.history.push(3); - await time.advanceBlock(); - await time.advanceBlock(); - }); - - it('returns latest value', async function () { - expect(await this.methods.history.latest()).to.be.bignumber.equal('3'); - - const ckpt = await this.methods.history.latestCheckpoint(); - expect(ckpt[0]).to.be.equal(true); - expect(ckpt[1]).to.be.bignumber.equal(web3.utils.toBN(this.tx3.receipt.blockNumber)); - expect(ckpt[2]).to.be.bignumber.equal(web3.utils.toBN('3')); - }); - - for (const getAtBlockVariant of ['getAtBlock', 'getAtRecentBlock']) { - describe(`lookup: ${getAtBlockVariant}`, function () { - it('returns past values', async function () { - expect( - await this.methods.history[getAtBlockVariant](this.tx1.receipt.blockNumber - 1), - ).to.be.bignumber.equal('0'); - expect(await this.methods.history[getAtBlockVariant](this.tx1.receipt.blockNumber)).to.be.bignumber.equal( - '1', - ); - expect(await this.methods.history[getAtBlockVariant](this.tx2.receipt.blockNumber)).to.be.bignumber.equal( - '2', - ); - // Block with no new checkpoints - expect( - await this.methods.history[getAtBlockVariant](this.tx2.receipt.blockNumber + 1), - ).to.be.bignumber.equal('2'); - expect(await this.methods.history[getAtBlockVariant](this.tx3.receipt.blockNumber)).to.be.bignumber.equal( - '3', - ); - expect( - await this.methods.history[getAtBlockVariant](this.tx3.receipt.blockNumber + 1), - ).to.be.bignumber.equal('3'); - }); - it('reverts if block number >= current block', async function () { - await expectRevert( - this.methods.history[getAtBlockVariant](await web3.eth.getBlockNumber()), - 'Checkpoints: block not yet mined', - ); - - await expectRevert( - this.methods.history[getAtBlockVariant]((await web3.eth.getBlockNumber()) + 1), - 'Checkpoints: block not yet mined', - ); - }); - }); - } - - it('multiple checkpoints in the same block', async function () { - const lengthBefore = await this.methods.history.length(); - - await batchInBlock([ - () => this.methods.history.push(8, { gas: 100000 }), - () => this.methods.history.push(9, { gas: 100000 }), - () => this.methods.history.push(10, { gas: 100000 }), - ]); - - expect(await this.methods.history.length()).to.be.bignumber.equal(lengthBefore.addn(1)); - expect(await this.methods.history.latest()).to.be.bignumber.equal('10'); - }); - - it('more than 5 checkpoints', async function () { - for (let i = 4; i <= 6; i++) { - await this.methods.history.push(i); - } - expect(await this.methods.history.length()).to.be.bignumber.equal('6'); - const block = await web3.eth.getBlockNumber(); - // recent - expect(await this.methods.history.getAtRecentBlock(block - 1)).to.be.bignumber.equal('5'); - // non-recent - expect(await this.methods.history.getAtRecentBlock(block - 9)).to.be.bignumber.equal('0'); - }); - }); - }); - - for (const length of traceLengths) { + for (const length of VALUE_SIZES) { describe(`Trace${length}`, function () { + beforeEach(async function () { + this.methods = { + latest: (...args) => this.mock.methods[`$latest_${libraryName}_Trace${length}(uint256)`](0, ...args), + latestCheckpoint: (...args) => + this.mock.methods[`$latestCheckpoint_${libraryName}_Trace${length}(uint256)`](0, ...args), + length: (...args) => this.mock.methods[`$length_${libraryName}_Trace${length}(uint256)`](0, ...args), + push: (...args) => this.mock.methods[`$push(uint256,uint${256 - length},uint${length})`](0, ...args), + lowerLookup: (...args) => this.mock.methods[`$lowerLookup(uint256,uint${256 - length})`](0, ...args), + upperLookup: (...args) => this.mock.methods[`$upperLookup(uint256,uint${256 - length})`](0, ...args), + upperLookupRecent: (...args) => + this.mock.methods[`$upperLookupRecent(uint256,uint${256 - length})`](0, ...args), + }; + }); + describe('without checkpoints', function () { it('returns zero as latest value', async function () { - expect(await this.methods.trace[length].latest()).to.be.bignumber.equal('0'); + expect(await this.methods.latest()).to.be.bignumber.equal('0'); - const ckpt = await this.methods.trace[length].latestCheckpoint(); + const ckpt = await this.methods.latestCheckpoint(); expect(ckpt[0]).to.be.equal(false); expect(ckpt[1]).to.be.bignumber.equal('0'); expect(ckpt[2]).to.be.bignumber.equal('0'); }); it('lookup returns 0', async function () { - expect(await this.methods.trace[length].lowerLookup(0)).to.be.bignumber.equal('0'); - expect(await this.methods.trace[length].upperLookup(0)).to.be.bignumber.equal('0'); - expect(await this.methods.trace[length].upperLookupRecent(0)).to.be.bignumber.equal('0'); + expect(await this.methods.lowerLookup(0)).to.be.bignumber.equal('0'); + expect(await this.methods.upperLookup(0)).to.be.bignumber.equal('0'); + expect(await this.methods.upperLookupRecent(0)).to.be.bignumber.equal('0'); }); }); @@ -174,49 +59,46 @@ contract('Checkpoints', function () { { key: '11', value: '99' }, ]; for (const { key, value } of this.checkpoints) { - await this.methods.trace[length].push(key, value); + await this.methods.push(key, value); } }); it('length', async function () { - expect(await this.methods.trace[length].length()).to.be.bignumber.equal(this.checkpoints.length.toString()); + expect(await this.methods.length()).to.be.bignumber.equal(this.checkpoints.length.toString()); }); it('returns latest value', async function () { - expect(await this.methods.trace[length].latest()).to.be.bignumber.equal(last(this.checkpoints).value); + expect(await this.methods.latest()).to.be.bignumber.equal(last(this.checkpoints).value); - const ckpt = await this.methods.trace[length].latestCheckpoint(); + const ckpt = await this.methods.latestCheckpoint(); expect(ckpt[0]).to.be.equal(true); expect(ckpt[1]).to.be.bignumber.equal(last(this.checkpoints).key); expect(ckpt[2]).to.be.bignumber.equal(last(this.checkpoints).value); }); it('cannot push values in the past', async function () { - await expectRevert( - this.methods.trace[length].push(last(this.checkpoints).key - 1, '0'), - 'Checkpoint: decreasing keys', - ); + await expectRevert(this.methods.push(last(this.checkpoints).key - 1, '0'), 'Checkpoint: decreasing keys'); }); it('can update last value', async function () { const newValue = '42'; // check length before the update - expect(await this.methods.trace[length].length()).to.be.bignumber.equal(this.checkpoints.length.toString()); + expect(await this.methods.length()).to.be.bignumber.equal(this.checkpoints.length.toString()); // update last key - await this.methods.trace[length].push(last(this.checkpoints).key, newValue); - expect(await this.methods.trace[length].latest()).to.be.bignumber.equal(newValue); + await this.methods.push(last(this.checkpoints).key, newValue); + expect(await this.methods.latest()).to.be.bignumber.equal(newValue); // check that length did not change - expect(await this.methods.trace[length].length()).to.be.bignumber.equal(this.checkpoints.length.toString()); + expect(await this.methods.length()).to.be.bignumber.equal(this.checkpoints.length.toString()); }); it('lower lookup', async function () { for (let i = 0; i < 14; ++i) { const value = first(this.checkpoints.filter(x => i <= x.key))?.value || '0'; - expect(await this.methods.trace[length].lowerLookup(i)).to.be.bignumber.equal(value); + expect(await this.methods.lowerLookup(i)).to.be.bignumber.equal(value); } }); @@ -224,8 +106,8 @@ contract('Checkpoints', function () { for (let i = 0; i < 14; ++i) { const value = last(this.checkpoints.filter(x => i >= x.key))?.value || '0'; - expect(await this.methods.trace[length].upperLookup(i)).to.be.bignumber.equal(value); - expect(await this.methods.trace[length].upperLookupRecent(i)).to.be.bignumber.equal(value); + expect(await this.methods.upperLookup(i)).to.be.bignumber.equal(value); + expect(await this.methods.upperLookupRecent(i)).to.be.bignumber.equal(value); } }); @@ -240,13 +122,13 @@ contract('Checkpoints', function () { const allCheckpoints = [].concat(this.checkpoints, moreCheckpoints); for (const { key, value } of moreCheckpoints) { - await this.methods.trace[length].push(key, value); + await this.methods.push(key, value); } for (let i = 0; i < 25; ++i) { const value = last(allCheckpoints.filter(x => i >= x.key))?.value || '0'; - expect(await this.methods.trace[length].upperLookup(i)).to.be.bignumber.equal(value); - expect(await this.methods.trace[length].upperLookupRecent(i)).to.be.bignumber.equal(value); + expect(await this.methods.upperLookup(i)).to.be.bignumber.equal(value); + expect(await this.methods.upperLookupRecent(i)).to.be.bignumber.equal(value); } }); }); diff --git a/test/utils/Create2.test.js b/test/utils/Create2.test.js index 2fc27dc15..526602600 100644 --- a/test/utils/Create2.test.js +++ b/test/utils/Create2.test.js @@ -4,7 +4,10 @@ const { expect } = require('chai'); const Create2 = artifacts.require('$Create2'); const VestingWallet = artifacts.require('VestingWallet'); -const ERC1820Implementer = artifacts.require('$ERC1820Implementer'); +// This should be a contract that: +// - has no constructor arguments +// - has no immutable variable populated during construction +const ConstructorLessContract = Create2; contract('Create2', function (accounts) { const [deployerAccount, other] = accounts; @@ -38,14 +41,14 @@ contract('Create2', function (accounts) { }); describe('deploy', function () { - it('deploys a ERC1820Implementer from inline assembly code', async function () { - const offChainComputed = computeCreate2Address(saltHex, ERC1820Implementer.bytecode, this.factory.address); + it('deploys a contract without constructor', async function () { + const offChainComputed = computeCreate2Address(saltHex, ConstructorLessContract.bytecode, this.factory.address); - expectEvent(await this.factory.$deploy(0, saltHex, ERC1820Implementer.bytecode), 'return$deploy', { + expectEvent(await this.factory.$deploy(0, saltHex, ConstructorLessContract.bytecode), 'return$deploy', { addr: offChainComputed, }); - expect(ERC1820Implementer.bytecode).to.include((await web3.eth.getCode(offChainComputed)).slice(2)); + expect(ConstructorLessContract.bytecode).to.include((await web3.eth.getCode(offChainComputed)).slice(2)); }); it('deploys a contract with constructor arguments', async function () { diff --git a/test/utils/TimersBlockNumberImpl.test.js b/test/utils/TimersBlockNumberImpl.test.js deleted file mode 100644 index ee0d54bfb..000000000 --- a/test/utils/TimersBlockNumberImpl.test.js +++ /dev/null @@ -1,55 +0,0 @@ -const { BN, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); - -const TimersBlockNumberImpl = artifacts.require('TimersBlockNumberImpl'); - -contract('TimersBlockNumber', function () { - beforeEach(async function () { - this.instance = await TimersBlockNumberImpl.new(); - this.now = await web3.eth.getBlock('latest').then(({ number }) => number); - }); - - it('unset', async function () { - expect(await this.instance.getDeadline()).to.be.bignumber.equal('0'); - expect(await this.instance.isUnset()).to.be.equal(true); - expect(await this.instance.isStarted()).to.be.equal(false); - expect(await this.instance.isPending()).to.be.equal(false); - expect(await this.instance.isExpired()).to.be.equal(false); - }); - - it('pending', async function () { - await this.instance.setDeadline(this.now + 3); - expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now + 3)); - expect(await this.instance.isUnset()).to.be.equal(false); - expect(await this.instance.isStarted()).to.be.equal(true); - expect(await this.instance.isPending()).to.be.equal(true); - expect(await this.instance.isExpired()).to.be.equal(false); - }); - - it('expired', async function () { - await this.instance.setDeadline(this.now - 3); - expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now - 3)); - expect(await this.instance.isUnset()).to.be.equal(false); - expect(await this.instance.isStarted()).to.be.equal(true); - expect(await this.instance.isPending()).to.be.equal(false); - expect(await this.instance.isExpired()).to.be.equal(true); - }); - - it('reset', async function () { - await this.instance.reset(); - expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(0)); - expect(await this.instance.isUnset()).to.be.equal(true); - expect(await this.instance.isStarted()).to.be.equal(false); - expect(await this.instance.isPending()).to.be.equal(false); - expect(await this.instance.isExpired()).to.be.equal(false); - }); - - it('fast forward', async function () { - await this.instance.setDeadline(this.now + 3); - expect(await this.instance.isPending()).to.be.equal(true); - expect(await this.instance.isExpired()).to.be.equal(false); - await time.advanceBlockTo(this.now + 3); - expect(await this.instance.isPending()).to.be.equal(false); - expect(await this.instance.isExpired()).to.be.equal(true); - }); -}); diff --git a/test/utils/TimersTimestamp.test.js b/test/utils/TimersTimestamp.test.js deleted file mode 100644 index fc32de7d0..000000000 --- a/test/utils/TimersTimestamp.test.js +++ /dev/null @@ -1,55 +0,0 @@ -const { BN, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); - -const TimersTimestampImpl = artifacts.require('TimersTimestampImpl'); - -contract('TimersTimestamp', function () { - beforeEach(async function () { - this.instance = await TimersTimestampImpl.new(); - this.now = await web3.eth.getBlock('latest').then(({ timestamp }) => timestamp); - }); - - it('unset', async function () { - expect(await this.instance.getDeadline()).to.be.bignumber.equal('0'); - expect(await this.instance.isUnset()).to.be.equal(true); - expect(await this.instance.isStarted()).to.be.equal(false); - expect(await this.instance.isPending()).to.be.equal(false); - expect(await this.instance.isExpired()).to.be.equal(false); - }); - - it('pending', async function () { - await this.instance.setDeadline(this.now + 100); - expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now + 100)); - expect(await this.instance.isUnset()).to.be.equal(false); - expect(await this.instance.isStarted()).to.be.equal(true); - expect(await this.instance.isPending()).to.be.equal(true); - expect(await this.instance.isExpired()).to.be.equal(false); - }); - - it('expired', async function () { - await this.instance.setDeadline(this.now - 100); - expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now - 100)); - expect(await this.instance.isUnset()).to.be.equal(false); - expect(await this.instance.isStarted()).to.be.equal(true); - expect(await this.instance.isPending()).to.be.equal(false); - expect(await this.instance.isExpired()).to.be.equal(true); - }); - - it('reset', async function () { - await this.instance.reset(); - expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(0)); - expect(await this.instance.isUnset()).to.be.equal(true); - expect(await this.instance.isStarted()).to.be.equal(false); - expect(await this.instance.isPending()).to.be.equal(false); - expect(await this.instance.isExpired()).to.be.equal(false); - }); - - it('fast forward', async function () { - await this.instance.setDeadline(this.now + 100); - expect(await this.instance.isPending()).to.be.equal(true); - expect(await this.instance.isExpired()).to.be.equal(false); - await time.increaseTo(this.now + 100); - expect(await this.instance.isPending()).to.be.equal(false); - expect(await this.instance.isExpired()).to.be.equal(true); - }); -}); diff --git a/test/utils/escrow/ConditionalEscrow.test.js b/test/utils/escrow/ConditionalEscrow.test.js deleted file mode 100644 index 9e17a8bce..000000000 --- a/test/utils/escrow/ConditionalEscrow.test.js +++ /dev/null @@ -1,37 +0,0 @@ -const { ether, expectRevert } = require('@openzeppelin/test-helpers'); -const { shouldBehaveLikeEscrow } = require('./Escrow.behavior'); - -const ConditionalEscrowMock = artifacts.require('ConditionalEscrowMock'); - -contract('ConditionalEscrow', function (accounts) { - const [owner, payee, ...otherAccounts] = accounts; - - beforeEach(async function () { - this.escrow = await ConditionalEscrowMock.new({ from: owner }); - }); - - context('when withdrawal is allowed', function () { - beforeEach(async function () { - await Promise.all(otherAccounts.map(payee => this.escrow.setAllowed(payee, true))); - }); - - shouldBehaveLikeEscrow(owner, otherAccounts); - }); - - context('when withdrawal is disallowed', function () { - const amount = ether('23'); - - beforeEach(async function () { - await this.escrow.setAllowed(payee, false); - }); - - it('reverts on withdrawals', async function () { - await this.escrow.deposit(payee, { from: owner, value: amount }); - - await expectRevert( - this.escrow.withdraw(payee, { from: owner }), - 'ConditionalEscrow: payee is not allowed to withdraw', - ); - }); - }); -}); diff --git a/test/utils/escrow/Escrow.behavior.js b/test/utils/escrow/Escrow.behavior.js deleted file mode 100644 index f2059ed62..000000000 --- a/test/utils/escrow/Escrow.behavior.js +++ /dev/null @@ -1,90 +0,0 @@ -const { balance, ether, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); - -const { expect } = require('chai'); - -function shouldBehaveLikeEscrow(owner, [payee1, payee2]) { - const amount = ether('42'); - - describe('as an escrow', function () { - describe('deposits', function () { - it('can accept a single deposit', async function () { - await this.escrow.deposit(payee1, { from: owner, value: amount }); - - expect(await balance.current(this.escrow.address)).to.be.bignumber.equal(amount); - - expect(await this.escrow.depositsOf(payee1)).to.be.bignumber.equal(amount); - }); - - it('can accept an empty deposit', async function () { - await this.escrow.deposit(payee1, { from: owner, value: 0 }); - }); - - it('only the owner can deposit', async function () { - await expectRevert(this.escrow.deposit(payee1, { from: payee2 }), 'Ownable: caller is not the owner'); - }); - - it('emits a deposited event', async function () { - const receipt = await this.escrow.deposit(payee1, { from: owner, value: amount }); - expectEvent(receipt, 'Deposited', { - payee: payee1, - weiAmount: amount, - }); - }); - - it('can add multiple deposits on a single account', async function () { - await this.escrow.deposit(payee1, { from: owner, value: amount }); - await this.escrow.deposit(payee1, { from: owner, value: amount.muln(2) }); - - expect(await balance.current(this.escrow.address)).to.be.bignumber.equal(amount.muln(3)); - - expect(await this.escrow.depositsOf(payee1)).to.be.bignumber.equal(amount.muln(3)); - }); - - it('can track deposits to multiple accounts', async function () { - await this.escrow.deposit(payee1, { from: owner, value: amount }); - await this.escrow.deposit(payee2, { from: owner, value: amount.muln(2) }); - - expect(await balance.current(this.escrow.address)).to.be.bignumber.equal(amount.muln(3)); - - expect(await this.escrow.depositsOf(payee1)).to.be.bignumber.equal(amount); - - expect(await this.escrow.depositsOf(payee2)).to.be.bignumber.equal(amount.muln(2)); - }); - }); - - describe('withdrawals', async function () { - it('can withdraw payments', async function () { - const balanceTracker = await balance.tracker(payee1); - - await this.escrow.deposit(payee1, { from: owner, value: amount }); - await this.escrow.withdraw(payee1, { from: owner }); - - expect(await balanceTracker.delta()).to.be.bignumber.equal(amount); - - expect(await balance.current(this.escrow.address)).to.be.bignumber.equal('0'); - expect(await this.escrow.depositsOf(payee1)).to.be.bignumber.equal('0'); - }); - - it('can do an empty withdrawal', async function () { - await this.escrow.withdraw(payee1, { from: owner }); - }); - - it('only the owner can withdraw', async function () { - await expectRevert(this.escrow.withdraw(payee1, { from: payee1 }), 'Ownable: caller is not the owner'); - }); - - it('emits a withdrawn event', async function () { - await this.escrow.deposit(payee1, { from: owner, value: amount }); - const receipt = await this.escrow.withdraw(payee1, { from: owner }); - expectEvent(receipt, 'Withdrawn', { - payee: payee1, - weiAmount: amount, - }); - }); - }); - }); -} - -module.exports = { - shouldBehaveLikeEscrow, -}; diff --git a/test/utils/escrow/Escrow.test.js b/test/utils/escrow/Escrow.test.js deleted file mode 100644 index 47627904b..000000000 --- a/test/utils/escrow/Escrow.test.js +++ /dev/null @@ -1,14 +0,0 @@ -require('@openzeppelin/test-helpers'); -const { shouldBehaveLikeEscrow } = require('./Escrow.behavior'); - -const Escrow = artifacts.require('Escrow'); - -contract('Escrow', function (accounts) { - const [owner, ...otherAccounts] = accounts; - - beforeEach(async function () { - this.escrow = await Escrow.new({ from: owner }); - }); - - shouldBehaveLikeEscrow(owner, otherAccounts); -}); diff --git a/test/utils/escrow/RefundEscrow.test.js b/test/utils/escrow/RefundEscrow.test.js deleted file mode 100644 index c5344db16..000000000 --- a/test/utils/escrow/RefundEscrow.test.js +++ /dev/null @@ -1,143 +0,0 @@ -const { balance, constants, ether, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS } = constants; - -const { expect } = require('chai'); - -const RefundEscrow = artifacts.require('RefundEscrow'); - -contract('RefundEscrow', function (accounts) { - const [owner, beneficiary, refundee1, refundee2] = accounts; - - const amount = ether('54'); - const refundees = [refundee1, refundee2]; - - it('requires a non-null beneficiary', async function () { - await expectRevert( - RefundEscrow.new(ZERO_ADDRESS, { from: owner }), - 'RefundEscrow: beneficiary is the zero address', - ); - }); - - context('once deployed', function () { - beforeEach(async function () { - this.escrow = await RefundEscrow.new(beneficiary, { from: owner }); - }); - - context('active state', function () { - it('has beneficiary and state', async function () { - expect(await this.escrow.beneficiary()).to.equal(beneficiary); - expect(await this.escrow.state()).to.be.bignumber.equal('0'); - }); - - it('accepts deposits', async function () { - await this.escrow.deposit(refundee1, { from: owner, value: amount }); - - expect(await this.escrow.depositsOf(refundee1)).to.be.bignumber.equal(amount); - }); - - it('does not refund refundees', async function () { - await this.escrow.deposit(refundee1, { from: owner, value: amount }); - await expectRevert(this.escrow.withdraw(refundee1), 'ConditionalEscrow: payee is not allowed to withdraw'); - }); - - it('does not allow beneficiary withdrawal', async function () { - await this.escrow.deposit(refundee1, { from: owner, value: amount }); - await expectRevert( - this.escrow.beneficiaryWithdraw(), - 'RefundEscrow: beneficiary can only withdraw while closed', - ); - }); - }); - - it('only the owner can enter closed state', async function () { - await expectRevert(this.escrow.close({ from: beneficiary }), 'Ownable: caller is not the owner'); - - const receipt = await this.escrow.close({ from: owner }); - expectEvent(receipt, 'RefundsClosed'); - }); - - context('closed state', function () { - beforeEach(async function () { - await Promise.all(refundees.map(refundee => this.escrow.deposit(refundee, { from: owner, value: amount }))); - - await this.escrow.close({ from: owner }); - }); - - it('rejects deposits', async function () { - await expectRevert( - this.escrow.deposit(refundee1, { from: owner, value: amount }), - 'RefundEscrow: can only deposit while active', - ); - }); - - it('does not refund refundees', async function () { - await expectRevert(this.escrow.withdraw(refundee1), 'ConditionalEscrow: payee is not allowed to withdraw'); - }); - - it('allows beneficiary withdrawal', async function () { - const balanceTracker = await balance.tracker(beneficiary); - await this.escrow.beneficiaryWithdraw(); - expect(await balanceTracker.delta()).to.be.bignumber.equal(amount.muln(refundees.length)); - }); - - it('prevents entering the refund state', async function () { - await expectRevert( - this.escrow.enableRefunds({ from: owner }), - 'RefundEscrow: can only enable refunds while active', - ); - }); - - it('prevents re-entering the closed state', async function () { - await expectRevert(this.escrow.close({ from: owner }), 'RefundEscrow: can only close while active'); - }); - }); - - it('only the owner can enter refund state', async function () { - await expectRevert(this.escrow.enableRefunds({ from: beneficiary }), 'Ownable: caller is not the owner'); - - const receipt = await this.escrow.enableRefunds({ from: owner }); - expectEvent(receipt, 'RefundsEnabled'); - }); - - context('refund state', function () { - beforeEach(async function () { - await Promise.all(refundees.map(refundee => this.escrow.deposit(refundee, { from: owner, value: amount }))); - - await this.escrow.enableRefunds({ from: owner }); - }); - - it('rejects deposits', async function () { - await expectRevert( - this.escrow.deposit(refundee1, { from: owner, value: amount }), - 'RefundEscrow: can only deposit while active', - ); - }); - - it('refunds refundees', async function () { - for (const refundee of [refundee1, refundee2]) { - const balanceTracker = await balance.tracker(refundee); - await this.escrow.withdraw(refundee, { from: owner }); - expect(await balanceTracker.delta()).to.be.bignumber.equal(amount); - } - }); - - it('does not allow beneficiary withdrawal', async function () { - await expectRevert( - this.escrow.beneficiaryWithdraw(), - 'RefundEscrow: beneficiary can only withdraw while closed', - ); - }); - - it('prevents entering the closed state', async function () { - await expectRevert(this.escrow.close({ from: owner }), 'RefundEscrow: can only close while active'); - }); - - it('prevents re-entering the refund state', async function () { - await expectRevert( - this.escrow.enableRefunds({ from: owner }), - 'RefundEscrow: can only enable refunds while active', - ); - }); - }); - }); -}); diff --git a/test/utils/introspection/ERC1820Implementer.test.js b/test/utils/introspection/ERC1820Implementer.test.js deleted file mode 100644 index 83d0a6534..000000000 --- a/test/utils/introspection/ERC1820Implementer.test.js +++ /dev/null @@ -1,71 +0,0 @@ -const { expectRevert, singletons } = require('@openzeppelin/test-helpers'); -const { bufferToHex, keccakFromString } = require('ethereumjs-util'); - -const { expect } = require('chai'); - -const ERC1820Implementer = artifacts.require('$ERC1820Implementer'); - -contract('ERC1820Implementer', function (accounts) { - const [registryFunder, implementee, other] = accounts; - - const ERC1820_ACCEPT_MAGIC = bufferToHex(keccakFromString('ERC1820_ACCEPT_MAGIC')); - - beforeEach(async function () { - this.implementer = await ERC1820Implementer.new(); - this.registry = await singletons.ERC1820Registry(registryFunder); - - this.interfaceA = bufferToHex(keccakFromString('interfaceA')); - this.interfaceB = bufferToHex(keccakFromString('interfaceB')); - }); - - context('with no registered interfaces', function () { - it('returns false when interface implementation is queried', async function () { - expect(await this.implementer.canImplementInterfaceForAddress(this.interfaceA, implementee)).to.not.equal( - ERC1820_ACCEPT_MAGIC, - ); - }); - - it('reverts when attempting to set as implementer in the registry', async function () { - await expectRevert( - this.registry.setInterfaceImplementer(implementee, this.interfaceA, this.implementer.address, { - from: implementee, - }), - 'Does not implement the interface', - ); - }); - }); - - context('with registered interfaces', function () { - beforeEach(async function () { - await this.implementer.$_registerInterfaceForAddress(this.interfaceA, implementee); - }); - - it('returns true when interface implementation is queried', async function () { - expect(await this.implementer.canImplementInterfaceForAddress(this.interfaceA, implementee)).to.equal( - ERC1820_ACCEPT_MAGIC, - ); - }); - - it('returns false when interface implementation for non-supported interfaces is queried', async function () { - expect(await this.implementer.canImplementInterfaceForAddress(this.interfaceB, implementee)).to.not.equal( - ERC1820_ACCEPT_MAGIC, - ); - }); - - it('returns false when interface implementation for non-supported addresses is queried', async function () { - expect(await this.implementer.canImplementInterfaceForAddress(this.interfaceA, other)).to.not.equal( - ERC1820_ACCEPT_MAGIC, - ); - }); - - it('can be set as an implementer for supported interfaces in the registry', async function () { - await this.registry.setInterfaceImplementer(implementee, this.interfaceA, this.implementer.address, { - from: implementee, - }); - - expect(await this.registry.getInterfaceImplementer(implementee, this.interfaceA)).to.equal( - this.implementer.address, - ); - }); - }); -}); diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index 916136cdd..9e7e118df 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -5,7 +5,6 @@ pragma solidity ^0.8.0; import "forge-std/Test.sol"; import "../../../contracts/utils/math/Math.sol"; -import "../../../contracts/utils/math/SafeMath.sol"; contract MathTest is Test { // CEILDIV @@ -47,7 +46,7 @@ contract MathTest is Test { } function _squareBigger(uint256 value, uint256 ref) private pure returns (bool) { - (bool noOverflow, uint256 square) = SafeMath.tryMul(value, value); + (bool noOverflow, uint256 square) = Math.tryMul(value, value); return !noOverflow || square > ref; } diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 6b97a646f..df459c5f8 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -5,6 +5,21 @@ const { Rounding } = require('../../helpers/enums.js'); const Math = artifacts.require('$Math'); +function expectStruct(value, expected) { + for (const key in expected) { + if (BN.isBN(value[key])) { + expect(value[key]).to.be.bignumber.equal(expected[key]); + } else { + expect(value[key]).to.be.equal(expected[key]); + } + } +} + +async function testCommutativeIterable(fn, lhs, rhs, expected, ...extra) { + expectStruct(await fn(lhs, rhs, ...extra), expected); + expectStruct(await fn(rhs, lhs, ...extra), expected); +} + contract('Math', function () { const min = new BN('1234'); const max = new BN('5678'); @@ -15,6 +30,130 @@ contract('Math', function () { this.math = await Math.new(); }); + describe('tryAdd', function () { + it('adds correctly', async function () { + const a = new BN('5678'); + const b = new BN('1234'); + + await testCommutativeIterable(this.math.$tryAdd, a, b, [true, a.add(b)]); + }); + + it('reverts on addition overflow', async function () { + const a = MAX_UINT256; + const b = new BN('1'); + + await testCommutativeIterable(this.math.$tryAdd, a, b, [false, '0']); + }); + }); + + describe('trySub', function () { + it('subtracts correctly', async function () { + const a = new BN('5678'); + const b = new BN('1234'); + + expectStruct(await this.math.$trySub(a, b), [true, a.sub(b)]); + }); + + it('reverts if subtraction result would be negative', async function () { + const a = new BN('1234'); + const b = new BN('5678'); + + expectStruct(await this.math.$trySub(a, b), [false, '0']); + }); + }); + + describe('tryMul', function () { + it('multiplies correctly', async function () { + const a = new BN('1234'); + const b = new BN('5678'); + + await testCommutativeIterable(this.math.$tryMul, a, b, [true, a.mul(b)]); + }); + + it('multiplies by zero correctly', async function () { + const a = new BN('0'); + const b = new BN('5678'); + + await testCommutativeIterable(this.math.$tryMul, a, b, [true, a.mul(b)]); + }); + + it('reverts on multiplication overflow', async function () { + const a = MAX_UINT256; + const b = new BN('2'); + + await testCommutativeIterable(this.math.$tryMul, a, b, [false, '0']); + }); + }); + + describe('tryDiv', function () { + it('divides correctly', async function () { + const a = new BN('5678'); + const b = new BN('5678'); + + expectStruct(await this.math.$tryDiv(a, b), [true, a.div(b)]); + }); + + it('divides zero correctly', async function () { + const a = new BN('0'); + const b = new BN('5678'); + + expectStruct(await this.math.$tryDiv(a, b), [true, a.div(b)]); + }); + + it('returns complete number result on non-even division', async function () { + const a = new BN('7000'); + const b = new BN('5678'); + + expectStruct(await this.math.$tryDiv(a, b), [true, a.div(b)]); + }); + + it('reverts on division by zero', async function () { + const a = new BN('5678'); + const b = new BN('0'); + + expectStruct(await this.math.$tryDiv(a, b), [false, '0']); + }); + }); + + describe('tryMod', function () { + describe('modulos correctly', async function () { + it('when the dividend is smaller than the divisor', async function () { + const a = new BN('284'); + const b = new BN('5678'); + + expectStruct(await this.math.$tryMod(a, b), [true, a.mod(b)]); + }); + + it('when the dividend is equal to the divisor', async function () { + const a = new BN('5678'); + const b = new BN('5678'); + + expectStruct(await this.math.$tryMod(a, b), [true, a.mod(b)]); + }); + + it('when the dividend is larger than the divisor', async function () { + const a = new BN('7000'); + const b = new BN('5678'); + + expectStruct(await this.math.$tryMod(a, b), [true, a.mod(b)]); + }); + + it('when the dividend is a multiple of the divisor', async function () { + const a = new BN('17034'); // 17034 == 5678 * 3 + const b = new BN('5678'); + + expectStruct(await this.math.$tryMod(a, b), [true, a.mod(b)]); + }); + }); + + it('reverts with a 0 divisor', async function () { + const a = new BN('5678'); + const b = new BN('0'); + + expectStruct(await this.math.$tryMod(a, b), [false, '0']); + }); + }); + describe('max', function () { it('is correctly detected in first argument position', async function () { expect(await this.math.$max(max, min)).to.be.bignumber.equal(max); diff --git a/test/utils/math/SafeMath.test.js b/test/utils/math/SafeMath.test.js deleted file mode 100644 index 9598ac57c..000000000 --- a/test/utils/math/SafeMath.test.js +++ /dev/null @@ -1,433 +0,0 @@ -const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); -const { MAX_UINT256 } = constants; - -const { expect } = require('chai'); - -const SafeMath = artifacts.require('$SafeMath'); -const SafeMathMemoryCheck = artifacts.require('$SafeMathMemoryCheck'); - -function expectStruct(value, expected) { - for (const key in expected) { - if (BN.isBN(value[key])) { - expect(value[key]).to.be.bignumber.equal(expected[key]); - } else { - expect(value[key]).to.be.equal(expected[key]); - } - } -} - -contract('SafeMath', function () { - beforeEach(async function () { - this.safeMath = await SafeMath.new(); - }); - - async function testCommutative(fn, lhs, rhs, expected, ...extra) { - expect(await fn(lhs, rhs, ...extra)).to.be.bignumber.equal(expected); - expect(await fn(rhs, lhs, ...extra)).to.be.bignumber.equal(expected); - } - - async function testFailsCommutative(fn, lhs, rhs, reason, ...extra) { - if (reason === undefined) { - await expectRevert.unspecified(fn(lhs, rhs, ...extra)); - await expectRevert.unspecified(fn(rhs, lhs, ...extra)); - } else { - await expectRevert(fn(lhs, rhs, ...extra), reason); - await expectRevert(fn(rhs, lhs, ...extra), reason); - } - } - - async function testCommutativeIterable(fn, lhs, rhs, expected, ...extra) { - expectStruct(await fn(lhs, rhs, ...extra), expected); - expectStruct(await fn(rhs, lhs, ...extra), expected); - } - - describe('with flag', function () { - describe('add', function () { - it('adds correctly', async function () { - const a = new BN('5678'); - const b = new BN('1234'); - - testCommutativeIterable(this.safeMath.$tryAdd, a, b, [true, a.add(b)]); - }); - - it('reverts on addition overflow', async function () { - const a = MAX_UINT256; - const b = new BN('1'); - - testCommutativeIterable(this.safeMath.$tryAdd, a, b, [false, '0']); - }); - }); - - describe('sub', function () { - it('subtracts correctly', async function () { - const a = new BN('5678'); - const b = new BN('1234'); - - expectStruct(await this.safeMath.$trySub(a, b), [true, a.sub(b)]); - }); - - it('reverts if subtraction result would be negative', async function () { - const a = new BN('1234'); - const b = new BN('5678'); - - expectStruct(await this.safeMath.$trySub(a, b), [false, '0']); - }); - }); - - describe('mul', function () { - it('multiplies correctly', async function () { - const a = new BN('1234'); - const b = new BN('5678'); - - testCommutativeIterable(this.safeMath.$tryMul, a, b, [true, a.mul(b)]); - }); - - it('multiplies by zero correctly', async function () { - const a = new BN('0'); - const b = new BN('5678'); - - testCommutativeIterable(this.safeMath.$tryMul, a, b, [true, a.mul(b)]); - }); - - it('reverts on multiplication overflow', async function () { - const a = MAX_UINT256; - const b = new BN('2'); - - testCommutativeIterable(this.safeMath.$tryMul, a, b, [false, '0']); - }); - }); - - describe('div', function () { - it('divides correctly', async function () { - const a = new BN('5678'); - const b = new BN('5678'); - - expectStruct(await this.safeMath.$tryDiv(a, b), [true, a.div(b)]); - }); - - it('divides zero correctly', async function () { - const a = new BN('0'); - const b = new BN('5678'); - - expectStruct(await this.safeMath.$tryDiv(a, b), [true, a.div(b)]); - }); - - it('returns complete number result on non-even division', async function () { - const a = new BN('7000'); - const b = new BN('5678'); - - expectStruct(await this.safeMath.$tryDiv(a, b), [true, a.div(b)]); - }); - - it('reverts on division by zero', async function () { - const a = new BN('5678'); - const b = new BN('0'); - - expectStruct(await this.safeMath.$tryDiv(a, b), [false, '0']); - }); - }); - - describe('mod', function () { - describe('modulos correctly', async function () { - it('when the dividend is smaller than the divisor', async function () { - const a = new BN('284'); - const b = new BN('5678'); - - expectStruct(await this.safeMath.$tryMod(a, b), [true, a.mod(b)]); - }); - - it('when the dividend is equal to the divisor', async function () { - const a = new BN('5678'); - const b = new BN('5678'); - - expectStruct(await this.safeMath.$tryMod(a, b), [true, a.mod(b)]); - }); - - it('when the dividend is larger than the divisor', async function () { - const a = new BN('7000'); - const b = new BN('5678'); - - expectStruct(await this.safeMath.$tryMod(a, b), [true, a.mod(b)]); - }); - - it('when the dividend is a multiple of the divisor', async function () { - const a = new BN('17034'); // 17034 == 5678 * 3 - const b = new BN('5678'); - - expectStruct(await this.safeMath.$tryMod(a, b), [true, a.mod(b)]); - }); - }); - - it('reverts with a 0 divisor', async function () { - const a = new BN('5678'); - const b = new BN('0'); - - expectStruct(await this.safeMath.$tryMod(a, b), [false, '0']); - }); - }); - }); - - describe('with default revert message', function () { - describe('add', function () { - it('adds correctly', async function () { - const a = new BN('5678'); - const b = new BN('1234'); - - await testCommutative(this.safeMath.$add, a, b, a.add(b)); - }); - - it('reverts on addition overflow', async function () { - const a = MAX_UINT256; - const b = new BN('1'); - - await testFailsCommutative(this.safeMath.$add, a, b, undefined); - }); - }); - - describe('sub', function () { - it('subtracts correctly', async function () { - const a = new BN('5678'); - const b = new BN('1234'); - - expect(await this.safeMath.$sub(a, b)).to.be.bignumber.equal(a.sub(b)); - }); - - it('reverts if subtraction result would be negative', async function () { - const a = new BN('1234'); - const b = new BN('5678'); - - await expectRevert.unspecified(this.safeMath.$sub(a, b)); - }); - }); - - describe('mul', function () { - it('multiplies correctly', async function () { - const a = new BN('1234'); - const b = new BN('5678'); - - await testCommutative(this.safeMath.$mul, a, b, a.mul(b)); - }); - - it('multiplies by zero correctly', async function () { - const a = new BN('0'); - const b = new BN('5678'); - - await testCommutative(this.safeMath.$mul, a, b, '0'); - }); - - it('reverts on multiplication overflow', async function () { - const a = MAX_UINT256; - const b = new BN('2'); - - await testFailsCommutative(this.safeMath.$mul, a, b, undefined); - }); - }); - - describe('div', function () { - it('divides correctly', async function () { - const a = new BN('5678'); - const b = new BN('5678'); - - expect(await this.safeMath.$div(a, b)).to.be.bignumber.equal(a.div(b)); - }); - - it('divides zero correctly', async function () { - const a = new BN('0'); - const b = new BN('5678'); - - expect(await this.safeMath.$div(a, b)).to.be.bignumber.equal('0'); - }); - - it('returns complete number result on non-even division', async function () { - const a = new BN('7000'); - const b = new BN('5678'); - - expect(await this.safeMath.$div(a, b)).to.be.bignumber.equal('1'); - }); - - it('reverts on division by zero', async function () { - const a = new BN('5678'); - const b = new BN('0'); - - await expectRevert.unspecified(this.safeMath.$div(a, b)); - }); - }); - - describe('mod', function () { - describe('modulos correctly', async function () { - it('when the dividend is smaller than the divisor', async function () { - const a = new BN('284'); - const b = new BN('5678'); - - expect(await this.safeMath.$mod(a, b)).to.be.bignumber.equal(a.mod(b)); - }); - - it('when the dividend is equal to the divisor', async function () { - const a = new BN('5678'); - const b = new BN('5678'); - - expect(await this.safeMath.$mod(a, b)).to.be.bignumber.equal(a.mod(b)); - }); - - it('when the dividend is larger than the divisor', async function () { - const a = new BN('7000'); - const b = new BN('5678'); - - expect(await this.safeMath.$mod(a, b)).to.be.bignumber.equal(a.mod(b)); - }); - - it('when the dividend is a multiple of the divisor', async function () { - const a = new BN('17034'); // 17034 == 5678 * 3 - const b = new BN('5678'); - - expect(await this.safeMath.$mod(a, b)).to.be.bignumber.equal(a.mod(b)); - }); - }); - - it('reverts with a 0 divisor', async function () { - const a = new BN('5678'); - const b = new BN('0'); - - await expectRevert.unspecified(this.safeMath.$mod(a, b)); - }); - }); - }); - - describe('with custom revert message', function () { - describe('sub', function () { - it('subtracts correctly', async function () { - const a = new BN('5678'); - const b = new BN('1234'); - - expect( - await this.safeMath.methods['$sub(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - ).to.be.bignumber.equal(a.sub(b)); - }); - - it('reverts if subtraction result would be negative', async function () { - const a = new BN('1234'); - const b = new BN('5678'); - - await expectRevert( - this.safeMath.methods['$sub(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - 'MyErrorMessage', - ); - }); - }); - - describe('div', function () { - it('divides correctly', async function () { - const a = new BN('5678'); - const b = new BN('5678'); - - expect( - await this.safeMath.methods['$div(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - ).to.be.bignumber.equal(a.div(b)); - }); - - it('divides zero correctly', async function () { - const a = new BN('0'); - const b = new BN('5678'); - - expect( - await this.safeMath.methods['$div(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - ).to.be.bignumber.equal('0'); - }); - - it('returns complete number result on non-even division', async function () { - const a = new BN('7000'); - const b = new BN('5678'); - - expect( - await this.safeMath.methods['$div(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - ).to.be.bignumber.equal('1'); - }); - - it('reverts on division by zero', async function () { - const a = new BN('5678'); - const b = new BN('0'); - - await expectRevert( - this.safeMath.methods['$div(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - 'MyErrorMessage', - ); - }); - }); - - describe('mod', function () { - describe('modulos correctly', async function () { - it('when the dividend is smaller than the divisor', async function () { - const a = new BN('284'); - const b = new BN('5678'); - - expect( - await this.safeMath.methods['$mod(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - ).to.be.bignumber.equal(a.mod(b)); - }); - - it('when the dividend is equal to the divisor', async function () { - const a = new BN('5678'); - const b = new BN('5678'); - - expect( - await this.safeMath.methods['$mod(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - ).to.be.bignumber.equal(a.mod(b)); - }); - - it('when the dividend is larger than the divisor', async function () { - const a = new BN('7000'); - const b = new BN('5678'); - - expect( - await this.safeMath.methods['$mod(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - ).to.be.bignumber.equal(a.mod(b)); - }); - - it('when the dividend is a multiple of the divisor', async function () { - const a = new BN('17034'); // 17034 == 5678 * 3 - const b = new BN('5678'); - - expect( - await this.safeMath.methods['$mod(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - ).to.be.bignumber.equal(a.mod(b)); - }); - }); - - it('reverts with a 0 divisor', async function () { - const a = new BN('5678'); - const b = new BN('0'); - - await expectRevert( - this.safeMath.methods['$mod(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - 'MyErrorMessage', - ); - }); - }); - }); - - describe('memory leakage', function () { - beforeEach(async function () { - this.safeMathMemoryCheck = await SafeMathMemoryCheck.new(); - }); - - it('add', async function () { - expect(await this.safeMathMemoryCheck.$addMemoryCheck()).to.be.bignumber.equal('0'); - }); - - it('sub', async function () { - expect(await this.safeMathMemoryCheck.$subMemoryCheck()).to.be.bignumber.equal('0'); - }); - - it('mul', async function () { - expect(await this.safeMathMemoryCheck.$mulMemoryCheck()).to.be.bignumber.equal('0'); - }); - - it('div', async function () { - expect(await this.safeMathMemoryCheck.$divMemoryCheck()).to.be.bignumber.equal('0'); - }); - - it('mod', async function () { - expect(await this.safeMathMemoryCheck.$modMemoryCheck()).to.be.bignumber.equal('0'); - }); - }); -}); diff --git a/test/utils/math/SignedSafeMath.test.js b/test/utils/math/SignedSafeMath.test.js deleted file mode 100644 index 3b6530cf5..000000000 --- a/test/utils/math/SignedSafeMath.test.js +++ /dev/null @@ -1,152 +0,0 @@ -const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); -const { MAX_INT256, MIN_INT256 } = constants; - -const { expect } = require('chai'); - -const SignedSafeMath = artifacts.require('$SignedSafeMath'); - -contract('SignedSafeMath', function () { - beforeEach(async function () { - this.safeMath = await SignedSafeMath.new(); - }); - - async function testCommutative(fn, lhs, rhs, expected) { - expect(await fn(lhs, rhs)).to.be.bignumber.equal(expected); - expect(await fn(rhs, lhs)).to.be.bignumber.equal(expected); - } - - async function testFailsCommutative(fn, lhs, rhs) { - await expectRevert.unspecified(fn(lhs, rhs)); - await expectRevert.unspecified(fn(rhs, lhs)); - } - - describe('add', function () { - it('adds correctly if it does not overflow and the result is positive', async function () { - const a = new BN('1234'); - const b = new BN('5678'); - - await testCommutative(this.safeMath.$add, a, b, a.add(b)); - }); - - it('adds correctly if it does not overflow and the result is negative', async function () { - const a = MAX_INT256; - const b = MIN_INT256; - - await testCommutative(this.safeMath.$add, a, b, a.add(b)); - }); - - it('reverts on positive addition overflow', async function () { - const a = MAX_INT256; - const b = new BN('1'); - - await testFailsCommutative(this.safeMath.$add, a, b); - }); - - it('reverts on negative addition overflow', async function () { - const a = MIN_INT256; - const b = new BN('-1'); - - await testFailsCommutative(this.safeMath.$add, a, b); - }); - }); - - describe('sub', function () { - it('subtracts correctly if it does not overflow and the result is positive', async function () { - const a = new BN('5678'); - const b = new BN('1234'); - - const result = await this.safeMath.$sub(a, b); - expect(result).to.be.bignumber.equal(a.sub(b)); - }); - - it('subtracts correctly if it does not overflow and the result is negative', async function () { - const a = new BN('1234'); - const b = new BN('5678'); - - const result = await this.safeMath.$sub(a, b); - expect(result).to.be.bignumber.equal(a.sub(b)); - }); - - it('reverts on positive subtraction overflow', async function () { - const a = MAX_INT256; - const b = new BN('-1'); - - await expectRevert.unspecified(this.safeMath.$sub(a, b)); - }); - - it('reverts on negative subtraction overflow', async function () { - const a = MIN_INT256; - const b = new BN('1'); - - await expectRevert.unspecified(this.safeMath.$sub(a, b)); - }); - }); - - describe('mul', function () { - it('multiplies correctly', async function () { - const a = new BN('5678'); - const b = new BN('-1234'); - - await testCommutative(this.safeMath.$mul, a, b, a.mul(b)); - }); - - it('multiplies by zero correctly', async function () { - const a = new BN('0'); - const b = new BN('5678'); - - await testCommutative(this.safeMath.$mul, a, b, '0'); - }); - - it('reverts on multiplication overflow, positive operands', async function () { - const a = MAX_INT256; - const b = new BN('2'); - - await testFailsCommutative(this.safeMath.$mul, a, b); - }); - - it('reverts when minimum integer is multiplied by -1', async function () { - const a = MIN_INT256; - const b = new BN('-1'); - - await testFailsCommutative(this.safeMath.$mul, a, b); - }); - }); - - describe('div', function () { - it('divides correctly', async function () { - const a = new BN('-5678'); - const b = new BN('5678'); - - const result = await this.safeMath.$div(a, b); - expect(result).to.be.bignumber.equal(a.div(b)); - }); - - it('divides zero correctly', async function () { - const a = new BN('0'); - const b = new BN('5678'); - - expect(await this.safeMath.$div(a, b)).to.be.bignumber.equal('0'); - }); - - it('returns complete number result on non-even division', async function () { - const a = new BN('7000'); - const b = new BN('5678'); - - expect(await this.safeMath.$div(a, b)).to.be.bignumber.equal('1'); - }); - - it('reverts on division by zero', async function () { - const a = new BN('-5678'); - const b = new BN('0'); - - await expectRevert.unspecified(this.safeMath.$div(a, b)); - }); - - it('reverts on overflow, negative second', async function () { - const a = new BN(MIN_INT256); - const b = new BN('-1'); - - await expectRevert.unspecified(this.safeMath.$div(a, b)); - }); - }); -}); diff --git a/test/utils/structs/EnumerableMap.behavior.js b/test/utils/structs/EnumerableMap.behavior.js index b4b5ee372..3db45df6a 100644 --- a/test/utils/structs/EnumerableMap.behavior.js +++ b/test/utils/structs/EnumerableMap.behavior.js @@ -154,17 +154,6 @@ function shouldBehaveLikeMap(keys, values, zeroValue, methods, events) { }); }); - describe('get with message', function () { - it('existing value', async function () { - expect(await methods.getWithMessage(this.map, keyA, 'custom error string').then(r => r.toString())).to.be.equal( - valueA.toString(), - ); - }); - it('missing value', async function () { - await expectRevert(methods.getWithMessage(this.map, keyB, 'custom error string'), 'custom error string'); - }); - }); - describe('tryGet', function () { it('existing value', async function () { const result = await methods.tryGet(this.map, keyA); diff --git a/test/utils/structs/EnumerableMap.test.js b/test/utils/structs/EnumerableMap.test.js index f540ec99e..8861fa731 100644 --- a/test/utils/structs/EnumerableMap.test.js +++ b/test/utils/structs/EnumerableMap.test.js @@ -41,7 +41,6 @@ contract('EnumerableMap', function (accounts) { getMethods({ set: '$set(uint256,address,uint256)', get: '$get(uint256,address)', - getWithMessage: '$get(uint256,address,string)', tryGet: '$tryGet(uint256,address)', remove: '$remove(uint256,address)', length: `$length_${library}_AddressToUintMap(uint256)`, @@ -65,7 +64,6 @@ contract('EnumerableMap', function (accounts) { getMethods({ set: '$set(uint256,uint256,address)', get: `$get_${library}_UintToAddressMap(uint256,uint256)`, - getWithMessage: `$get_${library}_UintToAddressMap(uint256,uint256,string)`, tryGet: `$tryGet_${library}_UintToAddressMap(uint256,uint256)`, remove: `$remove_${library}_UintToAddressMap(uint256,uint256)`, length: `$length_${library}_UintToAddressMap(uint256)`, @@ -89,7 +87,6 @@ contract('EnumerableMap', function (accounts) { getMethods({ set: '$set(uint256,bytes32,bytes32)', get: `$get_${library}_Bytes32ToBytes32Map(uint256,bytes32)`, - getWithMessage: `$get_${library}_Bytes32ToBytes32Map(uint256,bytes32,string)`, tryGet: `$tryGet_${library}_Bytes32ToBytes32Map(uint256,bytes32)`, remove: `$remove_${library}_Bytes32ToBytes32Map(uint256,bytes32)`, length: `$length_${library}_Bytes32ToBytes32Map(uint256)`, @@ -113,7 +110,6 @@ contract('EnumerableMap', function (accounts) { getMethods({ set: '$set(uint256,uint256,uint256)', get: `$get_${library}_UintToUintMap(uint256,uint256)`, - getWithMessage: `$get_${library}_UintToUintMap(uint256,uint256,string)`, tryGet: `$tryGet_${library}_UintToUintMap(uint256,uint256)`, remove: `$remove_${library}_UintToUintMap(uint256,uint256)`, length: `$length_${library}_UintToUintMap(uint256)`, @@ -137,7 +133,6 @@ contract('EnumerableMap', function (accounts) { getMethods({ set: '$set(uint256,bytes32,uint256)', get: `$get_${library}_Bytes32ToUintMap(uint256,bytes32)`, - getWithMessage: `$get_${library}_Bytes32ToUintMap(uint256,bytes32,string)`, tryGet: `$tryGet_${library}_Bytes32ToUintMap(uint256,bytes32)`, remove: `$remove_${library}_Bytes32ToUintMap(uint256,bytes32)`, length: `$length_${library}_Bytes32ToUintMap(uint256)`, From dfef6a68ee18dbd2e1f5a099061a3b8a0e404485 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 19 May 2023 23:49:23 +0200 Subject: [PATCH 137/197] Fix error when running hardhat test with parameters (#4265) --- hardhat/task-test-get-files.js | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/hardhat/task-test-get-files.js b/hardhat/task-test-get-files.js index 896bf1274..108f40a42 100644 --- a/hardhat/task-test-get-files.js +++ b/hardhat/task-test-get-files.js @@ -3,33 +3,23 @@ const { TASK_TEST_GET_TEST_FILES } = require('hardhat/builtin-tasks/task-names') // Modifies `hardhat test` to skip the proxy tests after proxies are removed by the transpiler for upgradeability. -internalTask(TASK_TEST_GET_TEST_FILES).setAction(async ({ testFiles }, { config }) => { - if (testFiles.length !== 0) { - return testFiles; - } - - const globAsync = require('glob'); +internalTask(TASK_TEST_GET_TEST_FILES).setAction(async (args, hre, runSuper) => { const path = require('path'); const { promises: fs } = require('fs'); - const { promisify } = require('util'); - - const glob = promisify(globAsync); const hasProxies = await fs - .access(path.join(config.paths.sources, 'proxy/Proxy.sol')) + .access(path.join(hre.config.paths.sources, 'proxy/Proxy.sol')) .then(() => true) .catch(() => false); - return await glob(path.join(config.paths.tests, '**/*.js'), { - ignore: hasProxies - ? [] - : [ - 'proxy/beacon/BeaconProxy.test.js', - 'proxy/beacon/UpgradeableBeacon.test.js', - 'proxy/ERC1967/ERC1967Proxy.test.js', - 'proxy/transparent/ProxyAdmin.test.js', - 'proxy/transparent/TransparentUpgradeableProxy.test.js', - 'proxy/utils/UUPSUpgradeable.test.js', - ].map(p => path.join(config.paths.tests, p)), - }); + const ignoredIfProxy = [ + 'proxy/beacon/BeaconProxy.test.js', + 'proxy/beacon/UpgradeableBeacon.test.js', + 'proxy/ERC1967/ERC1967Proxy.test.js', + 'proxy/transparent/ProxyAdmin.test.js', + 'proxy/transparent/TransparentUpgradeableProxy.test.js', + 'proxy/utils/UUPSUpgradeable.test.js', + ].map(p => path.join(hre.config.paths.tests, p)); + + return (await runSuper(args)).filter(file => hasProxies || !ignoredIfProxy.includes(file)); }); From a1d57bac505eab080f7b5009cfd5f98dd2e779a2 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 23 May 2023 20:54:34 +0200 Subject: [PATCH 138/197] Improve FV specifications for AccessControlDefaultAdminRules (#4223) Co-authored-by: ernestognw Co-authored-by: Francisco --- .../specs/AccessControlDefaultAdminRules.spec | 244 ++++++++---------- certora/specs/helpers/helpers.spec | 9 + 2 files changed, 113 insertions(+), 140 deletions(-) diff --git a/certora/specs/AccessControlDefaultAdminRules.spec b/certora/specs/AccessControlDefaultAdminRules.spec index a4baa1871..58b9d1202 100644 --- a/certora/specs/AccessControlDefaultAdminRules.spec +++ b/certora/specs/AccessControlDefaultAdminRules.spec @@ -12,44 +12,23 @@ use rule onlyGrantCanGrant filtered { │ Helpers │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ +definition timeSanity(env e) returns bool = + e.block.timestamp > 0 && e.block.timestamp + defaultAdminDelay(e) < max_uint48(); -function max_uint48() returns mathint { - return (1 << 48) - 1; -} +definition delayChangeWaitSanity(env e, uint48 newDelay) returns bool = + e.block.timestamp + delayChangeWait_(e, newDelay) < max_uint48(); -function nonZeroAccount(address account) returns bool { - return account != 0; -} +definition isSet(uint48 schedule) returns bool = + schedule != 0; -function timeSanity(env e) returns bool { - return - e.block.timestamp > 0 && // Avoids 0 schedules - e.block.timestamp + defaultAdminDelay(e) < max_uint48(); -} +definition hasPassed(env e, uint48 schedule) returns bool = + schedule < e.block.timestamp; -function delayChangeWaitSanity(env e, uint48 newDelay) returns bool { - return e.block.timestamp + delayChangeWait_(e, newDelay) < max_uint48(); -} +definition increasingDelaySchedule(env e, uint48 newDelay) returns mathint = + e.block.timestamp + min(newDelay, defaultAdminDelayIncreaseWait()); -function isSet(uint48 schedule) returns bool { - return schedule != 0; -} - -function hasPassed(env e, uint48 schedule) returns bool { - return schedule < e.block.timestamp; -} - -function min(uint48 a, uint48 b) returns mathint { - return a < b ? a : b; -} - -function increasingDelaySchedule(env e, uint48 newDelay) returns mathint { - return e.block.timestamp + min(newDelay, defaultAdminDelayIncreaseWait()); -} - -function decreasingDelaySchedule(env e, uint48 newDelay) returns mathint { - return e.block.timestamp + defaultAdminDelay(e) - newDelay; -} +definition decreasingDelaySchedule(env e, uint48 newDelay) returns mathint = + e.block.timestamp + defaultAdminDelay(e) - newDelay; /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -57,11 +36,10 @@ function decreasingDelaySchedule(env e, uint48 newDelay) returns mathint { └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ invariant defaultAdminConsistency(address account) - defaultAdmin() == account <=> hasRole(DEFAULT_ADMIN_ROLE(), account) + (account == defaultAdmin() && account != 0) <=> hasRole(DEFAULT_ADMIN_ROLE(), account) { - preserved { - // defaultAdmin() returns the zero address when there's no default admin - require nonZeroAccount(account); + preserved with (env e) { + require nonzerosender(e); } } @@ -72,10 +50,12 @@ invariant defaultAdminConsistency(address account) */ invariant singleDefaultAdmin(address account, address another) hasRole(DEFAULT_ADMIN_ROLE(), account) && hasRole(DEFAULT_ADMIN_ROLE(), another) => another == account - // We filter here because we couldn't find a way to force Certora to have an initial state with - // only one DEFAULT_ADMIN_ROLE enforced, so a counter example is a different default admin since inception - // triggering the transfer, which is known to be impossible by definition. - filtered { f -> f.selector != acceptDefaultAdminTransfer().selector } + { + preserved { + requireInvariant defaultAdminConsistency(account); + requireInvariant defaultAdminConsistency(another); + } + } /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -118,7 +98,8 @@ rule revokeRoleEffect(env e, bytes32 role) { "roles can only be revoked by their owner except for the default admin role"; // effect - assert success => !hasRole(role, account), "role is revoked"; + assert success => !hasRole(role, account), + "role is revoked"; // no side effect assert hasOtherRoleBefore != hasOtherRoleAfter => (role == otherRole && account == otherAccount), @@ -137,35 +118,59 @@ rule renounceRoleEffect(env e, bytes32 role) { address account; address otherAccount; - bool hasOtherRoleBefore = hasRole(otherRole, otherAccount); - uint48 scheduleBefore = pendingDefaultAdminSchedule_(); + bool hasOtherRoleBefore = hasRole(otherRole, otherAccount); + address adminBefore = defaultAdmin(); address pendingAdminBefore = pendingDefaultAdmin_(); + uint48 scheduleBefore = pendingDefaultAdminSchedule_(); renounceRole@withrevert(e, role, account); bool success = !lastReverted; - bool hasOtherRoleAfter = hasRole(otherRole, otherAccount); + bool hasOtherRoleAfter = hasRole(otherRole, otherAccount); + address adminAfter = defaultAdmin(); + address pendingAdminAfter = pendingDefaultAdmin_(); + uint48 scheduleAfter = pendingDefaultAdminSchedule_(); // liveness assert success <=> ( account == e.msg.sender && ( + role != DEFAULT_ADMIN_ROLE() || + account != adminBefore || ( - role != DEFAULT_ADMIN_ROLE() - ) || ( - role == DEFAULT_ADMIN_ROLE() && pendingAdminBefore == 0 && isSet(scheduleBefore) && hasPassed(e, scheduleBefore) ) ) - ), "an account only can renounce by itself with a delay for the default admin role"; + ), + "an account only can renounce by itself with a delay for the default admin role"; // effect - assert success => !hasRole(role, account), "role is renounced"; + assert success => !hasRole(role, account), + "role is renounced"; + + assert success => ( + ( + role == DEFAULT_ADMIN_ROLE() && + account == adminBefore + ) ? ( + adminAfter == 0 && + pendingAdminAfter == 0 && + scheduleAfter == 0 + ) : ( + adminAfter == adminBefore && + pendingAdminAfter == pendingAdminBefore && + scheduleAfter == scheduleBefore + ) + ), + "renouncing default admin role cleans state iff called by previous admin"; // no side effect - assert hasOtherRoleBefore != hasOtherRoleAfter => (role == otherRole && account == otherAccount), + assert hasOtherRoleBefore != hasOtherRoleAfter => ( + role == otherRole && + account == otherAccount + ), "no other role is affected"; } @@ -175,10 +180,6 @@ rule renounceRoleEffect(env e, bytes32 role) { └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule noDefaultAdminChange(env e, method f, calldataarg args) { - require nonZeroAccount(e.msg.sender); - requireInvariant defaultAdminConsistency(defaultAdmin()); - requireInvariant singleDefaultAdmin(e.msg.sender, defaultAdmin()); - address adminBefore = defaultAdmin(); f(e, args); address adminAfter = defaultAdmin(); @@ -186,18 +187,17 @@ rule noDefaultAdminChange(env e, method f, calldataarg args) { assert adminBefore != adminAfter => ( f.selector == acceptDefaultAdminTransfer().selector || f.selector == renounceRole(bytes32,address).selector - ), "default admin is only affected by accepting an admin transfer or renoucing"; + ), + "default admin is only affected by accepting an admin transfer or renoucing"; } /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Rule: pendingDefaultAdmin is only affected by beginning, accepting or canceling an admin transfer │ +│ Rule: pendingDefaultAdmin is only affected by beginning, completing (accept or renounce), or canceling an admin │ +│ transfer │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule noPendingDefaultAdminChange(env e, method f, calldataarg args) { - requireInvariant defaultAdminConsistency(defaultAdmin()); - requireInvariant singleDefaultAdmin(e.msg.sender, defaultAdmin()); - address pendingAdminBefore = pendingDefaultAdmin_(); address scheduleBefore = pendingDefaultAdminSchedule_(); f(e, args); @@ -210,8 +210,10 @@ rule noPendingDefaultAdminChange(env e, method f, calldataarg args) { ) => ( f.selector == beginDefaultAdminTransfer(address).selector || f.selector == acceptDefaultAdminTransfer().selector || - f.selector == cancelDefaultAdminTransfer().selector - ), "pending admin and its schedule is only affected by beginning, accepting or cancelling an admin transfer"; + f.selector == cancelDefaultAdminTransfer().selector || + f.selector == renounceRole(bytes32,address).selector + ), + "pending admin and its schedule is only affected by beginning, completing, or cancelling an admin transfer"; } /* @@ -224,7 +226,8 @@ rule noDefaultAdminDelayChange(env e, method f, calldataarg args) { f(e, args); uint48 delayAfter = defaultAdminDelay(e); - assert delayBefore == delayAfter, "delay can't be changed atomically by any function"; + assert delayBefore == delayAfter, + "delay can't be changed atomically by any function"; } /* @@ -240,7 +243,8 @@ rule noPendingDefaultAdminDelayChange(env e, method f, calldataarg args) { assert pendingDelayBefore != pendingDelayAfter => ( f.selector == changeDefaultAdminDelay(uint48).selector || f.selector == rollbackDefaultAdminDelay().selector - ), "pending delay is only affected by changeDefaultAdminDelay or rollbackDefaultAdminDelay"; + ), + "pending delay is only affected by changeDefaultAdminDelay or rollbackDefaultAdminDelay"; } /* @@ -263,10 +267,10 @@ rule noDefaultAdminDelayIncreaseWaitChange(env e, method f, calldataarg args) { └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule beginDefaultAdminTransfer(env e, address newAdmin) { - require nonpayable(e); require timeSanity(e); - requireInvariant defaultAdminConsistency(defaultAdmin()); - requireInvariant singleDefaultAdmin(e.msg.sender, defaultAdmin()); + require nonpayable(e); + require nonzerosender(e); + requireInvariant defaultAdminConsistency(e.msg.sender); beginDefaultAdminTransfer@withrevert(e, newAdmin); bool success = !lastReverted; @@ -288,18 +292,24 @@ rule beginDefaultAdminTransfer(env e, address newAdmin) { └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule pendingDefaultAdminDelayEnforced(env e1, env e2, method f, calldataarg args, address newAdmin) { - require e1.block.timestamp < e2.block.timestamp; + require e1.block.timestamp <= e2.block.timestamp; uint48 delayBefore = defaultAdminDelay(e1); address adminBefore = defaultAdmin(); + // There might be a better way to generalize this without requiring `beginDefaultAdminTransfer`, but currently // it's the only way in which we can attest that only `delayBefore` has passed before a change. beginDefaultAdminTransfer(e1, newAdmin); f(e2, args); + address adminAfter = defaultAdmin(); - assert adminAfter == newAdmin => ((e2.block.timestamp >= e1.block.timestamp + delayBefore) || adminBefore == newAdmin), - "A delay can't change in less than applied schedule"; + // change can only happen towards the newAdmin, with the delay + assert adminAfter != adminBefore => ( + adminAfter == newAdmin && + e2.block.timestamp >= e1.block.timestamp + delayBefore + ), + "The admin can only change after the enforced delay and to the previously scheduled new admin"; } /* @@ -309,17 +319,19 @@ rule pendingDefaultAdminDelayEnforced(env e1, env e2, method f, calldataarg args */ rule acceptDefaultAdminTransfer(env e) { require nonpayable(e); - requireInvariant defaultAdminConsistency(defaultAdmin()); - requireInvariant singleDefaultAdmin(e.msg.sender, defaultAdmin()); address pendingAdminBefore = pendingDefaultAdmin_(); - uint48 scheduleAfter = pendingDefaultAdminSchedule_(); + uint48 scheduleBefore = pendingDefaultAdminSchedule_(); acceptDefaultAdminTransfer@withrevert(e); bool success = !lastReverted; // liveness - assert success <=> e.msg.sender == pendingAdminBefore && isSet(scheduleAfter) && hasPassed(e, scheduleAfter), + assert success <=> ( + e.msg.sender == pendingAdminBefore && + isSet(scheduleBefore) && + hasPassed(e, scheduleBefore) + ), "only the pending default admin can accept the role after the schedule has been set and passed"; // effect @@ -338,8 +350,8 @@ rule acceptDefaultAdminTransfer(env e) { */ rule cancelDefaultAdminTransfer(env e) { require nonpayable(e); - requireInvariant defaultAdminConsistency(defaultAdmin()); - requireInvariant singleDefaultAdmin(e.msg.sender, defaultAdmin()); + require nonzerosender(e); + requireInvariant defaultAdminConsistency(e.msg.sender); cancelDefaultAdminTransfer@withrevert(e); bool success = !lastReverted; @@ -361,11 +373,11 @@ rule cancelDefaultAdminTransfer(env e) { └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule changeDefaultAdminDelay(env e, uint48 newDelay) { - require nonpayable(e); require timeSanity(e); + require nonpayable(e); + require nonzerosender(e); require delayChangeWaitSanity(e, newDelay); - requireInvariant defaultAdminConsistency(defaultAdmin()); - requireInvariant singleDefaultAdmin(e.msg.sender, defaultAdmin()); + requireInvariant defaultAdminConsistency(e.msg.sender); uint48 delayBefore = defaultAdminDelay(e); @@ -377,7 +389,9 @@ rule changeDefaultAdminDelay(env e, uint48 newDelay) { "only the current default admin can begin a delay change"; // effect - assert success => pendingDelay_(e) == newDelay, "pending delay is set"; + assert success => pendingDelay_(e) == newDelay, + "pending delay is set"; + assert success => ( pendingDelaySchedule_(e) > e.block.timestamp || delayBefore == newDelay || // Interpreted as decreasing, x - x = 0 @@ -392,17 +406,22 @@ rule changeDefaultAdminDelay(env e, uint48 newDelay) { └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule pendingDelayWaitEnforced(env e1, env e2, method f, calldataarg args, uint48 newDelay) { - require e1.block.timestamp < e2.block.timestamp; + require e1.block.timestamp <= e2.block.timestamp; uint48 delayBefore = defaultAdminDelay(e1); + changeDefaultAdminDelay(e1, newDelay); f(e2, args); + uint48 delayAfter = defaultAdminDelay(e2); mathint delayWait = newDelay > delayBefore ? increasingDelaySchedule(e1, newDelay) : decreasingDelaySchedule(e1, newDelay); - assert delayAfter == newDelay => (e2.block.timestamp >= delayWait || delayBefore == newDelay), - "A delay can't change in less than applied schedule"; + assert delayAfter != delayBefore => ( + delayAfter == newDelay && + e2.block.timestamp >= delayWait + ), + "A delay can only change after the applied schedule"; } /* @@ -427,8 +446,8 @@ rule pendingDelayWait(env e, uint48 newDelay) { */ rule rollbackDefaultAdminDelay(env e) { require nonpayable(e); - requireInvariant defaultAdminConsistency(defaultAdmin()); - requireInvariant singleDefaultAdmin(e.msg.sender, defaultAdmin()); + require nonzerosender(e); + requireInvariant defaultAdminConsistency(e.msg.sender); rollbackDefaultAdminDelay@withrevert(e); bool success = !lastReverted; @@ -443,58 +462,3 @@ rule rollbackDefaultAdminDelay(env e) { assert success => pendingDelaySchedule_(e) == 0, "Pending default admin delay is reset"; } - -/* -┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Rule: pending default admin and the delay can only change along with their corresponding schedules │ -└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -*/ -rule pendingValueAndScheduleCoupling(env e, address newAdmin, uint48 newDelay) { - requireInvariant defaultAdminConsistency(defaultAdmin()); - requireInvariant singleDefaultAdmin(e.msg.sender, defaultAdmin()); - - // Pending admin - address pendingAdminBefore = pendingDefaultAdmin_(); - uint48 pendingAdminScheduleBefore = pendingDefaultAdminSchedule_(); - - beginDefaultAdminTransfer(e, newAdmin); - - address pendingAdminAfter = pendingDefaultAdmin_(); - uint48 pendingAdminScheduleAfter = pendingDefaultAdminSchedule_(); - - assert ( - pendingAdminScheduleBefore != pendingDefaultAdminSchedule_() && - pendingAdminBefore == pendingAdminAfter - ) => newAdmin == pendingAdminBefore, "pending admin stays the same if the new admin set is the same"; - - assert ( - pendingAdminBefore != pendingAdminAfter && - pendingAdminScheduleBefore == pendingDefaultAdminSchedule_() - ) => ( - // Schedule doesn't change if: - // - The defaultAdminDelay was reduced to a value such that added to the block.timestamp is equal to previous schedule - e.block.timestamp + defaultAdminDelay(e) == pendingAdminScheduleBefore - ), "pending admin stays the same if a default admin transfer is begun on accepted edge cases"; - - // Pending delay - address pendingDelayBefore = pendingDelay_(e); - uint48 pendingDelayScheduleBefore = pendingDelaySchedule_(e); - - changeDefaultAdminDelay(e, newDelay); - - address pendingDelayAfter = pendingDelay_(e); - uint48 pendingDelayScheduleAfter = pendingDelaySchedule_(e); - - assert ( - pendingDelayScheduleBefore != pendingDelayScheduleAfter && - pendingDelayBefore == pendingDelayAfter - ) => newDelay == pendingDelayBefore || pendingDelayBefore == 0, "pending delay stays the same if the new delay set is the same"; - - assert ( - pendingDelayBefore != pendingDelayAfter && - pendingDelayScheduleBefore == pendingDelayScheduleAfter - ) => ( - increasingDelaySchedule(e, newDelay) == pendingDelayScheduleBefore || - decreasingDelaySchedule(e, newDelay) == pendingDelayScheduleBefore - ), "pending delay stays the same if a default admin transfer is begun on accepted edge cases"; -} diff --git a/certora/specs/helpers/helpers.spec b/certora/specs/helpers/helpers.spec index 24842d62f..04e76df94 100644 --- a/certora/specs/helpers/helpers.spec +++ b/certora/specs/helpers/helpers.spec @@ -1 +1,10 @@ +// environment definition nonpayable(env e) returns bool = e.msg.value == 0; +definition nonzerosender(env e) returns bool = e.msg.sender != 0; + +// constants +definition max_uint48() returns mathint = (1 << 48) - 1; + +// math +definition min(mathint a, mathint b) returns mathint = a < b ? a : b; +definition max(mathint a, mathint b) returns mathint = a > b ? a : b; From 7e814a3074baa921db584c180ff6e300cdec8735 Mon Sep 17 00:00:00 2001 From: Francisco Date: Tue, 23 May 2023 22:21:17 +0100 Subject: [PATCH 139/197] Fix release merge script (#4273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- scripts/release/workflow/prepare-release-merge.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/release/workflow/prepare-release-merge.sh b/scripts/release/workflow/prepare-release-merge.sh index 8be96922c..4e6da5145 100644 --- a/scripts/release/workflow/prepare-release-merge.sh +++ b/scripts/release/workflow/prepare-release-merge.sh @@ -9,15 +9,18 @@ MERGE_BRANCH=merge/$GITHUB_REF_NAME git checkout -B "$MERGE_BRANCH" "$GITHUB_REF_NAME" # Get deleted changesets in this branch that might conflict with master -readarray -t DELETED_CHANGESETS < <(git diff origin/master --name-only -- '.changeset/*.md') +# --diff-filter=D - Only deleted files +readarray -t DELETED_CHANGESETS < <(git diff origin/master --diff-filter=D --name-only -- '.changeset/*.md') # Merge master, which will take those files cherry-picked. Auto-resolve conflicts favoring master. -git merge origin/master -m "Merge master to $GITHUB_REF_NAME" -X theirs +# Ignore conflicts that can't be resolved. +git merge origin/master -m "Merge master to $GITHUB_REF_NAME" -X theirs || true # Remove the originally deleted changesets to correctly sync with master rm -f "${DELETED_CHANGESETS[@]}" -git add .changeset/ +# Only git add deleted files +git ls-files --deleted .changeset/ | xargs git add # Allow empty here since there may be no changes if `rm -f` failed for all changesets git commit --allow-empty -m "Sync changesets with master" From 13d5e0466a9855e9305119ed383e54fc913fdc60 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 23 May 2023 23:26:43 +0200 Subject: [PATCH 140/197] Make Ownable's initial owner explicit (#4267) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- .changeset/clever-pumas-beg.md | 5 +++++ contracts/access/Ownable.sol | 4 ++-- contracts/mocks/ERC1271WalletMock.sol | 4 +--- contracts/proxy/beacon/UpgradeableBeacon.sol | 5 ++--- contracts/proxy/transparent/ProxyAdmin.sol | 5 +++++ test/access/Ownable.test.js | 2 +- test/access/Ownable2Step.test.js | 2 +- test/proxy/beacon/BeaconProxy.test.js | 12 ++++++------ test/proxy/beacon/UpgradeableBeacon.test.js | 7 +++++-- test/proxy/transparent/ProxyAdmin.test.js | 3 +-- 10 files changed, 29 insertions(+), 20 deletions(-) create mode 100644 .changeset/clever-pumas-beg.md diff --git a/.changeset/clever-pumas-beg.md b/.changeset/clever-pumas-beg.md new file mode 100644 index 000000000..5f1f4b13b --- /dev/null +++ b/.changeset/clever-pumas-beg.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`Ownable`: Add an `initialOwner` parameter to the constructor, making the ownership initialization explicit. diff --git a/contracts/access/Ownable.sol b/contracts/access/Ownable.sol index 1378ffb43..1351dad03 100644 --- a/contracts/access/Ownable.sol +++ b/contracts/access/Ownable.sol @@ -25,8 +25,8 @@ abstract contract Ownable is Context { /** * @dev Initializes the contract setting the deployer as the initial owner. */ - constructor() { - _transferOwnership(_msgSender()); + constructor(address initialOwner) { + _transferOwnership(initialOwner); } /** diff --git a/contracts/mocks/ERC1271WalletMock.sol b/contracts/mocks/ERC1271WalletMock.sol index 015288998..4e6d3ce57 100644 --- a/contracts/mocks/ERC1271WalletMock.sol +++ b/contracts/mocks/ERC1271WalletMock.sol @@ -7,9 +7,7 @@ import "../interfaces/IERC1271.sol"; import "../utils/cryptography/ECDSA.sol"; contract ERC1271WalletMock is Ownable, IERC1271 { - constructor(address originalOwner) { - transferOwnership(originalOwner); - } + constructor(address originalOwner) Ownable(originalOwner) {} function isValidSignature(bytes32 hash, bytes memory signature) public view override returns (bytes4 magicValue) { return ECDSA.recover(hash, signature) == owner() ? this.isValidSignature.selector : bytes4(0); diff --git a/contracts/proxy/beacon/UpgradeableBeacon.sol b/contracts/proxy/beacon/UpgradeableBeacon.sol index 9eeb149a6..557c58f67 100644 --- a/contracts/proxy/beacon/UpgradeableBeacon.sol +++ b/contracts/proxy/beacon/UpgradeableBeacon.sol @@ -21,10 +21,9 @@ contract UpgradeableBeacon is IBeacon, Ownable { event Upgraded(address indexed implementation); /** - * @dev Sets the address of the initial implementation, and the deployer account as the owner who can upgrade the - * beacon. + * @dev Sets the address of the initial implementation, and the initial owner who can upgrade the beacon. */ - constructor(address implementation_) { + constructor(address implementation_, address initialOwner) Ownable(initialOwner) { _setImplementation(implementation_); } diff --git a/contracts/proxy/transparent/ProxyAdmin.sol b/contracts/proxy/transparent/ProxyAdmin.sol index 78a409e01..3739ad85f 100644 --- a/contracts/proxy/transparent/ProxyAdmin.sol +++ b/contracts/proxy/transparent/ProxyAdmin.sol @@ -11,6 +11,11 @@ import "../../access/Ownable.sol"; * explanation of why you would want to use this see the documentation for {TransparentUpgradeableProxy}. */ contract ProxyAdmin is Ownable { + /** + * @dev Sets the initial owner who can perform upgrades. + */ + constructor(address initialOwner) Ownable(initialOwner) {} + /** * @dev Changes the admin of `proxy` to `newAdmin`. * diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index 109150874..07b8764a5 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -9,7 +9,7 @@ contract('Ownable', function (accounts) { const [owner, other] = accounts; beforeEach(async function () { - this.ownable = await Ownable.new({ from: owner }); + this.ownable = await Ownable.new(owner); }); it('has an owner', async function () { diff --git a/test/access/Ownable2Step.test.js b/test/access/Ownable2Step.test.js index ce043057b..dfda6b708 100644 --- a/test/access/Ownable2Step.test.js +++ b/test/access/Ownable2Step.test.js @@ -8,7 +8,7 @@ contract('Ownable2Step', function (accounts) { const [owner, accountA, accountB] = accounts; beforeEach(async function () { - this.ownable2Step = await Ownable2Step.new({ from: owner }); + this.ownable2Step = await Ownable2Step.new(owner); }); describe('transfer ownership', function () { diff --git a/test/proxy/beacon/BeaconProxy.test.js b/test/proxy/beacon/BeaconProxy.test.js index 968f00be8..68db10ddc 100644 --- a/test/proxy/beacon/BeaconProxy.test.js +++ b/test/proxy/beacon/BeaconProxy.test.js @@ -11,7 +11,7 @@ const BadBeaconNoImpl = artifacts.require('BadBeaconNoImpl'); const BadBeaconNotContract = artifacts.require('BadBeaconNotContract'); contract('BeaconProxy', function (accounts) { - const [anotherAccount] = accounts; + const [upgradeableBeaconAdmin, anotherAccount] = accounts; describe('bad beacon is not accepted', async function () { it('non-contract beacon', async function () { @@ -49,7 +49,7 @@ contract('BeaconProxy', function (accounts) { }); beforeEach('deploy beacon', async function () { - this.beacon = await UpgradeableBeacon.new(this.implementationV0.address); + this.beacon = await UpgradeableBeacon.new(this.implementationV0.address, upgradeableBeaconAdmin); }); it('no initialization', async function () { @@ -81,7 +81,7 @@ contract('BeaconProxy', function (accounts) { }); it('upgrade a proxy by upgrading its beacon', async function () { - const beacon = await UpgradeableBeacon.new(this.implementationV0.address); + const beacon = await UpgradeableBeacon.new(this.implementationV0.address, upgradeableBeaconAdmin); const value = '10'; const data = this.implementationV0.contract.methods.initializeNonPayableWithValue(value).encodeABI(); @@ -96,7 +96,7 @@ contract('BeaconProxy', function (accounts) { expect(await dummy.version()).to.eq('V1'); // upgrade beacon - await beacon.upgradeTo(this.implementationV1.address); + await beacon.upgradeTo(this.implementationV1.address, { from: upgradeableBeaconAdmin }); // test upgraded version expect(await dummy.version()).to.eq('V2'); @@ -106,7 +106,7 @@ contract('BeaconProxy', function (accounts) { const value1 = '10'; const value2 = '42'; - const beacon = await UpgradeableBeacon.new(this.implementationV0.address); + const beacon = await UpgradeableBeacon.new(this.implementationV0.address, upgradeableBeaconAdmin); const proxy1InitializeData = this.implementationV0.contract.methods .initializeNonPayableWithValue(value1) @@ -130,7 +130,7 @@ contract('BeaconProxy', function (accounts) { expect(await dummy2.version()).to.eq('V1'); // upgrade beacon - await beacon.upgradeTo(this.implementationV1.address); + await beacon.upgradeTo(this.implementationV1.address, { from: upgradeableBeaconAdmin }); // test upgraded version expect(await dummy1.version()).to.eq('V2'); diff --git a/test/proxy/beacon/UpgradeableBeacon.test.js b/test/proxy/beacon/UpgradeableBeacon.test.js index d65f3e0a5..c19b250b9 100644 --- a/test/proxy/beacon/UpgradeableBeacon.test.js +++ b/test/proxy/beacon/UpgradeableBeacon.test.js @@ -9,13 +9,16 @@ contract('UpgradeableBeacon', function (accounts) { const [owner, other] = accounts; it('cannot be created with non-contract implementation', async function () { - await expectRevert(UpgradeableBeacon.new(accounts[0]), 'UpgradeableBeacon: implementation is not a contract'); + await expectRevert( + UpgradeableBeacon.new(accounts[0], owner), + 'UpgradeableBeacon: implementation is not a contract', + ); }); context('once deployed', async function () { beforeEach('deploying beacon', async function () { this.v1 = await Implementation1.new(); - this.beacon = await UpgradeableBeacon.new(this.v1.address, { from: owner }); + this.beacon = await UpgradeableBeacon.new(this.v1.address, owner); }); it('returns implementation', async function () { diff --git a/test/proxy/transparent/ProxyAdmin.test.js b/test/proxy/transparent/ProxyAdmin.test.js index efaaf94c2..85b6695df 100644 --- a/test/proxy/transparent/ProxyAdmin.test.js +++ b/test/proxy/transparent/ProxyAdmin.test.js @@ -17,12 +17,11 @@ contract('ProxyAdmin', function (accounts) { beforeEach(async function () { const initializeData = Buffer.from(''); - this.proxyAdmin = await ProxyAdmin.new({ from: proxyAdminOwner }); + this.proxyAdmin = await ProxyAdmin.new(proxyAdminOwner); const proxy = await TransparentUpgradeableProxy.new( this.implementationV1.address, this.proxyAdmin.address, initializeData, - { from: proxyAdminOwner }, ); this.proxy = await ITransparentUpgradeableProxy.at(proxy.address); }); From cbc6145f5f33744e91e40be153f151c2e0fb850f Mon Sep 17 00:00:00 2001 From: Balaji Shetty Pachai <32358081+balajipachai@users.noreply.github.com> Date: Wed, 24 May 2023 14:09:06 +0530 Subject: [PATCH 141/197] Removes zero address check from balanceOf in ERC1155 (#4263) Co-authored-by: bpachai Co-authored-by: Francisco Giordano --- .changeset/smooth-books-wink.md | 5 +++++ contracts/token/ERC1155/ERC1155.sol | 1 - test/token/ERC1155/ERC1155.behavior.js | 22 ++++++++++------------ 3 files changed, 15 insertions(+), 13 deletions(-) create mode 100644 .changeset/smooth-books-wink.md diff --git a/.changeset/smooth-books-wink.md b/.changeset/smooth-books-wink.md new file mode 100644 index 000000000..e5eb3fbeb --- /dev/null +++ b/.changeset/smooth-books-wink.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`ERC1155`: Remove check for address zero in `balanceOf`. diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol index b20b711d5..445c3179d 100644 --- a/contracts/token/ERC1155/ERC1155.sol +++ b/contracts/token/ERC1155/ERC1155.sol @@ -68,7 +68,6 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * - `account` cannot be the zero address. */ function balanceOf(address account, uint256 id) public view virtual override returns (uint256) { - require(account != address(0), "ERC1155: address zero is not a valid owner"); return _balances[id][account]; } diff --git a/test/token/ERC1155/ERC1155.behavior.js b/test/token/ERC1155/ERC1155.behavior.js index 96d448a9e..c41c69c62 100644 --- a/test/token/ERC1155/ERC1155.behavior.js +++ b/test/token/ERC1155/ERC1155.behavior.js @@ -20,11 +20,8 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m describe('like an ERC1155', function () { describe('balanceOf', function () { - it('reverts when queried about the zero address', async function () { - await expectRevert( - this.token.balanceOf(ZERO_ADDRESS, firstTokenId), - 'ERC1155: address zero is not a valid owner', - ); + it('should return 0 when queried about the zero address', async function () { + expect(await this.token.balanceOf(ZERO_ADDRESS, firstTokenId)).to.be.bignumber.equal('0'); }); context("when accounts don't own tokens", function () { @@ -76,14 +73,15 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m ); }); - it('reverts when one of the addresses is the zero address', async function () { - await expectRevert( - this.token.balanceOfBatch( - [firstTokenHolder, secondTokenHolder, ZERO_ADDRESS], - [firstTokenId, secondTokenId, unknownTokenId], - ), - 'ERC1155: address zero is not a valid owner', + it('should return 0 as the balance when one of the addresses is the zero address', async function () { + const result = await this.token.balanceOfBatch( + [firstTokenHolder, secondTokenHolder, ZERO_ADDRESS], + [firstTokenId, secondTokenId, unknownTokenId], ); + expect(result).to.be.an('array'); + expect(result[0]).to.be.a.bignumber.equal('0'); + expect(result[1]).to.be.a.bignumber.equal('0'); + expect(result[2]).to.be.a.bignumber.equal('0'); }); context("when accounts don't own tokens", function () { From 09329f8a18f08df65863a5060f6e776bf7fccacf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 10:37:00 -0300 Subject: [PATCH 142/197] Merge release-v4.9 branch (#4274) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Francisco Giordano Co-authored-by: github-actions[bot] Co-authored-by: Hadrien Croubois --- .changeset/beige-buses-drop.md | 7 ---- .changeset/curvy-shrimps-enjoy.md | 7 ---- .changeset/curvy-suns-sort.md | 7 ---- .changeset/early-oranges-raise.md | 5 --- .changeset/famous-rules-burn.md | 7 ---- .changeset/five-ducks-develop.md | 5 --- .changeset/five-poets-mix.md | 5 --- .changeset/flat-deers-end.md | 5 --- .changeset/four-bats-sniff.md | 5 --- .changeset/funny-rockets-compete.md | 7 ---- .changeset/gold-chicken-clean.md | 7 ---- .changeset/happy-socks-travel.md | 5 --- .changeset/healthy-squids-stare.md | 7 ---- .changeset/lemon-dogs-kiss.md | 7 ---- .changeset/little-kiwis-ring.md | 7 ---- .changeset/loud-wolves-promise.md | 5 --- .changeset/lovely-dragons-appear.md | 5 --- .changeset/modern-games-exist.md | 5 --- .changeset/new-ways-own.md | 5 --- .changeset/ninety-hornets-kick.md | 5 --- .changeset/perfect-insects-listen.md | 5 --- .changeset/pretty-hornets-play.md | 7 ---- .changeset/proud-comics-deliver.md | 5 --- .changeset/short-roses-judge.md | 5 --- .changeset/silent-dancers-type.md | 5 --- .changeset/slimy-knives-hug.md | 5 --- .changeset/small-cars-appear.md | 5 --- .changeset/small-terms-sleep.md | 5 --- .changeset/strong-bulldogs-buy.md | 5 --- .changeset/tame-ladybugs-sit.md | 7 ---- .changeset/tender-needles-dance.md | 7 ---- .changeset/thin-dragons-report.md | 5 --- .changeset/thirty-swans-exercise.md | 5 --- .changeset/violet-frogs-hide.md | 5 --- .changeset/warm-masks-obey.md | 5 --- .changeset/yellow-swans-cover.md | 5 --- CHANGELOG.md | 39 +++++++++++++++++++ contracts/access/AccessControl.sol | 2 +- .../access/AccessControlDefaultAdminRules.sol | 2 +- .../IAccessControlDefaultAdminRules.sol | 2 +- contracts/access/Ownable.sol | 2 +- contracts/access/Ownable2Step.sol | 2 +- .../polygon/CrossChainEnabledPolygonChild.sol | 2 +- contracts/finance/VestingWallet.sol | 2 +- contracts/governance/Governor.sol | 2 +- contracts/governance/IGovernor.sol | 2 +- contracts/governance/TimelockController.sol | 2 +- .../GovernorCompatibilityBravo.sol | 2 +- .../IGovernorCompatibilityBravo.sol | 2 +- .../extensions/GovernorCountingSimple.sol | 2 +- .../extensions/GovernorPreventLateQuorum.sol | 2 +- .../extensions/GovernorSettings.sol | 2 +- .../extensions/GovernorTimelockCompound.sol | 2 +- .../extensions/GovernorTimelockControl.sol | 2 +- .../governance/extensions/GovernorVotes.sol | 2 +- .../extensions/GovernorVotesComp.sol | 2 +- .../GovernorVotesQuorumFraction.sol | 2 +- contracts/governance/utils/IVotes.sol | 2 +- contracts/governance/utils/Votes.sol | 2 +- contracts/interfaces/IERC1363.sol | 2 +- contracts/interfaces/IERC1363Receiver.sol | 2 +- contracts/interfaces/IERC1363Spender.sol | 2 +- contracts/interfaces/IERC1967.sol | 1 + contracts/interfaces/IERC2612.sol | 2 +- contracts/interfaces/IERC2981.sol | 2 +- .../interfaces/IERC3156FlashBorrower.sol | 2 +- contracts/interfaces/IERC4626.sol | 2 +- contracts/interfaces/IERC4906.sol | 1 + contracts/interfaces/IERC5267.sol | 1 + contracts/interfaces/IERC5313.sol | 1 + contracts/interfaces/IERC5805.sol | 2 +- contracts/interfaces/IERC6372.sol | 2 +- contracts/interfaces/draft-IERC2612.sol | 1 + contracts/metatx/MinimalForwarder.sol | 2 +- contracts/package.json | 2 +- contracts/proxy/Clones.sol | 2 +- contracts/proxy/ERC1967/ERC1967Upgrade.sol | 2 +- .../TransparentUpgradeableProxy.sol | 2 +- contracts/proxy/utils/Initializable.sol | 2 +- contracts/proxy/utils/UUPSUpgradeable.sol | 2 +- contracts/security/ReentrancyGuard.sol | 2 +- contracts/token/ERC1155/ERC1155.sol | 2 +- contracts/token/ERC1155/IERC1155.sol | 2 +- .../ERC1155/extensions/ERC1155Burnable.sol | 2 +- .../presets/ERC1155PresetMinterPauser.sol | 2 +- contracts/token/ERC20/ERC20.sol | 2 +- contracts/token/ERC20/IERC20.sol | 2 +- .../token/ERC20/extensions/ERC20Pausable.sol | 2 +- .../token/ERC20/extensions/ERC20Permit.sol | 2 +- .../token/ERC20/extensions/ERC20Snapshot.sol | 2 +- .../token/ERC20/extensions/ERC20Votes.sol | 2 +- .../token/ERC20/extensions/ERC20Wrapper.sol | 2 +- contracts/token/ERC20/extensions/ERC4626.sol | 2 +- .../token/ERC20/extensions/IERC20Permit.sol | 2 +- .../ERC20/extensions/draft-ERC20Permit.sol | 2 +- .../ERC20/extensions/draft-IERC20Permit.sol | 1 + .../ERC20/presets/ERC20PresetFixedSupply.sol | 2 +- contracts/token/ERC20/utils/SafeERC20.sol | 2 +- contracts/token/ERC20/utils/TokenTimelock.sol | 2 +- contracts/token/ERC721/ERC721.sol | 2 +- contracts/token/ERC721/IERC721.sol | 2 +- .../ERC721/extensions/ERC721Consecutive.sol | 2 +- .../ERC721/extensions/ERC721URIStorage.sol | 2 +- .../token/ERC721/extensions/ERC721Votes.sol | 2 +- .../token/ERC721/extensions/ERC721Wrapper.sol | 1 + .../ERC721PresetMinterPauserAutoId.sol | 2 +- contracts/token/ERC721/utils/ERC721Holder.sol | 2 +- contracts/token/ERC777/ERC777.sol | 2 +- contracts/token/ERC777/IERC777.sol | 2 +- contracts/token/common/ERC2981.sol | 2 +- contracts/utils/Address.sol | 2 +- contracts/utils/Arrays.sol | 2 +- contracts/utils/Checkpoints.sol | 2 +- contracts/utils/Create2.sol | 2 +- contracts/utils/Multicall.sol | 2 +- contracts/utils/ShortStrings.sol | 1 + contracts/utils/StorageSlot.sol | 2 +- contracts/utils/Strings.sol | 2 +- contracts/utils/Timers.sol | 2 +- contracts/utils/cryptography/ECDSA.sol | 2 +- contracts/utils/cryptography/EIP712.sol | 2 +- contracts/utils/cryptography/MerkleProof.sol | 2 +- .../utils/cryptography/SignatureChecker.sol | 2 +- .../utils/introspection/ERC165Checker.sol | 2 +- .../introspection/ERC1820Implementer.sol | 2 +- .../utils/introspection/IERC1820Registry.sol | 2 +- contracts/utils/math/Math.sol | 2 +- contracts/utils/math/SafeMath.sol | 2 +- contracts/utils/structs/BitMaps.sol | 2 +- contracts/utils/structs/DoubleEndedQueue.sol | 2 +- contracts/utils/structs/EnumerableMap.sol | 2 +- contracts/utils/structs/EnumerableSet.sol | 2 +- contracts/vendor/amb/IAMB.sol | 2 +- contracts/vendor/arbitrum/IArbSys.sol | 2 +- contracts/vendor/arbitrum/IBridge.sol | 2 +- contracts/vendor/arbitrum/IOutbox.sol | 2 +- .../vendor/optimism/ICrossDomainMessenger.sol | 2 +- .../vendor/polygon/IFxMessageProcessor.sol | 2 +- package.json | 2 +- 139 files changed, 141 insertions(+), 298 deletions(-) delete mode 100644 .changeset/beige-buses-drop.md delete mode 100644 .changeset/curvy-shrimps-enjoy.md delete mode 100644 .changeset/curvy-suns-sort.md delete mode 100644 .changeset/early-oranges-raise.md delete mode 100644 .changeset/famous-rules-burn.md delete mode 100644 .changeset/five-ducks-develop.md delete mode 100644 .changeset/five-poets-mix.md delete mode 100644 .changeset/flat-deers-end.md delete mode 100644 .changeset/four-bats-sniff.md delete mode 100644 .changeset/funny-rockets-compete.md delete mode 100644 .changeset/gold-chicken-clean.md delete mode 100644 .changeset/happy-socks-travel.md delete mode 100644 .changeset/healthy-squids-stare.md delete mode 100644 .changeset/lemon-dogs-kiss.md delete mode 100644 .changeset/little-kiwis-ring.md delete mode 100644 .changeset/loud-wolves-promise.md delete mode 100644 .changeset/lovely-dragons-appear.md delete mode 100644 .changeset/modern-games-exist.md delete mode 100644 .changeset/new-ways-own.md delete mode 100644 .changeset/ninety-hornets-kick.md delete mode 100644 .changeset/perfect-insects-listen.md delete mode 100644 .changeset/pretty-hornets-play.md delete mode 100644 .changeset/proud-comics-deliver.md delete mode 100644 .changeset/short-roses-judge.md delete mode 100644 .changeset/silent-dancers-type.md delete mode 100644 .changeset/slimy-knives-hug.md delete mode 100644 .changeset/small-cars-appear.md delete mode 100644 .changeset/small-terms-sleep.md delete mode 100644 .changeset/strong-bulldogs-buy.md delete mode 100644 .changeset/tame-ladybugs-sit.md delete mode 100644 .changeset/tender-needles-dance.md delete mode 100644 .changeset/thin-dragons-report.md delete mode 100644 .changeset/thirty-swans-exercise.md delete mode 100644 .changeset/violet-frogs-hide.md delete mode 100644 .changeset/warm-masks-obey.md delete mode 100644 .changeset/yellow-swans-cover.md diff --git a/.changeset/beige-buses-drop.md b/.changeset/beige-buses-drop.md deleted file mode 100644 index ecfd08b35..000000000 --- a/.changeset/beige-buses-drop.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`Initializable`: optimize `_disableInitializers` by using `!=` instead of `<`. - -pr: #3787 diff --git a/.changeset/curvy-shrimps-enjoy.md b/.changeset/curvy-shrimps-enjoy.md deleted file mode 100644 index 22c2bc54c..000000000 --- a/.changeset/curvy-shrimps-enjoy.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ReentrancyGuard`: Add a `_reentrancyGuardEntered` function to expose the guard status. - -pr: #3714 diff --git a/.changeset/curvy-suns-sort.md b/.changeset/curvy-suns-sort.md deleted file mode 100644 index 201f45ca7..000000000 --- a/.changeset/curvy-suns-sort.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`Ownable2Step`: make `acceptOwnership` public virtual to enable usecases that require overriding it. - -pr: #3960 diff --git a/.changeset/early-oranges-raise.md b/.changeset/early-oranges-raise.md deleted file mode 100644 index af60a4432..000000000 --- a/.changeset/early-oranges-raise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ERC721Wrapper`: add a new extension of the `ERC721` token which wraps an underlying token. Deposit and withdraw guarantee that the ownership of each token is backed by a corresponding underlying token with the same identifier. diff --git a/.changeset/famous-rules-burn.md b/.changeset/famous-rules-burn.md deleted file mode 100644 index a97aca0b3..000000000 --- a/.changeset/famous-rules-burn.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`EnumerableMap`: add a `keys()` function that returns an array containing all the keys. - -pr: #3920 diff --git a/.changeset/five-ducks-develop.md b/.changeset/five-ducks-develop.md deleted file mode 100644 index fe25db071..000000000 --- a/.changeset/five-ducks-develop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`UUPSUpgradeable.sol`: Change visibility to the functions `upgradeTo ` and `upgradeToAndCall ` from `external` to `public`. diff --git a/.changeset/five-poets-mix.md b/.changeset/five-poets-mix.md deleted file mode 100644 index f5050b246..000000000 --- a/.changeset/five-poets-mix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`TimelockController`: Add the `CallSalt` event to emit on operation schedule. diff --git a/.changeset/flat-deers-end.md b/.changeset/flat-deers-end.md deleted file mode 100644 index 61895f2cf..000000000 --- a/.changeset/flat-deers-end.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Governor`: add a public `cancel(uint256)` function. diff --git a/.changeset/four-bats-sniff.md b/.changeset/four-bats-sniff.md deleted file mode 100644 index 137b5e515..000000000 --- a/.changeset/four-bats-sniff.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Governor`: Enable timestamp operation for blockchains without a stable block time. This is achieved by connecting a Governor's internal clock to match a voting token's EIP-6372 interface. diff --git a/.changeset/funny-rockets-compete.md b/.changeset/funny-rockets-compete.md deleted file mode 100644 index 3f665bc9e..000000000 --- a/.changeset/funny-rockets-compete.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -Reformatted codebase with latest version of Prettier Solidity. - -pr: #3898 diff --git a/.changeset/gold-chicken-clean.md b/.changeset/gold-chicken-clean.md deleted file mode 100644 index 1353e9c9c..000000000 --- a/.changeset/gold-chicken-clean.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Strings`: add `equal` method. - -pr: #3774 diff --git a/.changeset/happy-socks-travel.md b/.changeset/happy-socks-travel.md deleted file mode 100644 index b29d6bacd..000000000 --- a/.changeset/happy-socks-travel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`IERC5313`: Add an interface for EIP-5313 that is now final. diff --git a/.changeset/healthy-squids-stare.md b/.changeset/healthy-squids-stare.md deleted file mode 100644 index 9e2c9f3dd..000000000 --- a/.changeset/healthy-squids-stare.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`Math`: optimize `log256` rounding check. - -pr: #3745 diff --git a/.changeset/lemon-dogs-kiss.md b/.changeset/lemon-dogs-kiss.md deleted file mode 100644 index 5e2787cf1..000000000 --- a/.changeset/lemon-dogs-kiss.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`ERC20Votes`: optimize by using unchecked arithmetic. - -pr: #3748 diff --git a/.changeset/little-kiwis-ring.md b/.changeset/little-kiwis-ring.md deleted file mode 100644 index 81909a513..000000000 --- a/.changeset/little-kiwis-ring.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`Multicall`: annotate `multicall` function as upgrade safe to not raise a flag for its delegatecall. - -pr: #3961 diff --git a/.changeset/loud-wolves-promise.md b/.changeset/loud-wolves-promise.md deleted file mode 100644 index 544b52c5f..000000000 --- a/.changeset/loud-wolves-promise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`AccessControlDefaultAdminRules`: Clean up pending admin schedule on renounce. diff --git a/.changeset/lovely-dragons-appear.md b/.changeset/lovely-dragons-appear.md deleted file mode 100644 index fe538634a..000000000 --- a/.changeset/lovely-dragons-appear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`IERC4906`: Add an interface for ERC-4906 that is now Final. diff --git a/.changeset/modern-games-exist.md b/.changeset/modern-games-exist.md deleted file mode 100644 index bd89b4f16..000000000 --- a/.changeset/modern-games-exist.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`StorageSlot`: Add support for `string` and `bytes`. diff --git a/.changeset/new-ways-own.md b/.changeset/new-ways-own.md deleted file mode 100644 index f940bfeb7..000000000 --- a/.changeset/new-ways-own.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`ERC20Pausable`, `ERC721Pausable`, `ERC1155Pausable`: Add note regarding missing public pausing functionality diff --git a/.changeset/ninety-hornets-kick.md b/.changeset/ninety-hornets-kick.md deleted file mode 100644 index 16886c5c1..000000000 --- a/.changeset/ninety-hornets-kick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Votes`, `ERC20Votes`, `ERC721Votes`: support timestamp checkpointing using EIP-6372. diff --git a/.changeset/perfect-insects-listen.md b/.changeset/perfect-insects-listen.md deleted file mode 100644 index 9e60120ed..000000000 --- a/.changeset/perfect-insects-listen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ERC4626`: Add mitigation to the inflation attack through virtual shares and assets. diff --git a/.changeset/pretty-hornets-play.md b/.changeset/pretty-hornets-play.md deleted file mode 100644 index e7d15c24a..000000000 --- a/.changeset/pretty-hornets-play.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Strings`: add `toString` method for signed integers. - -pr: #3773 diff --git a/.changeset/proud-comics-deliver.md b/.changeset/proud-comics-deliver.md deleted file mode 100644 index e9c1015f8..000000000 --- a/.changeset/proud-comics-deliver.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ERC20Wrapper`: Make the `underlying` variable private and add a public accessor. diff --git a/.changeset/short-roses-judge.md b/.changeset/short-roses-judge.md deleted file mode 100644 index 002aebb11..000000000 --- a/.changeset/short-roses-judge.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`EIP712`: add EIP-5267 support for better domain discovery. diff --git a/.changeset/silent-dancers-type.md b/.changeset/silent-dancers-type.md deleted file mode 100644 index 74ecf500d..000000000 --- a/.changeset/silent-dancers-type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`AccessControlDefaultAdminRules`: Add an extension of `AccessControl` with additional security rules for the `DEFAULT_ADMIN_ROLE`. diff --git a/.changeset/slimy-knives-hug.md b/.changeset/slimy-knives-hug.md deleted file mode 100644 index 94099eea7..000000000 --- a/.changeset/slimy-knives-hug.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`SignatureChecker`: Add `isValidERC1271SignatureNow` for checking a signature directly against a smart contract using ERC-1271. diff --git a/.changeset/small-cars-appear.md b/.changeset/small-cars-appear.md deleted file mode 100644 index 0263bcd18..000000000 --- a/.changeset/small-cars-appear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`ECDSA`: Add a function `toDataWithIntendedValidatorHash` that encodes data with version 0x00 following EIP-191. diff --git a/.changeset/small-terms-sleep.md b/.changeset/small-terms-sleep.md deleted file mode 100644 index ed184a1c4..000000000 --- a/.changeset/small-terms-sleep.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`SafeERC20`: Add a `forceApprove` function to improve compatibility with tokens behaving like USDT. diff --git a/.changeset/strong-bulldogs-buy.md b/.changeset/strong-bulldogs-buy.md deleted file mode 100644 index 001b0f88f..000000000 --- a/.changeset/strong-bulldogs-buy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ERC1967Upgrade`: removed contract-wide `oz-upgrades-unsafe-allow delegatecall` annotation, replaced by granular annotation in `UUPSUpgradeable`. diff --git a/.changeset/tame-ladybugs-sit.md b/.changeset/tame-ladybugs-sit.md deleted file mode 100644 index 4cddc219e..000000000 --- a/.changeset/tame-ladybugs-sit.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`MerkleProof`: optimize by using unchecked arithmetic. - -pr: #3745 diff --git a/.changeset/tender-needles-dance.md b/.changeset/tender-needles-dance.md deleted file mode 100644 index 75ce9fbf8..000000000 --- a/.changeset/tender-needles-dance.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ERC20Wrapper`: self wrapping and deposit by the wrapper itself are now explicitly forbidden. - -commit: 3214f6c25 diff --git a/.changeset/thin-dragons-report.md b/.changeset/thin-dragons-report.md deleted file mode 100644 index b73730f7f..000000000 --- a/.changeset/thin-dragons-report.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ECDSA`: optimize bytes32 computation by using assembly instead of `abi.encodePacked`. diff --git a/.changeset/thirty-swans-exercise.md b/.changeset/thirty-swans-exercise.md deleted file mode 100644 index a460271b0..000000000 --- a/.changeset/thirty-swans-exercise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ERC721URIStorage`: Emit ERC-4906 `MetadataUpdate` in `_setTokenURI`. diff --git a/.changeset/violet-frogs-hide.md b/.changeset/violet-frogs-hide.md deleted file mode 100644 index 21d2bf984..000000000 --- a/.changeset/violet-frogs-hide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ShortStrings`: Added a library for handling short strings in a gas efficient way, with fallback to storage for longer strings. diff --git a/.changeset/warm-masks-obey.md b/.changeset/warm-masks-obey.md deleted file mode 100644 index 3bcfa9bdd..000000000 --- a/.changeset/warm-masks-obey.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`SignatureChecker`: Allow return data length greater than 32 from EIP-1271 signers. diff --git a/.changeset/yellow-swans-cover.md b/.changeset/yellow-swans-cover.md deleted file mode 100644 index ee1680178..000000000 --- a/.changeset/yellow-swans-cover.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`UUPSUpgradeable`: added granular `oz-upgrades-unsafe-allow-reachable` annotation to improve upgrade safety checks on latest version of the Upgrades Plugins (starting with `@openzeppelin/upgrades-core@1.21.0`). diff --git a/CHANGELOG.md b/CHANGELOG.md index ffbd3acea..b3b25ac5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Changelog + +## 4.9.0 (2023-05-23) + +- `ReentrancyGuard`: Add a `_reentrancyGuardEntered` function to expose the guard status. ([#3714](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3714)) +- `ERC721Wrapper`: add a new extension of the `ERC721` token which wraps an underlying token. Deposit and withdraw guarantee that the ownership of each token is backed by a corresponding underlying token with the same identifier. ([#3863](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3863)) +- `EnumerableMap`: add a `keys()` function that returns an array containing all the keys. ([#3920](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3920)) +- `Governor`: add a public `cancel(uint256)` function. ([#3983](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3983)) +- `Governor`: Enable timestamp operation for blockchains without a stable block time. This is achieved by connecting a Governor's internal clock to match a voting token's EIP-6372 interface. ([#3934](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3934)) +- `Strings`: add `equal` method. ([#3774](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3774)) +- `IERC5313`: Add an interface for EIP-5313 that is now final. ([#4013](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4013)) +- `IERC4906`: Add an interface for ERC-4906 that is now Final. ([#4012](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4012)) +- `StorageSlot`: Add support for `string` and `bytes`. ([#4008](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4008)) +- `Votes`, `ERC20Votes`, `ERC721Votes`: support timestamp checkpointing using EIP-6372. ([#3934](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3934)) +- `ERC4626`: Add mitigation to the inflation attack through virtual shares and assets. ([#3979](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3979)) +- `Strings`: add `toString` method for signed integers. ([#3773](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3773)) +- `ERC20Wrapper`: Make the `underlying` variable private and add a public accessor. ([#4029](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4029)) +- `EIP712`: add EIP-5267 support for better domain discovery. ([#3969](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3969)) +- `AccessControlDefaultAdminRules`: Add an extension of `AccessControl` with additional security rules for the `DEFAULT_ADMIN_ROLE`. ([#4009](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4009)) +- `SignatureChecker`: Add `isValidERC1271SignatureNow` for checking a signature directly against a smart contract using ERC-1271. ([#3932](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3932)) +- `SafeERC20`: Add a `forceApprove` function to improve compatibility with tokens behaving like USDT. ([#4067](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4067)) +- `ERC1967Upgrade`: removed contract-wide `oz-upgrades-unsafe-allow delegatecall` annotation, replaced by granular annotation in `UUPSUpgradeable`. ([#3971](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3971)) +- `ERC20Wrapper`: self wrapping and deposit by the wrapper itself are now explicitly forbidden. ([#4100](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4100)) +- `ECDSA`: optimize bytes32 computation by using assembly instead of `abi.encodePacked`. ([#3853](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3853)) +- `ERC721URIStorage`: Emit ERC-4906 `MetadataUpdate` in `_setTokenURI`. ([#4012](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4012)) +- `ShortStrings`: Added a library for handling short strings in a gas efficient way, with fallback to storage for longer strings. ([#4023](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4023)) +- `SignatureChecker`: Allow return data length greater than 32 from EIP-1271 signers. ([#4038](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4038)) +- `UUPSUpgradeable`: added granular `oz-upgrades-unsafe-allow-reachable` annotation to improve upgrade safety checks on latest version of the Upgrades Plugins (starting with `@openzeppelin/upgrades-core@1.21.0`). ([#3971](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3971)) +- `Initializable`: optimize `_disableInitializers` by using `!=` instead of `<`. ([#3787](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3787)) +- `Ownable2Step`: make `acceptOwnership` public virtual to enable usecases that require overriding it. ([#3960](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3960)) +- `UUPSUpgradeable.sol`: Change visibility to the functions `upgradeTo ` and `upgradeToAndCall ` from `external` to `public`. ([#3959](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3959)) +- `TimelockController`: Add the `CallSalt` event to emit on operation schedule. ([#4001](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4001)) +- Reformatted codebase with latest version of Prettier Solidity. ([#3898](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3898)) +- `Math`: optimize `log256` rounding check. ([#3745](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3745)) +- `ERC20Votes`: optimize by using unchecked arithmetic. ([#3748](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3748)) +- `Multicall`: annotate `multicall` function as upgrade safe to not raise a flag for its delegatecall. ([#3961](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3961)) +- `ERC20Pausable`, `ERC721Pausable`, `ERC1155Pausable`: Add note regarding missing public pausing functionality ([#4007](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4007)) +- `ECDSA`: Add a function `toDataWithIntendedValidatorHash` that encodes data with version 0x00 following EIP-191. ([#4063](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4063)) +- `MerkleProof`: optimize by using unchecked arithmetic. ([#3745](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3745)) + ### Breaking changes - `EIP712`: Addition of ERC5267 support requires support for user defined value types, which was released in Solidity version 0.8.8. This requires a pragma change from `^0.8.0` to `^0.8.8`. diff --git a/contracts/access/AccessControl.sol b/contracts/access/AccessControl.sol index 3a73de78b..0ec6be39c 100644 --- a/contracts/access/AccessControl.sol +++ b/contracts/access/AccessControl.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (access/AccessControl.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (access/AccessControl.sol) pragma solidity ^0.8.0; diff --git a/contracts/access/AccessControlDefaultAdminRules.sol b/contracts/access/AccessControlDefaultAdminRules.sol index 07a5b4f70..9de32002a 100644 --- a/contracts/access/AccessControlDefaultAdminRules.sol +++ b/contracts/access/AccessControlDefaultAdminRules.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (access/AccessControlDefaultAdminRules.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (access/AccessControlDefaultAdminRules.sol) pragma solidity ^0.8.0; diff --git a/contracts/access/IAccessControlDefaultAdminRules.sol b/contracts/access/IAccessControlDefaultAdminRules.sol index d28c49d95..434324e50 100644 --- a/contracts/access/IAccessControlDefaultAdminRules.sol +++ b/contracts/access/IAccessControlDefaultAdminRules.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.9.0 (access/IAccessControlDefaultAdminRules.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (access/IAccessControlDefaultAdminRules.sol) pragma solidity ^0.8.0; diff --git a/contracts/access/Ownable.sol b/contracts/access/Ownable.sol index 1378ffb43..c181ea1aa 100644 --- a/contracts/access/Ownable.sol +++ b/contracts/access/Ownable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol) pragma solidity ^0.8.0; diff --git a/contracts/access/Ownable2Step.sol b/contracts/access/Ownable2Step.sol index f5a3d8047..e6f699881 100644 --- a/contracts/access/Ownable2Step.sol +++ b/contracts/access/Ownable2Step.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (access/Ownable2Step.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable2Step.sol) pragma solidity ^0.8.0; diff --git a/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol b/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol index fa0994834..1a201fd32 100644 --- a/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol +++ b/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (crosschain/polygon/CrossChainEnabledPolygonChild.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (crosschain/polygon/CrossChainEnabledPolygonChild.sol) pragma solidity ^0.8.4; diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol index fe67eb54f..45cf9ce52 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (finance/VestingWallet.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (finance/VestingWallet.sol) pragma solidity ^0.8.0; import "../token/ERC20/utils/SafeERC20.sol"; diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index 241d6139b..390d2b94a 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (governance/Governor.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/Governor.sol) pragma solidity ^0.8.0; diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index e4ad83e87..492d95b9a 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (governance/IGovernor.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/IGovernor.sol) pragma solidity ^0.8.0; diff --git a/contracts/governance/TimelockController.sol b/contracts/governance/TimelockController.sol index e330cca06..f70a7d11d 100644 --- a/contracts/governance/TimelockController.sol +++ b/contracts/governance/TimelockController.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.2) (governance/TimelockController.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/TimelockController.sol) pragma solidity ^0.8.0; diff --git a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol index 1332ac79d..ecc94fba0 100644 --- a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.3) (governance/compatibility/GovernorCompatibilityBravo.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/compatibility/GovernorCompatibilityBravo.sol) pragma solidity ^0.8.0; diff --git a/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol b/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol index 7aa806a18..e64a66a66 100644 --- a/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (governance/compatibility/IGovernorCompatibilityBravo.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/compatibility/IGovernorCompatibilityBravo.sol) pragma solidity ^0.8.0; diff --git a/contracts/governance/extensions/GovernorCountingSimple.sol b/contracts/governance/extensions/GovernorCountingSimple.sol index f3eea9d7f..b9517445a 100644 --- a/contracts/governance/extensions/GovernorCountingSimple.sol +++ b/contracts/governance/extensions/GovernorCountingSimple.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (governance/extensions/GovernorCountingSimple.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorCountingSimple.sol) pragma solidity ^0.8.0; diff --git a/contracts/governance/extensions/GovernorPreventLateQuorum.sol b/contracts/governance/extensions/GovernorPreventLateQuorum.sol index 68496ca1e..752a92cf1 100644 --- a/contracts/governance/extensions/GovernorPreventLateQuorum.sol +++ b/contracts/governance/extensions/GovernorPreventLateQuorum.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (governance/extensions/GovernorPreventLateQuorum.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorPreventLateQuorum.sol) pragma solidity ^0.8.0; diff --git a/contracts/governance/extensions/GovernorSettings.sol b/contracts/governance/extensions/GovernorSettings.sol index 527f41cd8..ec6a98300 100644 --- a/contracts/governance/extensions/GovernorSettings.sol +++ b/contracts/governance/extensions/GovernorSettings.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (governance/extensions/GovernorSettings.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorSettings.sol) pragma solidity ^0.8.0; diff --git a/contracts/governance/extensions/GovernorTimelockCompound.sol b/contracts/governance/extensions/GovernorTimelockCompound.sol index 912171cc3..a706da6a8 100644 --- a/contracts/governance/extensions/GovernorTimelockCompound.sol +++ b/contracts/governance/extensions/GovernorTimelockCompound.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (governance/extensions/GovernorTimelockCompound.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorTimelockCompound.sol) pragma solidity ^0.8.0; diff --git a/contracts/governance/extensions/GovernorTimelockControl.sol b/contracts/governance/extensions/GovernorTimelockControl.sol index 0cf2ea5f0..7cb60bab1 100644 --- a/contracts/governance/extensions/GovernorTimelockControl.sol +++ b/contracts/governance/extensions/GovernorTimelockControl.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (governance/extensions/GovernorTimelockControl.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorTimelockControl.sol) pragma solidity ^0.8.0; diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol index 644317111..c2e65cba9 100644 --- a/contracts/governance/extensions/GovernorVotes.sol +++ b/contracts/governance/extensions/GovernorVotes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (governance/extensions/GovernorVotes.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorVotes.sol) pragma solidity ^0.8.0; diff --git a/contracts/governance/extensions/GovernorVotesComp.sol b/contracts/governance/extensions/GovernorVotesComp.sol index 17250ad7b..e7d7c2c7c 100644 --- a/contracts/governance/extensions/GovernorVotesComp.sol +++ b/contracts/governance/extensions/GovernorVotesComp.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (governance/extensions/GovernorVotesComp.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorVotesComp.sol) pragma solidity ^0.8.0; diff --git a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol index 403570260..097a79670 100644 --- a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol +++ b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (governance/extensions/GovernorVotesQuorumFraction.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorVotesQuorumFraction.sol) pragma solidity ^0.8.0; diff --git a/contracts/governance/utils/IVotes.sol b/contracts/governance/utils/IVotes.sol index 4f2b7eb85..647f79655 100644 --- a/contracts/governance/utils/IVotes.sol +++ b/contracts/governance/utils/IVotes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (governance/utils/IVotes.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/utils/IVotes.sol) pragma solidity ^0.8.0; /** diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index b24ce824a..f0a53ba3f 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (governance/utils/Votes.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/utils/Votes.sol) pragma solidity ^0.8.0; import "../../interfaces/IERC5805.sol"; diff --git a/contracts/interfaces/IERC1363.sol b/contracts/interfaces/IERC1363.sol index 63d87b962..817f3dafc 100644 --- a/contracts/interfaces/IERC1363.sol +++ b/contracts/interfaces/IERC1363.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1363.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC1363.sol) pragma solidity ^0.8.0; diff --git a/contracts/interfaces/IERC1363Receiver.sol b/contracts/interfaces/IERC1363Receiver.sol index f5e7a0c28..382d7f111 100644 --- a/contracts/interfaces/IERC1363Receiver.sol +++ b/contracts/interfaces/IERC1363Receiver.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1363Receiver.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC1363Receiver.sol) pragma solidity ^0.8.0; diff --git a/contracts/interfaces/IERC1363Spender.sol b/contracts/interfaces/IERC1363Spender.sol index 16dd5e0fe..09a7bd278 100644 --- a/contracts/interfaces/IERC1363Spender.sol +++ b/contracts/interfaces/IERC1363Spender.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1363Spender.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC1363Spender.sol) pragma solidity ^0.8.0; diff --git a/contracts/interfaces/IERC1967.sol b/contracts/interfaces/IERC1967.sol index ab4450eec..6fb112a2e 100644 --- a/contracts/interfaces/IERC1967.sol +++ b/contracts/interfaces/IERC1967.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC1967.sol) pragma solidity ^0.8.0; diff --git a/contracts/interfaces/IERC2612.sol b/contracts/interfaces/IERC2612.sol index 6dfdf6f63..cd5fca4cc 100644 --- a/contracts/interfaces/IERC2612.sol +++ b/contracts/interfaces/IERC2612.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC2612.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC2612.sol) pragma solidity ^0.8.0; diff --git a/contracts/interfaces/IERC2981.sol b/contracts/interfaces/IERC2981.sol index 1c9448a91..465b872ee 100644 --- a/contracts/interfaces/IERC2981.sol +++ b/contracts/interfaces/IERC2981.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (interfaces/IERC2981.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC2981.sol) pragma solidity ^0.8.0; diff --git a/contracts/interfaces/IERC3156FlashBorrower.sol b/contracts/interfaces/IERC3156FlashBorrower.sol index 0428391fc..84bd72150 100644 --- a/contracts/interfaces/IERC3156FlashBorrower.sol +++ b/contracts/interfaces/IERC3156FlashBorrower.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (interfaces/IERC3156FlashBorrower.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC3156FlashBorrower.sol) pragma solidity ^0.8.0; diff --git a/contracts/interfaces/IERC4626.sol b/contracts/interfaces/IERC4626.sol index 08e5de717..77dd96a05 100644 --- a/contracts/interfaces/IERC4626.sol +++ b/contracts/interfaces/IERC4626.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (interfaces/IERC4626.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC4626.sol) pragma solidity ^0.8.0; diff --git a/contracts/interfaces/IERC4906.sol b/contracts/interfaces/IERC4906.sol index c9eaa1296..f5a6e00f9 100644 --- a/contracts/interfaces/IERC4906.sol +++ b/contracts/interfaces/IERC4906.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC4906.sol) pragma solidity ^0.8.0; diff --git a/contracts/interfaces/IERC5267.sol b/contracts/interfaces/IERC5267.sol index 3adc4a703..4d3a6b92a 100644 --- a/contracts/interfaces/IERC5267.sol +++ b/contracts/interfaces/IERC5267.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC5267.sol) pragma solidity ^0.8.0; diff --git a/contracts/interfaces/IERC5313.sol b/contracts/interfaces/IERC5313.sol index 2c9a47da9..e26094c06 100644 --- a/contracts/interfaces/IERC5313.sol +++ b/contracts/interfaces/IERC5313.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC5313.sol) pragma solidity ^0.8.0; diff --git a/contracts/interfaces/IERC5805.sol b/contracts/interfaces/IERC5805.sol index 2c2e5e345..a012ccb15 100644 --- a/contracts/interfaces/IERC5805.sol +++ b/contracts/interfaces/IERC5805.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (interfaces/IERC5805.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC5805.sol) pragma solidity ^0.8.0; diff --git a/contracts/interfaces/IERC6372.sol b/contracts/interfaces/IERC6372.sol index e1a0bf8b0..4c5fe039c 100644 --- a/contracts/interfaces/IERC6372.sol +++ b/contracts/interfaces/IERC6372.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (interfaces/IERC6372.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC6372.sol) pragma solidity ^0.8.0; diff --git a/contracts/interfaces/draft-IERC2612.sol b/contracts/interfaces/draft-IERC2612.sol index 1ea7bf1c0..024b753e2 100644 --- a/contracts/interfaces/draft-IERC2612.sol +++ b/contracts/interfaces/draft-IERC2612.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/draft-IERC2612.sol) pragma solidity ^0.8.0; diff --git a/contracts/metatx/MinimalForwarder.sol b/contracts/metatx/MinimalForwarder.sol index 9298ae675..de2646269 100644 --- a/contracts/metatx/MinimalForwarder.sol +++ b/contracts/metatx/MinimalForwarder.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (metatx/MinimalForwarder.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (metatx/MinimalForwarder.sol) pragma solidity ^0.8.0; diff --git a/contracts/package.json b/contracts/package.json index 55e70b179..4d0f576bb 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,7 +1,7 @@ { "name": "@openzeppelin/contracts", "description": "Secure Smart Contract library for Solidity", - "version": "4.8.2", + "version": "4.9.0", "files": [ "**/*.sol", "/build/contracts/*.json", diff --git a/contracts/proxy/Clones.sol b/contracts/proxy/Clones.sol index 712519892..583e0223e 100644 --- a/contracts/proxy/Clones.sol +++ b/contracts/proxy/Clones.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (proxy/Clones.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/Clones.sol) pragma solidity ^0.8.0; diff --git a/contracts/proxy/ERC1967/ERC1967Upgrade.sol b/contracts/proxy/ERC1967/ERC1967Upgrade.sol index 3942ca699..cc9e5695b 100644 --- a/contracts/proxy/ERC1967/ERC1967Upgrade.sol +++ b/contracts/proxy/ERC1967/ERC1967Upgrade.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.3) (proxy/ERC1967/ERC1967Upgrade.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/ERC1967/ERC1967Upgrade.sol) pragma solidity ^0.8.2; diff --git a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol index e49768ab9..6f18b492f 100644 --- a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.3) (proxy/transparent/TransparentUpgradeableProxy.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/transparent/TransparentUpgradeableProxy.sol) pragma solidity ^0.8.0; diff --git a/contracts/proxy/utils/Initializable.sol b/contracts/proxy/utils/Initializable.sol index 3c898ec5a..33ec22791 100644 --- a/contracts/proxy/utils/Initializable.sol +++ b/contracts/proxy/utils/Initializable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.1) (proxy/utils/Initializable.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/Initializable.sol) pragma solidity ^0.8.2; diff --git a/contracts/proxy/utils/UUPSUpgradeable.sol b/contracts/proxy/utils/UUPSUpgradeable.sol index 4ff026638..86684cfc3 100644 --- a/contracts/proxy/utils/UUPSUpgradeable.sol +++ b/contracts/proxy/utils/UUPSUpgradeable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (proxy/utils/UUPSUpgradeable.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/UUPSUpgradeable.sol) pragma solidity ^0.8.0; diff --git a/contracts/security/ReentrancyGuard.sol b/contracts/security/ReentrancyGuard.sol index f9281ec64..dac508b6a 100644 --- a/contracts/security/ReentrancyGuard.sol +++ b/contracts/security/ReentrancyGuard.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (security/ReentrancyGuard.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol index 445c3179d..2da521d55 100644 --- a/contracts/token/ERC1155/ERC1155.sol +++ b/contracts/token/ERC1155/ERC1155.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC1155/ERC1155.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC1155/ERC1155.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC1155/IERC1155.sol b/contracts/token/ERC1155/IERC1155.sol index eae0b7029..3446e604a 100644 --- a/contracts/token/ERC1155/IERC1155.sol +++ b/contracts/token/ERC1155/IERC1155.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC1155/IERC1155.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC1155/IERC1155.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC1155/extensions/ERC1155Burnable.sol b/contracts/token/ERC1155/extensions/ERC1155Burnable.sol index cc81957a7..9ee371986 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Burnable.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Burnable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC1155/extensions/ERC1155Burnable.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC1155/extensions/ERC1155Burnable.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC1155/presets/ERC1155PresetMinterPauser.sol b/contracts/token/ERC1155/presets/ERC1155PresetMinterPauser.sol index fd7729aa7..2f68dcaf5 100644 --- a/contracts/token/ERC1155/presets/ERC1155PresetMinterPauser.sol +++ b/contracts/token/ERC1155/presets/ERC1155PresetMinterPauser.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/presets/ERC1155PresetMinterPauser.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC1155/presets/ERC1155PresetMinterPauser.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/ERC20.sol b/contracts/token/ERC20/ERC20.sol index 7c53c6962..91b7f98f6 100644 --- a/contracts/token/ERC20/ERC20.sol +++ b/contracts/token/ERC20/ERC20.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/ERC20.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/IERC20.sol b/contracts/token/ERC20/IERC20.sol index 66c4e4d88..6d5b4e9f1 100644 --- a/contracts/token/ERC20/IERC20.sol +++ b/contracts/token/ERC20/IERC20.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/extensions/ERC20Pausable.sol b/contracts/token/ERC20/extensions/ERC20Pausable.sol index 36cc30ce4..3b2b6329a 100644 --- a/contracts/token/ERC20/extensions/ERC20Pausable.sol +++ b/contracts/token/ERC20/extensions/ERC20Pausable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.2) (token/ERC20/extensions/ERC20Pausable.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC20Pausable.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/extensions/ERC20Permit.sol b/contracts/token/ERC20/extensions/ERC20Permit.sol index a357199b1..7ee7331f7 100644 --- a/contracts/token/ERC20/extensions/ERC20Permit.sol +++ b/contracts/token/ERC20/extensions/ERC20Permit.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/extensions/ERC20Permit.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC20Permit.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/extensions/ERC20Snapshot.sol b/contracts/token/ERC20/extensions/ERC20Snapshot.sol index ee104b0ec..504e7a921 100644 --- a/contracts/token/ERC20/extensions/ERC20Snapshot.sol +++ b/contracts/token/ERC20/extensions/ERC20Snapshot.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC20/extensions/ERC20Snapshot.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC20Snapshot.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index f44b89604..8b31fda6e 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.1) (token/ERC20/extensions/ERC20Votes.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC20Votes.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/extensions/ERC20Wrapper.sol b/contracts/token/ERC20/extensions/ERC20Wrapper.sol index bfe782e43..bc85a0b37 100644 --- a/contracts/token/ERC20/extensions/ERC20Wrapper.sol +++ b/contracts/token/ERC20/extensions/ERC20Wrapper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/extensions/ERC20Wrapper.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC20Wrapper.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index 40e9cf2b3..4f6a2d64e 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.1) (token/ERC20/extensions/ERC4626.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC4626.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/extensions/IERC20Permit.sol b/contracts/token/ERC20/extensions/IERC20Permit.sol index bb43e53b6..0deb54b14 100644 --- a/contracts/token/ERC20/extensions/IERC20Permit.sol +++ b/contracts/token/ERC20/extensions/IERC20Permit.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Permit.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/IERC20Permit.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/extensions/draft-ERC20Permit.sol b/contracts/token/ERC20/extensions/draft-ERC20Permit.sol index 6579ef33f..55b38b268 100644 --- a/contracts/token/ERC20/extensions/draft-ERC20Permit.sol +++ b/contracts/token/ERC20/extensions/draft-ERC20Permit.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/extensions/draft-ERC20Permit.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/draft-ERC20Permit.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/extensions/draft-IERC20Permit.sol b/contracts/token/ERC20/extensions/draft-IERC20Permit.sol index 1df6c537d..ff6ede3cc 100644 --- a/contracts/token/ERC20/extensions/draft-IERC20Permit.sol +++ b/contracts/token/ERC20/extensions/draft-IERC20Permit.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/draft-IERC20Permit.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol b/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol index e8268145d..d293999cd 100644 --- a/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol +++ b/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/presets/ERC20PresetFixedSupply.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/presets/ERC20PresetFixedSupply.sol) pragma solidity ^0.8.0; import "../extensions/ERC20Burnable.sol"; diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index 2f76386d5..47d1684f1 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/utils/SafeERC20.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/utils/SafeERC20.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/utils/TokenTimelock.sol b/contracts/token/ERC20/utils/TokenTimelock.sol index ed855b7bc..cffdac5d4 100644 --- a/contracts/token/ERC20/utils/TokenTimelock.sol +++ b/contracts/token/ERC20/utils/TokenTimelock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/utils/TokenTimelock.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/utils/TokenTimelock.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 428338d90..7942e6fe4 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.2) (token/ERC721/ERC721.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/ERC721.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC721/IERC721.sol b/contracts/token/ERC721/IERC721.sol index 7b60a9f51..293f56683 100644 --- a/contracts/token/ERC721/IERC721.sol +++ b/contracts/token/ERC721/IERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/IERC721.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/IERC721.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC721/extensions/ERC721Consecutive.sol b/contracts/token/ERC721/extensions/ERC721Consecutive.sol index 9451c8c59..4312d9849 100644 --- a/contracts/token/ERC721/extensions/ERC721Consecutive.sol +++ b/contracts/token/ERC721/extensions/ERC721Consecutive.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.2) (token/ERC721/extensions/ERC721Consecutive.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/extensions/ERC721Consecutive.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC721/extensions/ERC721URIStorage.sol b/contracts/token/ERC721/extensions/ERC721URIStorage.sol index 201b05a81..c9d79b6b2 100644 --- a/contracts/token/ERC721/extensions/ERC721URIStorage.sol +++ b/contracts/token/ERC721/extensions/ERC721URIStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC721/extensions/ERC721URIStorage.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/extensions/ERC721URIStorage.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 31397f107..55b82c1c1 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/extensions/ERC721Votes.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/extensions/ERC721Votes.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC721/extensions/ERC721Wrapper.sol b/contracts/token/ERC721/extensions/ERC721Wrapper.sol index 83e59ce88..c7ff06dec 100644 --- a/contracts/token/ERC721/extensions/ERC721Wrapper.sol +++ b/contracts/token/ERC721/extensions/ERC721Wrapper.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/extensions/ERC721Wrapper.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol b/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol index 478e5808d..da53b88bd 100644 --- a/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol +++ b/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC721/utils/ERC721Holder.sol b/contracts/token/ERC721/utils/ERC721Holder.sol index cfa533a47..f18ad8a1d 100644 --- a/contracts/token/ERC721/utils/ERC721Holder.sol +++ b/contracts/token/ERC721/utils/ERC721Holder.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (token/ERC721/utils/ERC721Holder.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/utils/ERC721Holder.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC777/ERC777.sol b/contracts/token/ERC777/ERC777.sol index aa95b1843..c9a17d862 100644 --- a/contracts/token/ERC777/ERC777.sol +++ b/contracts/token/ERC777/ERC777.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC777/ERC777.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC777/ERC777.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC777/IERC777.sol b/contracts/token/ERC777/IERC777.sol index d3bede626..2d4f6df88 100644 --- a/contracts/token/ERC777/IERC777.sol +++ b/contracts/token/ERC777/IERC777.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC777/IERC777.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC777/IERC777.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/common/ERC2981.sol b/contracts/token/common/ERC2981.sol index f49cc8d98..7d47b6c7e 100644 --- a/contracts/token/common/ERC2981.sol +++ b/contracts/token/common/ERC2981.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (token/common/ERC2981.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/common/ERC2981.sol) pragma solidity ^0.8.0; diff --git a/contracts/utils/Address.sol b/contracts/utils/Address.sol index 5ff14140a..0791a66a7 100644 --- a/contracts/utils/Address.sol +++ b/contracts/utils/Address.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/Address.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Address.sol) pragma solidity ^0.8.1; diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index 24964759a..66fe322b5 100644 --- a/contracts/utils/Arrays.sol +++ b/contracts/utils/Arrays.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/Arrays.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Arrays.sol) pragma solidity ^0.8.0; diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index 1bb66dec9..9d4f0f96e 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.1) (utils/Checkpoints.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Checkpoints.sol) // This file was procedurally generated from scripts/generate/templates/Checkpoints.js. pragma solidity ^0.8.0; diff --git a/contracts/utils/Create2.sol b/contracts/utils/Create2.sol index 2255a4df8..29fa97329 100644 --- a/contracts/utils/Create2.sol +++ b/contracts/utils/Create2.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/Create2.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Create2.sol) pragma solidity ^0.8.0; diff --git a/contracts/utils/Multicall.sol b/contracts/utils/Multicall.sol index 7470c5595..5729f8452 100644 --- a/contracts/utils/Multicall.sol +++ b/contracts/utils/Multicall.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (utils/Multicall.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Multicall.sol) pragma solidity ^0.8.0; diff --git a/contracts/utils/ShortStrings.sol b/contracts/utils/ShortStrings.sol index a8904777a..90a054999 100644 --- a/contracts/utils/ShortStrings.sol +++ b/contracts/utils/ShortStrings.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (utils/ShortStrings.sol) pragma solidity ^0.8.8; diff --git a/contracts/utils/StorageSlot.sol b/contracts/utils/StorageSlot.sol index 44285c900..df62b8f7b 100644 --- a/contracts/utils/StorageSlot.sol +++ b/contracts/utils/StorageSlot.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (utils/StorageSlot.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/StorageSlot.sol) // This file was procedurally generated from scripts/generate/templates/StorageSlot.js. pragma solidity ^0.8.0; diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 3a037f477..657ebd663 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/Strings.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Strings.sol) pragma solidity ^0.8.0; diff --git a/contracts/utils/Timers.sol b/contracts/utils/Timers.sol index 1c92b029b..04f866452 100644 --- a/contracts/utils/Timers.sol +++ b/contracts/utils/Timers.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/Timers.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Timers.sol) pragma solidity ^0.8.0; diff --git a/contracts/utils/cryptography/ECDSA.sol b/contracts/utils/cryptography/ECDSA.sol index 77279eb4f..4326e5b66 100644 --- a/contracts/utils/cryptography/ECDSA.sol +++ b/contracts/utils/cryptography/ECDSA.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/ECDSA.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/ECDSA.sol) pragma solidity ^0.8.0; diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol index 6a4e1cad2..2a0e73486 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/EIP712.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/EIP712.sol) pragma solidity ^0.8.8; diff --git a/contracts/utils/cryptography/MerkleProof.sol b/contracts/utils/cryptography/MerkleProof.sol index 3862fdbfe..cd79e51cf 100644 --- a/contracts/utils/cryptography/MerkleProof.sol +++ b/contracts/utils/cryptography/MerkleProof.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/MerkleProof.sol) pragma solidity ^0.8.0; diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index b81cf40be..1815d27fe 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/SignatureChecker.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/SignatureChecker.sol) pragma solidity ^0.8.0; diff --git a/contracts/utils/introspection/ERC165Checker.sol b/contracts/utils/introspection/ERC165Checker.sol index fd51159cd..4831b6522 100644 --- a/contracts/utils/introspection/ERC165Checker.sol +++ b/contracts/utils/introspection/ERC165Checker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.2) (utils/introspection/ERC165Checker.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/introspection/ERC165Checker.sol) pragma solidity ^0.8.0; diff --git a/contracts/utils/introspection/ERC1820Implementer.sol b/contracts/utils/introspection/ERC1820Implementer.sol index cf4b50498..7a684eded 100644 --- a/contracts/utils/introspection/ERC1820Implementer.sol +++ b/contracts/utils/introspection/ERC1820Implementer.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC1820Implementer.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/introspection/ERC1820Implementer.sol) pragma solidity ^0.8.0; diff --git a/contracts/utils/introspection/IERC1820Registry.sol b/contracts/utils/introspection/IERC1820Registry.sol index a146bc2a6..922df0408 100644 --- a/contracts/utils/introspection/IERC1820Registry.sol +++ b/contracts/utils/introspection/IERC1820Registry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/introspection/IERC1820Registry.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/introspection/IERC1820Registry.sol) pragma solidity ^0.8.0; diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index f8e7ca0a9..551b1a512 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/math/Math.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/math/Math.sol) pragma solidity ^0.8.0; diff --git a/contracts/utils/math/SafeMath.sol b/contracts/utils/math/SafeMath.sol index 2f48fb736..f7315a61e 100644 --- a/contracts/utils/math/SafeMath.sol +++ b/contracts/utils/math/SafeMath.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (utils/math/SafeMath.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/math/SafeMath.sol) pragma solidity ^0.8.0; diff --git a/contracts/utils/structs/BitMaps.sol b/contracts/utils/structs/BitMaps.sol index eb67bfab0..2567fce47 100644 --- a/contracts/utils/structs/BitMaps.sol +++ b/contracts/utils/structs/BitMaps.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/structs/BitMaps.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/BitMaps.sol) pragma solidity ^0.8.0; /** diff --git a/contracts/utils/structs/DoubleEndedQueue.sol b/contracts/utils/structs/DoubleEndedQueue.sol index 6b3ea70e3..325918d2d 100644 --- a/contracts/utils/structs/DoubleEndedQueue.sol +++ b/contracts/utils/structs/DoubleEndedQueue.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (utils/structs/DoubleEndedQueue.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/DoubleEndedQueue.sol) pragma solidity ^0.8.4; import "../math/SafeCast.sol"; diff --git a/contracts/utils/structs/EnumerableMap.sol b/contracts/utils/structs/EnumerableMap.sol index fb21f02cf..c49d2d675 100644 --- a/contracts/utils/structs/EnumerableMap.sol +++ b/contracts/utils/structs/EnumerableMap.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/structs/EnumerableMap.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/EnumerableMap.sol) // This file was procedurally generated from scripts/generate/templates/EnumerableMap.js. pragma solidity ^0.8.0; diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index a01f82d41..447f96302 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/structs/EnumerableSet.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/EnumerableSet.sol) // This file was procedurally generated from scripts/generate/templates/EnumerableSet.js. pragma solidity ^0.8.0; diff --git a/contracts/vendor/amb/IAMB.sol b/contracts/vendor/amb/IAMB.sol index 73a2bd24b..658f7260e 100644 --- a/contracts/vendor/amb/IAMB.sol +++ b/contracts/vendor/amb/IAMB.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (vendor/amb/IAMB.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (vendor/amb/IAMB.sol) pragma solidity ^0.8.0; interface IAMB { diff --git a/contracts/vendor/arbitrum/IArbSys.sol b/contracts/vendor/arbitrum/IArbSys.sol index 9b79d5c16..ea4e7504f 100644 --- a/contracts/vendor/arbitrum/IArbSys.sol +++ b/contracts/vendor/arbitrum/IArbSys.sol @@ -1,7 +1,7 @@ // Copyright 2021-2022, Offchain Labs, Inc. // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE // SPDX-License-Identifier: BUSL-1.1 -// OpenZeppelin Contracts (last updated v4.8.0) (vendor/arbitrum/IArbSys.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (vendor/arbitrum/IArbSys.sol) pragma solidity >=0.4.21 <0.9.0; diff --git a/contracts/vendor/arbitrum/IBridge.sol b/contracts/vendor/arbitrum/IBridge.sol index e71bedce0..398fb6130 100644 --- a/contracts/vendor/arbitrum/IBridge.sol +++ b/contracts/vendor/arbitrum/IBridge.sol @@ -1,7 +1,7 @@ // Copyright 2021-2022, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE // SPDX-License-Identifier: BUSL-1.1 -// OpenZeppelin Contracts (last updated v4.8.0) (vendor/arbitrum/IBridge.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (vendor/arbitrum/IBridge.sol) // solhint-disable-next-line compiler-version pragma solidity >=0.6.9 <0.9.0; diff --git a/contracts/vendor/arbitrum/IOutbox.sol b/contracts/vendor/arbitrum/IOutbox.sol index 22fa58f40..359ea6c1f 100644 --- a/contracts/vendor/arbitrum/IOutbox.sol +++ b/contracts/vendor/arbitrum/IOutbox.sol @@ -1,7 +1,7 @@ // Copyright 2021-2022, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE // SPDX-License-Identifier: BUSL-1.1 -// OpenZeppelin Contracts (last updated v4.8.0) (vendor/arbitrum/IOutbox.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (vendor/arbitrum/IOutbox.sol) // solhint-disable-next-line compiler-version pragma solidity >=0.6.9 <0.9.0; diff --git a/contracts/vendor/optimism/ICrossDomainMessenger.sol b/contracts/vendor/optimism/ICrossDomainMessenger.sol index cc01a48ab..461b88f6f 100644 --- a/contracts/vendor/optimism/ICrossDomainMessenger.sol +++ b/contracts/vendor/optimism/ICrossDomainMessenger.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (vendor/optimism/ICrossDomainMessenger.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (vendor/optimism/ICrossDomainMessenger.sol) pragma solidity >0.5.0 <0.9.0; /** diff --git a/contracts/vendor/polygon/IFxMessageProcessor.sol b/contracts/vendor/polygon/IFxMessageProcessor.sol index be73e6f53..1c7b6f6f7 100644 --- a/contracts/vendor/polygon/IFxMessageProcessor.sol +++ b/contracts/vendor/polygon/IFxMessageProcessor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (vendor/polygon/IFxMessageProcessor.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (vendor/polygon/IFxMessageProcessor.sol) pragma solidity ^0.8.0; interface IFxMessageProcessor { diff --git a/package.json b/package.json index b198a0823..c96956a32 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openzeppelin-solidity", "description": "Secure Smart Contract library for Solidity", - "version": "4.8.2", + "version": "4.9.0", "files": [ "/contracts/**/*.sol", "/build/contracts/*.json", From 25edd3cd628312baae4741544044ed4ed96767c6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 25 May 2023 21:27:48 +0200 Subject: [PATCH 143/197] Use SafeERC20.forceApprove in safeIncreaseAllowance and safeDecreaseAllowance (#4260) Co-authored-by: Francisco --- .changeset/wild-windows-trade.md | 5 +++++ contracts/token/ERC20/utils/SafeERC20.sol | 4 ++-- test/token/ERC20/utils/SafeERC20.test.js | 11 +++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 .changeset/wild-windows-trade.md diff --git a/.changeset/wild-windows-trade.md b/.changeset/wild-windows-trade.md new file mode 100644 index 000000000..f599d0fcb --- /dev/null +++ b/.changeset/wild-windows-trade.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`SafeERC20`: Refactor `safeDecreaseAllowance` and `safeIncreaseAllowance` to support USDT-like tokens. diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index f24447fb4..7248134f2 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -41,7 +41,7 @@ library SafeERC20 { */ function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { uint256 oldAllowance = token.allowance(address(this), spender); - _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance + value)); + forceApprove(token, spender, oldAllowance + value); } /** @@ -52,7 +52,7 @@ library SafeERC20 { unchecked { uint256 oldAllowance = token.allowance(address(this), spender); require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); - _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance - value)); + forceApprove(token, spender, oldAllowance - value); } } diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index 04b3b5cb1..b0daf4384 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -182,16 +182,19 @@ contract('SafeERC20', function (accounts) { await this.token.$_approve(this.mock.address, spender, 100); }); - it('safeApprove can increase approval', async function () { - await expectRevert(this.mock.$safeIncreaseAllowance(this.token.address, spender, 10), 'USDT approval failure'); + it('safeIncreaseAllowance works', async function () { + await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10); + expect(this.token.allowance(this.mock.address, spender, 90)); }); - it('safeApprove can decrease approval', async function () { - await expectRevert(this.mock.$safeDecreaseAllowance(this.token.address, spender, 10), 'USDT approval failure'); + it('safeDecreaseAllowance works', async function () { + await this.mock.$safeDecreaseAllowance(this.token.address, spender, 10); + expect(this.token.allowance(this.mock.address, spender, 110)); }); it('forceApprove works', async function () { await this.mock.$forceApprove(this.token.address, spender, 200); + expect(this.token.allowance(this.mock.address, spender, 200)); }); }); }); From 5420879d9b834a0579423d668fb60c5fc13b60cc Mon Sep 17 00:00:00 2001 From: kfishnchips <110192548+kfishnchips@users.noreply.github.com> Date: Fri, 26 May 2023 12:07:16 -0400 Subject: [PATCH 144/197] Ability to set starting token id for ERC721Consecutive (#4097) Co-authored-by: Hadrien Croubois Co-authored-by: ernestognw --- .changeset/spotty-hotels-type.md | 5 + .../mocks/token/ERC721ConsecutiveMock.sol | 9 + .../ERC721/extensions/ERC721Consecutive.sol | 43 ++- .../ERC721/extensions/ERC721Consecutive.t.sol | 90 +++++- .../extensions/ERC721Consecutive.test.js | 284 +++++++++--------- 5 files changed, 263 insertions(+), 168 deletions(-) create mode 100644 .changeset/spotty-hotels-type.md diff --git a/.changeset/spotty-hotels-type.md b/.changeset/spotty-hotels-type.md new file mode 100644 index 000000000..866d8fc02 --- /dev/null +++ b/.changeset/spotty-hotels-type.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ERC721Consecutive`: Add a `_firstConsecutiveId` internal function that can be overridden to change the id of the first token minted through `_mintConsecutive`. diff --git a/contracts/mocks/token/ERC721ConsecutiveMock.sol b/contracts/mocks/token/ERC721ConsecutiveMock.sol index 427f44a19..65859f956 100644 --- a/contracts/mocks/token/ERC721ConsecutiveMock.sol +++ b/contracts/mocks/token/ERC721ConsecutiveMock.sol @@ -11,13 +11,18 @@ import "../../token/ERC721/extensions/draft-ERC721Votes.sol"; * @title ERC721ConsecutiveMock */ contract ERC721ConsecutiveMock is ERC721Consecutive, ERC721Pausable, ERC721Votes { + uint96 private immutable _offset; + constructor( string memory name, string memory symbol, + uint96 offset, address[] memory delegates, address[] memory receivers, uint96[] memory amounts ) ERC721(name, symbol) EIP712(name, "1") { + _offset = offset; + for (uint256 i = 0; i < delegates.length; ++i) { _delegate(delegates[i], delegates[i]); } @@ -27,6 +32,10 @@ contract ERC721ConsecutiveMock is ERC721Consecutive, ERC721Pausable, ERC721Votes } } + function _firstConsecutiveId() internal view virtual override returns (uint96) { + return _offset; + } + function _ownerOf(uint256 tokenId) internal view virtual override(ERC721, ERC721Consecutive) returns (address) { return super._ownerOf(tokenId); } diff --git a/contracts/token/ERC721/extensions/ERC721Consecutive.sol b/contracts/token/ERC721/extensions/ERC721Consecutive.sol index 4312d9849..abdba5b90 100644 --- a/contracts/token/ERC721/extensions/ERC721Consecutive.sol +++ b/contracts/token/ERC721/extensions/ERC721Consecutive.sol @@ -20,8 +20,8 @@ import "../../../utils/structs/BitMaps.sol"; * regained after construction. During construction, only batch minting is allowed. * * IMPORTANT: This extension bypasses the hooks {_beforeTokenTransfer} and {_afterTokenTransfer} for tokens minted in - * batch. When using this extension, you should consider the {_beforeConsecutiveTokenTransfer} and - * {_afterConsecutiveTokenTransfer} hooks in addition to {_beforeTokenTransfer} and {_afterTokenTransfer}. + * batch. The hooks will be only called once per batch, so you should take `batchSize` parameter into consideration + * when relying on hooks. * * IMPORTANT: When overriding {_afterTokenTransfer}, be careful about call ordering. {ownerOf} may return invalid * values during the {_afterTokenTransfer} execution if the super call is not called first. To be safe, execute the @@ -56,7 +56,7 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { address owner = super._ownerOf(tokenId); // If token is owned by the core, or beyond consecutive range, return base value - if (owner != address(0) || tokenId > type(uint96).max) { + if (owner != address(0) || tokenId > type(uint96).max || tokenId < _firstConsecutiveId()) { return owner; } @@ -82,7 +82,7 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { * Emits a {IERC2309-ConsecutiveTransfer} event. */ function _mintConsecutive(address to, uint96 batchSize) internal virtual returns (uint96) { - uint96 first = _totalConsecutiveSupply(); + uint96 next = _nextConsecutiveId(); // minting a batch of size 0 is a no-op if (batchSize > 0) { @@ -91,29 +91,29 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { require(batchSize <= _maxBatchSize(), "ERC721Consecutive: batch too large"); // hook before - _beforeTokenTransfer(address(0), to, first, batchSize); + _beforeTokenTransfer(address(0), to, next, batchSize); // push an ownership checkpoint & emit event - uint96 last = first + batchSize - 1; + uint96 last = next + batchSize - 1; _sequentialOwnership.push(last, uint160(to)); // The invariant required by this function is preserved because the new sequentialOwnership checkpoint // is attributing ownership of `batchSize` new tokens to account `to`. __unsafe_increaseBalance(to, batchSize); - emit ConsecutiveTransfer(first, last, address(0), to); + emit ConsecutiveTransfer(next, last, address(0), to); // hook after - _afterTokenTransfer(address(0), to, first, batchSize); + _afterTokenTransfer(address(0), to, next, batchSize); } - return first; + return next; } /** * @dev See {ERC721-_mint}. Override version that restricts normal minting to after construction. * - * Warning: Using {ERC721Consecutive} prevents using {_mint} during construction in favor of {_mintConsecutive}. + * WARNING: Using {ERC721Consecutive} prevents using {_mint} during construction in favor of {_mintConsecutive}. * After construction, {_mintConsecutive} is no longer available and {_mint} becomes available. */ function _mint(address to, uint256 tokenId) internal virtual override { @@ -132,17 +132,30 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { ) internal virtual override { if ( to == address(0) && // if we burn - firstTokenId < _totalConsecutiveSupply() && // and the tokenId was minted in a batch - !_sequentialBurn.get(firstTokenId) // and the token was never marked as burnt - ) { + firstTokenId >= _firstConsecutiveId() && + firstTokenId < _nextConsecutiveId() && + !_sequentialBurn.get(firstTokenId) + ) // and the token was never marked as burnt + { require(batchSize == 1, "ERC721Consecutive: batch burn not supported"); _sequentialBurn.set(firstTokenId); } super._afterTokenTransfer(from, to, firstTokenId, batchSize); } - function _totalConsecutiveSupply() private view returns (uint96) { + /** + * @dev Used to offset the first token id in {_nextConsecutiveId} + */ + function _firstConsecutiveId() internal view virtual returns (uint96) { + return 0; + } + + /** + * @dev Returns the next tokenId to mint using {_mintConsecutive}. It will return {_firstConsecutiveId} + * if no consecutive tokenId has been minted before. + */ + function _nextConsecutiveId() private view returns (uint96) { (bool exists, uint96 latestId, ) = _sequentialOwnership.latestCheckpoint(); - return exists ? latestId + 1 : 0; + return exists ? latestId + 1 : _firstConsecutiveId(); } } diff --git a/test/token/ERC721/extensions/ERC721Consecutive.t.sol b/test/token/ERC721/extensions/ERC721Consecutive.t.sol index 3cc868929..5bcbb024d 100644 --- a/test/token/ERC721/extensions/ERC721Consecutive.t.sol +++ b/test/token/ERC721/extensions/ERC721Consecutive.t.sol @@ -14,9 +14,11 @@ function toSingleton(address account) pure returns (address[] memory) { } contract ERC721ConsecutiveTarget is StdUtils, ERC721Consecutive { + uint96 private immutable _offset; uint256 public totalMinted = 0; - constructor(address[] memory receivers, uint256[] memory batches) ERC721("", "") { + constructor(address[] memory receivers, uint256[] memory batches, uint256 startingId) ERC721("", "") { + _offset = uint96(startingId); for (uint256 i = 0; i < batches.length; i++) { address receiver = receivers[i % receivers.length]; uint96 batchSize = uint96(bound(batches[i], 0, _maxBatchSize())); @@ -28,43 +30,71 @@ contract ERC721ConsecutiveTarget is StdUtils, ERC721Consecutive { function burn(uint256 tokenId) public { _burn(tokenId); } + + function _firstConsecutiveId() internal view virtual override returns (uint96) { + return _offset; + } } contract ERC721ConsecutiveTest is Test { - function test_balance(address receiver, uint256[] calldata batches) public { + function test_balance(address receiver, uint256[] calldata batches, uint96 startingId) public { vm.assume(receiver != address(0)); - ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches); + uint256 startingTokenId = bound(startingId, 0, 5000); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches, startingTokenId); assertEq(token.balanceOf(receiver), token.totalMinted()); } - function test_ownership(address receiver, uint256[] calldata batches, uint256[2] calldata unboundedTokenId) public { + function test_ownership( + address receiver, + uint256[] calldata batches, + uint256[2] calldata unboundedTokenId, + uint96 startingId + ) public { vm.assume(receiver != address(0)); - ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches); + uint256 startingTokenId = bound(startingId, 0, 5000); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches, startingTokenId); if (token.totalMinted() > 0) { - uint256 validTokenId = bound(unboundedTokenId[0], 0, token.totalMinted() - 1); + uint256 validTokenId = bound( + unboundedTokenId[0], + startingTokenId, + startingTokenId + token.totalMinted() - 1 + ); assertEq(token.ownerOf(validTokenId), receiver); } - uint256 invalidTokenId = bound(unboundedTokenId[1], token.totalMinted(), type(uint256).max); + uint256 invalidTokenId = bound( + unboundedTokenId[1], + startingTokenId + token.totalMinted(), + startingTokenId + token.totalMinted() + 1 + ); vm.expectRevert(); token.ownerOf(invalidTokenId); } - function test_burn(address receiver, uint256[] calldata batches, uint256 unboundedTokenId) public { + function test_burn( + address receiver, + uint256[] calldata batches, + uint256 unboundedTokenId, + uint96 startingId + ) public { vm.assume(receiver != address(0)); - ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches); + uint256 startingTokenId = bound(startingId, 0, 5000); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches, startingTokenId); // only test if we minted at least one token uint256 supply = token.totalMinted(); vm.assume(supply > 0); // burn a token in [0; supply[ - uint256 tokenId = bound(unboundedTokenId, 0, supply - 1); + uint256 tokenId = bound(unboundedTokenId, startingTokenId, startingTokenId + supply - 1); token.burn(tokenId); // balance should have decreased @@ -78,25 +108,28 @@ contract ERC721ConsecutiveTest is Test { function test_transfer( address[2] calldata accounts, uint256[2] calldata unboundedBatches, - uint256[2] calldata unboundedTokenId + uint256[2] calldata unboundedTokenId, + uint96 startingId ) public { vm.assume(accounts[0] != address(0)); vm.assume(accounts[1] != address(0)); vm.assume(accounts[0] != accounts[1]); + uint256 startingTokenId = bound(startingId, 1, 5000); + address[] memory receivers = new address[](2); receivers[0] = accounts[0]; receivers[1] = accounts[1]; // We assume _maxBatchSize is 5000 (the default). This test will break otherwise. uint256[] memory batches = new uint256[](2); - batches[0] = bound(unboundedBatches[0], 1, 5000); - batches[1] = bound(unboundedBatches[1], 1, 5000); + batches[0] = bound(unboundedBatches[0], startingTokenId, 5000); + batches[1] = bound(unboundedBatches[1], startingTokenId, 5000); - ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(receivers, batches); + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(receivers, batches, startingTokenId); - uint256 tokenId0 = bound(unboundedTokenId[0], 0, batches[0] - 1); - uint256 tokenId1 = bound(unboundedTokenId[1], 0, batches[1] - 1) + batches[0]; + uint256 tokenId0 = bound(unboundedTokenId[0], startingTokenId, batches[0]); + uint256 tokenId1 = bound(unboundedTokenId[1], startingTokenId, batches[1]) + batches[0]; assertEq(token.ownerOf(tokenId0), accounts[0]); assertEq(token.ownerOf(tokenId1), accounts[1]); @@ -119,4 +152,29 @@ contract ERC721ConsecutiveTest is Test { assertEq(token.balanceOf(accounts[0]), batches[0]); assertEq(token.balanceOf(accounts[1]), batches[1]); } + + function test_start_consecutive_id( + address receiver, + uint256[2] calldata unboundedBatches, + uint256[2] calldata unboundedTokenId, + uint96 startingId + ) public { + vm.assume(receiver != address(0)); + + uint256 startingTokenId = bound(startingId, 1, 5000); + + // We assume _maxBatchSize is 5000 (the default). This test will break otherwise. + uint256[] memory batches = new uint256[](2); + batches[0] = bound(unboundedBatches[0], startingTokenId, 5000); + batches[1] = bound(unboundedBatches[1], startingTokenId, 5000); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches, startingTokenId); + + uint256 tokenId0 = bound(unboundedTokenId[0], startingTokenId, batches[0]); + uint256 tokenId1 = bound(unboundedTokenId[1], startingTokenId, batches[1]); + + assertEq(token.ownerOf(tokenId0), receiver); + assertEq(token.ownerOf(tokenId1), receiver); + assertEq(token.balanceOf(receiver), batches[0] + batches[1]); + } } diff --git a/test/token/ERC721/extensions/ERC721Consecutive.test.js b/test/token/ERC721/extensions/ERC721Consecutive.test.js index a92aef480..4e2df4690 100644 --- a/test/token/ERC721/extensions/ERC721Consecutive.test.js +++ b/test/token/ERC721/extensions/ERC721Consecutive.test.js @@ -20,159 +20,169 @@ contract('ERC721Consecutive', function (accounts) { ]; const delegates = [user1, user3]; - describe('with valid batches', function () { - beforeEach(async function () { - this.token = await ERC721ConsecutiveMock.new( - name, - symbol, - delegates, - batches.map(({ receiver }) => receiver), - batches.map(({ amount }) => amount), - ); - }); - - describe('minting during construction', function () { - it('events are emitted at construction', async function () { - let first = 0; - - for (const batch of batches) { - if (batch.amount > 0) { - await expectEvent.inConstruction(this.token, 'ConsecutiveTransfer', { - fromTokenId: web3.utils.toBN(first), - toTokenId: web3.utils.toBN(first + batch.amount - 1), - fromAddress: constants.ZERO_ADDRESS, - toAddress: batch.receiver, - }); - } else { - // expectEvent.notEmitted.inConstruction only looks at event name, and doesn't check the parameters - } - first += batch.amount; - } - }); - - it('ownership is set', async function () { - const owners = batches.flatMap(({ receiver, amount }) => Array(amount).fill(receiver)); - - for (const tokenId in owners) { - expect(await this.token.ownerOf(tokenId)).to.be.equal(owners[tokenId]); - } - }); - - it('balance & voting power are set', async function () { - for (const account of accounts) { - const balance = batches - .filter(({ receiver }) => receiver === account) - .map(({ amount }) => amount) - .reduce((a, b) => a + b, 0); - - expect(await this.token.balanceOf(account)).to.be.bignumber.equal(web3.utils.toBN(balance)); - - // If not delegated at construction, check before + do delegation - if (!delegates.includes(account)) { - expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(0)); - - await this.token.delegate(account, { from: account }); - } - - // At this point all accounts should have delegated - expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(balance)); - } - }); - }); - - describe('minting after construction', function () { - it('consecutive minting is not possible after construction', async function () { - await expectRevert( - this.token.$_mintConsecutive(user1, 10), - 'ERC721Consecutive: batch minting restricted to constructor', + for (const offset of [0, 1, 42]) { + describe(`with offset ${offset}`, function () { + beforeEach(async function () { + this.token = await ERC721ConsecutiveMock.new( + name, + symbol, + offset, + delegates, + batches.map(({ receiver }) => receiver), + batches.map(({ amount }) => amount), ); }); - it('simple minting is possible after construction', async function () { - const tokenId = batches.reduce((acc, { amount }) => acc + amount, 0); + describe('minting during construction', function () { + it('events are emitted at construction', async function () { + let first = offset; - expect(await this.token.$_exists(tokenId)).to.be.equal(false); + for (const batch of batches) { + if (batch.amount > 0) { + await expectEvent.inConstruction(this.token, 'ConsecutiveTransfer', { + fromTokenId: web3.utils.toBN(first), + toTokenId: web3.utils.toBN(first + batch.amount - 1), + fromAddress: constants.ZERO_ADDRESS, + toAddress: batch.receiver, + }); + } else { + // expectEvent.notEmitted.inConstruction only looks at event name, and doesn't check the parameters + } + first += batch.amount; + } + }); - expectEvent(await this.token.$_mint(user1, tokenId), 'Transfer', { - from: constants.ZERO_ADDRESS, - to: user1, - tokenId: tokenId.toString(), + it('ownership is set', async function () { + const owners = [ + ...Array(offset).fill(constants.ZERO_ADDRESS), + ...batches.flatMap(({ receiver, amount }) => Array(amount).fill(receiver)), + ]; + + for (const tokenId in owners) { + if (owners[tokenId] != constants.ZERO_ADDRESS) { + expect(await this.token.ownerOf(tokenId)).to.be.equal(owners[tokenId]); + } + } + }); + + it('balance & voting power are set', async function () { + for (const account of accounts) { + const balance = batches + .filter(({ receiver }) => receiver === account) + .map(({ amount }) => amount) + .reduce((a, b) => a + b, 0); + + expect(await this.token.balanceOf(account)).to.be.bignumber.equal(web3.utils.toBN(balance)); + + // If not delegated at construction, check before + do delegation + if (!delegates.includes(account)) { + expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(0)); + + await this.token.delegate(account, { from: account }); + } + + // At this point all accounts should have delegated + expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(balance)); + } }); }); - it('cannot mint a token that has been batched minted', async function () { - const tokenId = batches.reduce((acc, { amount }) => acc + amount, 0) - 1; + describe('minting after construction', function () { + it('consecutive minting is not possible after construction', async function () { + await expectRevert( + this.token.$_mintConsecutive(user1, 10), + 'ERC721Consecutive: batch minting restricted to constructor', + ); + }); - expect(await this.token.$_exists(tokenId)).to.be.equal(true); + it('simple minting is possible after construction', async function () { + const tokenId = batches.reduce((acc, { amount }) => acc + amount, offset); - await expectRevert(this.token.$_mint(user1, tokenId), 'ERC721: token already minted'); + expect(await this.token.$_exists(tokenId)).to.be.equal(false); + + expectEvent(await this.token.$_mint(user1, tokenId), 'Transfer', { + from: constants.ZERO_ADDRESS, + to: user1, + tokenId: tokenId.toString(), + }); + }); + + it('cannot mint a token that has been batched minted', async function () { + const tokenId = batches.reduce((acc, { amount }) => acc + amount, offset) - 1; + + expect(await this.token.$_exists(tokenId)).to.be.equal(true); + + await expectRevert(this.token.$_mint(user1, tokenId), 'ERC721: token already minted'); + }); + }); + + describe('ERC721 behavior', function () { + const tokenId = web3.utils.toBN(offset + 1); + + it('core takes over ownership on transfer', async function () { + await this.token.transferFrom(user1, receiver, tokenId, { from: user1 }); + + expect(await this.token.ownerOf(tokenId)).to.be.equal(receiver); + }); + + it('tokens can be burned and re-minted #1', async function () { + expectEvent(await this.token.$_burn(tokenId, { from: user1 }), 'Transfer', { + from: user1, + to: constants.ZERO_ADDRESS, + tokenId, + }); + + await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); + + expectEvent(await this.token.$_mint(user2, tokenId), 'Transfer', { + from: constants.ZERO_ADDRESS, + to: user2, + tokenId, + }); + + expect(await this.token.ownerOf(tokenId)).to.be.equal(user2); + }); + + it('tokens can be burned and re-minted #2', async function () { + const tokenId = web3.utils.toBN(batches.reduce((acc, { amount }) => acc + amount, offset)); + + expect(await this.token.$_exists(tokenId)).to.be.equal(false); + await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); + + // mint + await this.token.$_mint(user1, tokenId); + + expect(await this.token.$_exists(tokenId)).to.be.equal(true); + expect(await this.token.ownerOf(tokenId), user1); + + // burn + expectEvent(await this.token.$_burn(tokenId, { from: user1 }), 'Transfer', { + from: user1, + to: constants.ZERO_ADDRESS, + tokenId, + }); + + expect(await this.token.$_exists(tokenId)).to.be.equal(false); + await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); + + // re-mint + expectEvent(await this.token.$_mint(user2, tokenId), 'Transfer', { + from: constants.ZERO_ADDRESS, + to: user2, + tokenId, + }); + + expect(await this.token.$_exists(tokenId)).to.be.equal(true); + expect(await this.token.ownerOf(tokenId), user2); + }); }); }); - - describe('ERC721 behavior', function () { - it('core takes over ownership on transfer', async function () { - await this.token.transferFrom(user1, receiver, 1, { from: user1 }); - - expect(await this.token.ownerOf(1)).to.be.equal(receiver); - }); - - it('tokens can be burned and re-minted #1', async function () { - expectEvent(await this.token.$_burn(1, { from: user1 }), 'Transfer', { - from: user1, - to: constants.ZERO_ADDRESS, - tokenId: '1', - }); - - await expectRevert(this.token.ownerOf(1), 'ERC721: invalid token ID'); - - expectEvent(await this.token.$_mint(user2, 1), 'Transfer', { - from: constants.ZERO_ADDRESS, - to: user2, - tokenId: '1', - }); - - expect(await this.token.ownerOf(1)).to.be.equal(user2); - }); - - it('tokens can be burned and re-minted #2', async function () { - const tokenId = batches.reduce((acc, { amount }) => acc.addn(amount), web3.utils.toBN(0)); - - expect(await this.token.$_exists(tokenId)).to.be.equal(false); - await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); - - // mint - await this.token.$_mint(user1, tokenId); - - expect(await this.token.$_exists(tokenId)).to.be.equal(true); - expect(await this.token.ownerOf(tokenId), user1); - - // burn - expectEvent(await this.token.$_burn(tokenId, { from: user1 }), 'Transfer', { - from: user1, - to: constants.ZERO_ADDRESS, - tokenId, - }); - - expect(await this.token.$_exists(tokenId)).to.be.equal(false); - await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); - - // re-mint - expectEvent(await this.token.$_mint(user2, tokenId), 'Transfer', { - from: constants.ZERO_ADDRESS, - to: user2, - tokenId, - }); - - expect(await this.token.$_exists(tokenId)).to.be.equal(true); - expect(await this.token.ownerOf(tokenId), user2); - }); - }); - }); + } describe('invalid use', function () { it('cannot mint a batch larger than 5000', async function () { await expectRevert( - ERC721ConsecutiveMock.new(name, symbol, [], [user1], ['5001']), + ERC721ConsecutiveMock.new(name, symbol, 0, [], [user1], ['5001']), 'ERC721Consecutive: batch too large', ); }); From 4448c13c3cf5881c278179ad1dc8ddfdd74f47e6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 26 May 2023 21:19:27 +0200 Subject: [PATCH 145/197] Move the Checkpoints library to utils/structs (#4275) --- .changeset/fresh-birds-kiss.md | 5 +++++ .../governance/extensions/GovernorVotesQuorumFraction.sol | 2 +- contracts/governance/utils/Votes.sol | 2 +- contracts/token/ERC721/extensions/ERC721Consecutive.sol | 2 +- contracts/utils/{ => structs}/Checkpoints.sol | 6 +++--- scripts/generate/run.js | 4 ++-- scripts/generate/templates/Checkpoints.js | 4 ++-- scripts/generate/templates/Checkpoints.t.js | 4 ++-- test/utils/{ => structs}/Checkpoints.t.sol | 4 ++-- test/utils/{ => structs}/Checkpoints.test.js | 2 +- 10 files changed, 20 insertions(+), 15 deletions(-) create mode 100644 .changeset/fresh-birds-kiss.md rename contracts/utils/{ => structs}/Checkpoints.sol (99%) rename test/utils/{ => structs}/Checkpoints.t.sol (98%) rename test/utils/{ => structs}/Checkpoints.test.js (98%) diff --git a/.changeset/fresh-birds-kiss.md b/.changeset/fresh-birds-kiss.md new file mode 100644 index 000000000..221f54cdf --- /dev/null +++ b/.changeset/fresh-birds-kiss.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`Checkpoints`: library moved from `utils` to `utils/structs` diff --git a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol index e26ffb260..d42bb2f47 100644 --- a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol +++ b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.0; import "./GovernorVotes.sol"; -import "../../utils/Checkpoints.sol"; import "../../utils/math/SafeCast.sol"; +import "../../utils/structs/Checkpoints.sol"; /** * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token and a quorum expressed as a diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index a579811e5..cc1143fe2 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -5,8 +5,8 @@ pragma solidity ^0.8.0; import "../../interfaces/IERC5805.sol"; import "../../utils/Context.sol"; import "../../utils/Nonces.sol"; -import "../../utils/Checkpoints.sol"; import "../../utils/cryptography/EIP712.sol"; +import "../../utils/structs/Checkpoints.sol"; /** * @dev This is a base abstract contract that tracks voting units, which are a measure of voting power that can be diff --git a/contracts/token/ERC721/extensions/ERC721Consecutive.sol b/contracts/token/ERC721/extensions/ERC721Consecutive.sol index a6ab6c76b..4191ad684 100644 --- a/contracts/token/ERC721/extensions/ERC721Consecutive.sol +++ b/contracts/token/ERC721/extensions/ERC721Consecutive.sol @@ -5,8 +5,8 @@ pragma solidity ^0.8.1; import "../ERC721.sol"; import "../../../interfaces/IERC2309.sol"; -import "../../../utils/Checkpoints.sol"; import "../../../utils/structs/BitMaps.sol"; +import "../../../utils/structs/Checkpoints.sol"; /** * @dev Implementation of the ERC2309 "Consecutive Transfer Extension" as defined in diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/structs/Checkpoints.sol similarity index 99% rename from contracts/utils/Checkpoints.sol rename to contracts/utils/structs/Checkpoints.sol index 90f03aef7..f2ee45811 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/structs/Checkpoints.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.9.0) (utils/Checkpoints.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/Checkpoints.sol) // This file was procedurally generated from scripts/generate/templates/Checkpoints.js. pragma solidity ^0.8.0; -import "./math/Math.sol"; -import "./math/SafeCast.sol"; +import "../math/Math.sol"; +import "../math/SafeCast.sol"; /** * @dev This library defines the `History` struct, for checkpointing values as they change at different points in diff --git a/scripts/generate/run.js b/scripts/generate/run.js index 368b53ad1..53589455a 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -35,7 +35,7 @@ for (const [file, template] of Object.entries({ 'utils/math/SafeCast.sol': './templates/SafeCast.js', 'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js', 'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js', - 'utils/Checkpoints.sol': './templates/Checkpoints.js', + 'utils/structs/Checkpoints.sol': './templates/Checkpoints.js', 'utils/StorageSlot.sol': './templates/StorageSlot.js', })) { generateFromTemplate(file, template, './contracts/'); @@ -43,7 +43,7 @@ for (const [file, template] of Object.entries({ // Tests for (const [file, template] of Object.entries({ - 'utils/Checkpoints.t.sol': './templates/Checkpoints.t.js', + 'utils/structs/Checkpoints.t.sol': './templates/Checkpoints.t.js', })) { generateFromTemplate(file, template, './test/'); } diff --git a/scripts/generate/templates/Checkpoints.js b/scripts/generate/templates/Checkpoints.js index de58ad19e..f339fde22 100644 --- a/scripts/generate/templates/Checkpoints.js +++ b/scripts/generate/templates/Checkpoints.js @@ -5,8 +5,8 @@ const { OPTS } = require('./Checkpoints.opts.js'); const header = `\ pragma solidity ^0.8.0; -import "./math/Math.sol"; -import "./math/SafeCast.sol"; +import "../math/Math.sol"; +import "../math/SafeCast.sol"; /** * @dev This library defines the \`History\` struct, for checkpointing values as they change at different points in diff --git a/scripts/generate/templates/Checkpoints.t.js b/scripts/generate/templates/Checkpoints.t.js index 0451ecea4..2b8c7b67c 100644 --- a/scripts/generate/templates/Checkpoints.t.js +++ b/scripts/generate/templates/Checkpoints.t.js @@ -7,8 +7,8 @@ const header = `\ pragma solidity ^0.8.0; import "forge-std/Test.sol"; -import "../../contracts/utils/Checkpoints.sol"; -import "../../contracts/utils/math/SafeCast.sol"; +import "../../../contracts/utils/math/SafeCast.sol"; +import "../../../contracts/utils/structs/Checkpoints.sol"; `; /* eslint-disable max-len */ diff --git a/test/utils/Checkpoints.t.sol b/test/utils/structs/Checkpoints.t.sol similarity index 98% rename from test/utils/Checkpoints.t.sol rename to test/utils/structs/Checkpoints.t.sol index 12598da46..e3f4bd28b 100644 --- a/test/utils/Checkpoints.t.sol +++ b/test/utils/structs/Checkpoints.t.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.0; import "forge-std/Test.sol"; -import "../../contracts/utils/Checkpoints.sol"; -import "../../contracts/utils/math/SafeCast.sol"; +import "../../../contracts/utils/math/SafeCast.sol"; +import "../../../contracts/utils/structs/Checkpoints.sol"; contract CheckpointsTrace224Test is Test { using Checkpoints for Checkpoints.Trace224; diff --git a/test/utils/Checkpoints.test.js b/test/utils/structs/Checkpoints.test.js similarity index 98% rename from test/utils/Checkpoints.test.js rename to test/utils/structs/Checkpoints.test.js index d9fd2c8ea..ad95373a4 100644 --- a/test/utils/Checkpoints.test.js +++ b/test/utils/structs/Checkpoints.test.js @@ -1,7 +1,7 @@ const { expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); -const { VALUE_SIZES } = require('../../scripts/generate/templates/Checkpoints.opts.js'); +const { VALUE_SIZES } = require('../../../scripts/generate/templates/Checkpoints.opts.js'); const $Checkpoints = artifacts.require('$Checkpoints'); From 15c5c7179555ee46baf839db003029f4b2bcb470 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sat, 27 May 2023 00:30:00 +0200 Subject: [PATCH 146/197] Remove TokenTimelock, PaymentSplitter, ERC20Snapshot, ERC20VotesComp, GovernorVotesComp (#4276) --- CHANGELOG.md | 10 + contracts/finance/PaymentSplitter.sol | 214 ------- contracts/finance/README.adoc | 6 - contracts/finance/VestingWallet.sol | 12 +- contracts/governance/README.adoc | 4 - .../extensions/GovernorVotesComp.sol | 55 -- .../mocks/governance/GovernorCompMock.sol | 20 - .../GovernorCompatibilityBravoMock.sol | 4 +- contracts/mocks/token/VotesTimestamp.sol | 12 - contracts/token/ERC20/README.adoc | 15 +- .../token/ERC20/extensions/ERC20Snapshot.sol | 189 ------ .../token/ERC20/extensions/ERC20Votes.sol | 2 +- .../token/ERC20/extensions/ERC20VotesComp.sol | 46 -- contracts/token/ERC20/utils/TokenTimelock.sol | 72 --- scripts/upgradeable/upgradeable.patch | 36 +- test/finance/PaymentSplitter.test.js | 217 ------- test/finance/VestingWallet.test.js | 1 + .../GovernorCompatibilityBravo.test.js | 4 +- .../extensions/GovernorComp.test.js | 89 --- .../ERC20/extensions/ERC20Snapshot.test.js | 207 ------- .../ERC20/extensions/ERC20VotesComp.test.js | 579 ------------------ test/token/ERC20/utils/TokenTimelock.test.js | 71 --- 22 files changed, 37 insertions(+), 1828 deletions(-) delete mode 100644 contracts/finance/PaymentSplitter.sol delete mode 100644 contracts/governance/extensions/GovernorVotesComp.sol delete mode 100644 contracts/mocks/governance/GovernorCompMock.sol delete mode 100644 contracts/token/ERC20/extensions/ERC20Snapshot.sol delete mode 100644 contracts/token/ERC20/extensions/ERC20VotesComp.sol delete mode 100644 contracts/token/ERC20/utils/TokenTimelock.sol delete mode 100644 test/finance/PaymentSplitter.test.js delete mode 100644 test/governance/extensions/GovernorComp.test.js delete mode 100644 test/token/ERC20/extensions/ERC20Snapshot.test.js delete mode 100644 test/token/ERC20/extensions/ERC20VotesComp.test.js delete mode 100644 test/token/ERC20/utils/TokenTimelock.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2298e2e51..8d2853cf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +### Removals + +The following contracts were removed: + +- `ERC20Snapshot` +- `ERC20VotesComp` +- `GovernorVotesComp` +- `PaymentSplitter` +- `TokenTimelock` (removed in favor of `VestingWallet`) + ### How to upgrade from 4.x #### ERC20, ERC721, and ERC1155 diff --git a/contracts/finance/PaymentSplitter.sol b/contracts/finance/PaymentSplitter.sol deleted file mode 100644 index daa9090eb..000000000 --- a/contracts/finance/PaymentSplitter.sol +++ /dev/null @@ -1,214 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (finance/PaymentSplitter.sol) - -pragma solidity ^0.8.0; - -import "../token/ERC20/utils/SafeERC20.sol"; -import "../utils/Address.sol"; -import "../utils/Context.sol"; - -/** - * @title PaymentSplitter - * @dev This contract allows to split Ether payments among a group of accounts. The sender does not need to be aware - * that the Ether will be split in this way, since it is handled transparently by the contract. - * - * The split can be in equal parts or in any other arbitrary proportion. The way this is specified is by assigning each - * account to a number of shares. Of all the Ether that this contract receives, each account will then be able to claim - * an amount proportional to the percentage of total shares they were assigned. The distribution of shares is set at the - * time of contract deployment and can't be updated thereafter. - * - * `PaymentSplitter` follows a _pull payment_ model. This means that payments are not automatically forwarded to the - * accounts but kept in this contract, and the actual transfer is triggered as a separate step by calling the {release} - * function. - * - * NOTE: This contract assumes that ERC20 tokens will behave similarly to native tokens (Ether). Rebasing tokens, and - * tokens that apply fees during transfers, are likely to not be supported as expected. If in doubt, we encourage you - * to run tests before sending real value to this contract. - */ -contract PaymentSplitter is Context { - event PayeeAdded(address account, uint256 shares); - event PaymentReleased(address to, uint256 amount); - event ERC20PaymentReleased(IERC20 indexed token, address to, uint256 amount); - event PaymentReceived(address from, uint256 amount); - - uint256 private _totalShares; - uint256 private _totalReleased; - - mapping(address => uint256) private _shares; - mapping(address => uint256) private _released; - address[] private _payees; - - mapping(IERC20 => uint256) private _erc20TotalReleased; - mapping(IERC20 => mapping(address => uint256)) private _erc20Released; - - /** - * @dev Creates an instance of `PaymentSplitter` where each account in `payees` is assigned the number of shares at - * the matching position in the `shares` array. - * - * All addresses in `payees` must be non-zero. Both arrays must have the same non-zero length, and there must be no - * duplicates in `payees`. - */ - constructor(address[] memory payees, uint256[] memory shares_) payable { - require(payees.length == shares_.length, "PaymentSplitter: payees and shares length mismatch"); - require(payees.length > 0, "PaymentSplitter: no payees"); - - for (uint256 i = 0; i < payees.length; i++) { - _addPayee(payees[i], shares_[i]); - } - } - - /** - * @dev The Ether received will be logged with {PaymentReceived} events. Note that these events are not fully - * reliable: it's possible for a contract to receive Ether without triggering this function. This only affects the - * reliability of the events, and not the actual splitting of Ether. - * - * To learn more about this see the Solidity documentation for - * https://solidity.readthedocs.io/en/latest/contracts.html#fallback-function[fallback - * functions]. - */ - receive() external payable virtual { - emit PaymentReceived(_msgSender(), msg.value); - } - - /** - * @dev Getter for the total shares held by payees. - */ - function totalShares() public view returns (uint256) { - return _totalShares; - } - - /** - * @dev Getter for the total amount of Ether already released. - */ - function totalReleased() public view returns (uint256) { - return _totalReleased; - } - - /** - * @dev Getter for the total amount of `token` already released. `token` should be the address of an IERC20 - * contract. - */ - function totalReleased(IERC20 token) public view returns (uint256) { - return _erc20TotalReleased[token]; - } - - /** - * @dev Getter for the amount of shares held by an account. - */ - function shares(address account) public view returns (uint256) { - return _shares[account]; - } - - /** - * @dev Getter for the amount of Ether already released to a payee. - */ - function released(address account) public view returns (uint256) { - return _released[account]; - } - - /** - * @dev Getter for the amount of `token` tokens already released to a payee. `token` should be the address of an - * IERC20 contract. - */ - function released(IERC20 token, address account) public view returns (uint256) { - return _erc20Released[token][account]; - } - - /** - * @dev Getter for the address of the payee number `index`. - */ - function payee(uint256 index) public view returns (address) { - return _payees[index]; - } - - /** - * @dev Getter for the amount of payee's releasable Ether. - */ - function releasable(address account) public view returns (uint256) { - uint256 totalReceived = address(this).balance + totalReleased(); - return _pendingPayment(account, totalReceived, released(account)); - } - - /** - * @dev Getter for the amount of payee's releasable `token` tokens. `token` should be the address of an - * IERC20 contract. - */ - function releasable(IERC20 token, address account) public view returns (uint256) { - uint256 totalReceived = token.balanceOf(address(this)) + totalReleased(token); - return _pendingPayment(account, totalReceived, released(token, account)); - } - - /** - * @dev Triggers a transfer to `account` of the amount of Ether they are owed, according to their percentage of the - * total shares and their previous withdrawals. - */ - function release(address payable account) public virtual { - require(_shares[account] > 0, "PaymentSplitter: account has no shares"); - - uint256 payment = releasable(account); - - require(payment != 0, "PaymentSplitter: account is not due payment"); - - // _totalReleased is the sum of all values in _released. - // If "_totalReleased += payment" does not overflow, then "_released[account] += payment" cannot overflow. - _totalReleased += payment; - unchecked { - _released[account] += payment; - } - - Address.sendValue(account, payment); - emit PaymentReleased(account, payment); - } - - /** - * @dev Triggers a transfer to `account` of the amount of `token` tokens they are owed, according to their - * percentage of the total shares and their previous withdrawals. `token` must be the address of an IERC20 - * contract. - */ - function release(IERC20 token, address account) public virtual { - require(_shares[account] > 0, "PaymentSplitter: account has no shares"); - - uint256 payment = releasable(token, account); - - require(payment != 0, "PaymentSplitter: account is not due payment"); - - // _erc20TotalReleased[token] is the sum of all values in _erc20Released[token]. - // If "_erc20TotalReleased[token] += payment" does not overflow, then "_erc20Released[token][account] += payment" - // cannot overflow. - _erc20TotalReleased[token] += payment; - unchecked { - _erc20Released[token][account] += payment; - } - - SafeERC20.safeTransfer(token, account, payment); - emit ERC20PaymentReleased(token, account, payment); - } - - /** - * @dev internal logic for computing the pending payment of an `account` given the token historical balances and - * already released amounts. - */ - function _pendingPayment( - address account, - uint256 totalReceived, - uint256 alreadyReleased - ) private view returns (uint256) { - return (totalReceived * _shares[account]) / _totalShares - alreadyReleased; - } - - /** - * @dev Add a new payee to the contract. - * @param account The address of the payee to add. - * @param shares_ The number of shares owned by the payee. - */ - function _addPayee(address account, uint256 shares_) private { - require(account != address(0), "PaymentSplitter: account is the zero address"); - require(shares_ > 0, "PaymentSplitter: shares are 0"); - require(_shares[account] == 0, "PaymentSplitter: account already has shares"); - - _payees.push(account); - _shares[account] = shares_; - _totalShares = _totalShares + shares_; - emit PayeeAdded(account, shares_); - } -} diff --git a/contracts/finance/README.adoc b/contracts/finance/README.adoc index b64af3125..ac7e4f015 100644 --- a/contracts/finance/README.adoc +++ b/contracts/finance/README.adoc @@ -5,16 +5,10 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/ This directory includes primitives for financial systems: -- {PaymentSplitter} allows to split Ether and ERC20 payments among a group of accounts. The sender does not need to be - aware that the assets will be split in this way, since it is handled transparently by the contract. The split can be - in equal parts or in any other arbitrary proportion. - - {VestingWallet} handles the vesting of Ether and ERC20 tokens for a given beneficiary. Custody of multiple tokens can be given to this contract, which will release the token to the beneficiary following a given, customizable, vesting schedule. == Contracts -{{PaymentSplitter}} - {{VestingWallet}} diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol index 45cf9ce52..bb70d19fb 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -15,6 +15,9 @@ import "../utils/Context.sol"; * Any token transferred to this contract will follow the vesting schedule as if they were locked from the beginning. * Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly) * be immediately releasable. + * + * By setting the duration to 0, one can configure this contract to behave like an asset timelock that hold tokens for + * a beneficiary until a specified time. */ contract VestingWallet is Context { event EtherReleased(uint256 amount); @@ -62,6 +65,13 @@ contract VestingWallet is Context { return _duration; } + /** + * @dev Getter for the end timestamp. + */ + function end() public view virtual returns (uint256) { + return start() + duration(); + } + /** * @dev Amount of eth already released */ @@ -136,7 +146,7 @@ contract VestingWallet is Context { function _vestingSchedule(uint256 totalAllocation, uint64 timestamp) internal view virtual returns (uint256) { if (timestamp < start()) { return 0; - } else if (timestamp > start() + duration()) { + } else if (timestamp > end()) { return totalAllocation; } else { return (totalAllocation * (timestamp - start())) / duration(); diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index 171283662..00edfe23d 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -22,8 +22,6 @@ Votes modules determine the source of voting power, and sometimes quorum number. * {GovernorVotes}: Extracts voting weight from an {ERC20Votes}, or since v4.5 an {ERC721Votes} token. -* {GovernorVotesComp}: Extracts voting weight from a COMP-like or {ERC20VotesComp} token. - * {GovernorVotesQuorumFraction}: Combines with `GovernorVotes` to set the quorum as a fraction of the total token supply. Counting modules determine valid voting options. @@ -66,8 +64,6 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorVotesQuorumFraction}} -{{GovernorVotesComp}} - === Extensions {{GovernorTimelockControl}} diff --git a/contracts/governance/extensions/GovernorVotesComp.sol b/contracts/governance/extensions/GovernorVotesComp.sol deleted file mode 100644 index e7d7c2c7c..000000000 --- a/contracts/governance/extensions/GovernorVotesComp.sol +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorVotesComp.sol) - -pragma solidity ^0.8.0; - -import "../Governor.sol"; -import "../../token/ERC20/extensions/ERC20VotesComp.sol"; - -/** - * @dev Extension of {Governor} for voting weight extraction from a Comp token. - * - * _Available since v4.3._ - */ -abstract contract GovernorVotesComp is Governor { - ERC20VotesComp public immutable token; - - constructor(ERC20VotesComp token_) { - token = token_; - } - - /** - * @dev Clock (as specified in EIP-6372) is set to match the token's clock. Fallback to block numbers if the token - * does not implement EIP-6372. - */ - function clock() public view virtual override returns (uint48) { - try token.clock() returns (uint48 timepoint) { - return timepoint; - } catch { - return SafeCast.toUint48(block.number); - } - } - - /** - * @dev Machine-readable description of the clock as specified in EIP-6372. - */ - // solhint-disable-next-line func-name-mixedcase - function CLOCK_MODE() public view virtual override returns (string memory) { - try token.CLOCK_MODE() returns (string memory clockmode) { - return clockmode; - } catch { - return "mode=blocknumber&from=default"; - } - } - - /** - * Read the voting weight from the token's built-in snapshot mechanism (see {Governor-_getVotes}). - */ - function _getVotes( - address account, - uint256 timepoint, - bytes memory /*params*/ - ) internal view virtual override returns (uint256) { - return token.getPriorVotes(account, timepoint); - } -} diff --git a/contracts/mocks/governance/GovernorCompMock.sol b/contracts/mocks/governance/GovernorCompMock.sol deleted file mode 100644 index cc368c42f..000000000 --- a/contracts/mocks/governance/GovernorCompMock.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../../governance/extensions/GovernorCountingSimple.sol"; -import "../../governance/extensions/GovernorVotesComp.sol"; - -abstract contract GovernorCompMock is GovernorVotesComp, GovernorCountingSimple { - function quorum(uint256) public pure override returns (uint256) { - return 0; - } - - function votingDelay() public pure override returns (uint256) { - return 4; - } - - function votingPeriod() public pure override returns (uint256) { - return 16; - } -} diff --git a/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol b/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol index 1b87d1433..4cdc0b888 100644 --- a/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol +++ b/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol @@ -5,13 +5,13 @@ pragma solidity ^0.8.0; import "../../governance/compatibility/GovernorCompatibilityBravo.sol"; import "../../governance/extensions/GovernorTimelockCompound.sol"; import "../../governance/extensions/GovernorSettings.sol"; -import "../../governance/extensions/GovernorVotesComp.sol"; +import "../../governance/extensions/GovernorVotes.sol"; abstract contract GovernorCompatibilityBravoMock is GovernorCompatibilityBravo, GovernorSettings, GovernorTimelockCompound, - GovernorVotesComp + GovernorVotes { function quorum(uint256) public pure override returns (uint256) { return 0; diff --git a/contracts/mocks/token/VotesTimestamp.sol b/contracts/mocks/token/VotesTimestamp.sol index 179c500f4..630e778f1 100644 --- a/contracts/mocks/token/VotesTimestamp.sol +++ b/contracts/mocks/token/VotesTimestamp.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; import "../../token/ERC20/extensions/ERC20Votes.sol"; -import "../../token/ERC20/extensions/ERC20VotesComp.sol"; import "../../token/ERC721/extensions/ERC721Votes.sol"; abstract contract ERC20VotesTimestampMock is ERC20Votes { @@ -17,17 +16,6 @@ abstract contract ERC20VotesTimestampMock is ERC20Votes { } } -abstract contract ERC20VotesCompTimestampMock is ERC20VotesComp { - function clock() public view virtual override returns (uint48) { - return SafeCast.toUint48(block.timestamp); - } - - // solhint-disable-next-line func-name-mixedcase - function CLOCK_MODE() public view virtual override returns (string memory) { - return "mode=timestamp"; - } -} - abstract contract ERC721VotesTimestampMock is ERC721Votes { function clock() public view virtual override returns (uint48) { return SafeCast.toUint48(block.timestamp); diff --git a/contracts/token/ERC20/README.adoc b/contracts/token/ERC20/README.adoc index 326aae684..9482b581b 100644 --- a/contracts/token/ERC20/README.adoc +++ b/contracts/token/ERC20/README.adoc @@ -18,18 +18,19 @@ Additionally there are multiple custom extensions, including: * {ERC20Burnable}: destruction of own tokens. * {ERC20Capped}: enforcement of a cap to the total supply when minting tokens. * {ERC20Pausable}: ability to pause token transfers. -* {ERC20Snapshot}: efficient storage of past token balances to be later queried at any point in time. * {ERC20Permit}: gasless approval of tokens (standardized as ERC2612). * {ERC20FlashMint}: token level support for flash loans through the minting and burning of ephemeral tokens (standardized as ERC3156). * {ERC20Votes}: support for voting and vote delegation. -* {ERC20VotesComp}: support for voting and vote delegation (compatible with Compound's token, with uint96 restrictions). * {ERC20Wrapper}: wrapper to create an ERC20 backed by another ERC20, with deposit and withdraw methods. Useful in conjunction with {ERC20Votes}. * {ERC4626}: tokenized vault that manages shares (represented as ERC20) that are backed by assets (another ERC20). -Finally, there are some utilities to interact with ERC20 contracts in various ways. +Finally, there are some utilities to interact with ERC20 contracts in various ways: * {SafeERC20}: a wrapper around the interface that eliminates the need to handle boolean return values. -* {TokenTimelock}: hold tokens for a beneficiary until a specified time. + +Other utilities that support ERC20 assets can be found in codebase: + +* ERC20 tokens can be timelocked (held tokens for a beneficiary until a specified time) or vested (released following a given schedule) using a {VestingWallet}. NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC20 (such as <>) and expose them as external functions in the way they prefer. @@ -51,12 +52,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel {{ERC20Permit}} -{{ERC20Snapshot}} - {{ERC20Votes}} -{{ERC20VotesComp}} - {{ERC20Wrapper}} {{ERC20FlashMint}} @@ -66,5 +63,3 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel == Utilities {{SafeERC20}} - -{{TokenTimelock}} diff --git a/contracts/token/ERC20/extensions/ERC20Snapshot.sol b/contracts/token/ERC20/extensions/ERC20Snapshot.sol deleted file mode 100644 index 70abc543a..000000000 --- a/contracts/token/ERC20/extensions/ERC20Snapshot.sol +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC20Snapshot.sol) - -pragma solidity ^0.8.0; - -import "../ERC20.sol"; -import "../../../utils/Arrays.sol"; -import "../../../utils/Counters.sol"; - -/** - * @dev This contract extends an ERC20 token with a snapshot mechanism. When a snapshot is created, the balances and - * total supply at the time are recorded for later access. - * - * This can be used to safely create mechanisms based on token balances such as trustless dividends or weighted voting. - * In naive implementations it's possible to perform a "double spend" attack by reusing the same balance from different - * accounts. By using snapshots to calculate dividends or voting power, those attacks no longer apply. It can also be - * used to create an efficient ERC20 forking mechanism. - * - * Snapshots are created by the internal {_snapshot} function, which will emit the {Snapshot} event and return a - * snapshot id. To get the total supply at the time of a snapshot, call the function {totalSupplyAt} with the snapshot - * id. To get the balance of an account at the time of a snapshot, call the {balanceOfAt} function with the snapshot id - * and the account address. - * - * NOTE: Snapshot policy can be customized by overriding the {_getCurrentSnapshotId} method. For example, having it - * return `block.number` will trigger the creation of snapshot at the beginning of each new block. When overriding this - * function, be careful about the monotonicity of its result. Non-monotonic snapshot ids will break the contract. - * - * Implementing snapshots for every block using this method will incur significant gas costs. For a gas-efficient - * alternative consider {ERC20Votes}. - * - * ==== Gas Costs - * - * Snapshots are efficient. Snapshot creation is _O(1)_. Retrieval of balances or total supply from a snapshot is _O(log - * n)_ in the number of snapshots that have been created, although _n_ for a specific account will generally be much - * smaller since identical balances in subsequent snapshots are stored as a single entry. - * - * There is a constant overhead for normal ERC20 transfers due to the additional snapshot bookkeeping. This overhead is - * only significant for the first transfer that immediately follows a snapshot for a particular account. Subsequent - * transfers will have normal cost until the next snapshot, and so on. - */ - -abstract contract ERC20Snapshot is ERC20 { - // Inspired by Jordi Baylina's MiniMeToken to record historical balances: - // https://github.com/Giveth/minime/blob/ea04d950eea153a04c51fa510b068b9dded390cb/contracts/MiniMeToken.sol - - using Arrays for uint256[]; - using Counters for Counters.Counter; - - // Snapshotted values have arrays of ids and the value corresponding to that id. These could be an array of a - // Snapshot struct, but that would impede usage of functions that work on an array. - struct Snapshots { - uint256[] ids; - uint256[] values; - } - - mapping(address => Snapshots) private _accountBalanceSnapshots; - Snapshots private _totalSupplySnapshots; - - // Snapshot ids increase monotonically, with the first value being 1. An id of 0 is invalid. - Counters.Counter private _currentSnapshotId; - - /** - * @dev Emitted by {_snapshot} when a snapshot identified by `id` is created. - */ - event Snapshot(uint256 id); - - /** - * @dev Creates a new snapshot and returns its snapshot id. - * - * Emits a {Snapshot} event that contains the same id. - * - * {_snapshot} is `internal` and you have to decide how to expose it externally. Its usage may be restricted to a - * set of accounts, for example using {AccessControl}, or it may be open to the public. - * - * [WARNING] - * ==== - * While an open way of calling {_snapshot} is required for certain trust minimization mechanisms such as forking, - * you must consider that it can potentially be used by attackers in two ways. - * - * First, it can be used to increase the cost of retrieval of values from snapshots, although it will grow - * logarithmically thus rendering this attack ineffective in the long term. Second, it can be used to target - * specific accounts and increase the cost of ERC20 transfers for them, in the ways specified in the Gas Costs - * section above. - * - * We haven't measured the actual numbers; if this is something you're interested in please reach out to us. - * ==== - */ - function _snapshot() internal virtual returns (uint256) { - _currentSnapshotId.increment(); - - uint256 currentId = _getCurrentSnapshotId(); - emit Snapshot(currentId); - return currentId; - } - - /** - * @dev Get the current snapshotId - */ - function _getCurrentSnapshotId() internal view virtual returns (uint256) { - return _currentSnapshotId.current(); - } - - /** - * @dev Retrieves the balance of `account` at the time `snapshotId` was created. - */ - function balanceOfAt(address account, uint256 snapshotId) public view virtual returns (uint256) { - (bool snapshotted, uint256 value) = _valueAt(snapshotId, _accountBalanceSnapshots[account]); - - return snapshotted ? value : balanceOf(account); - } - - /** - * @dev Retrieves the total supply at the time `snapshotId` was created. - */ - function totalSupplyAt(uint256 snapshotId) public view virtual returns (uint256) { - (bool snapshotted, uint256 value) = _valueAt(snapshotId, _totalSupplySnapshots); - - return snapshotted ? value : totalSupply(); - } - - // Update balance and/or total supply snapshots before the values are modified. This is executed - // for _mint, _burn, and _transfer operations. - function _update(address from, address to, uint256 amount) internal virtual override { - if (from == address(0)) { - _updateTotalSupplySnapshot(); - } else { - _updateAccountSnapshot(from); - } - - if (to == address(0)) { - _updateTotalSupplySnapshot(); - } else { - _updateAccountSnapshot(to); - } - - super._update(from, to, amount); - } - - function _valueAt(uint256 snapshotId, Snapshots storage snapshots) private view returns (bool, uint256) { - require(snapshotId > 0, "ERC20Snapshot: id is 0"); - require(snapshotId <= _getCurrentSnapshotId(), "ERC20Snapshot: nonexistent id"); - - // When a valid snapshot is queried, there are three possibilities: - // a) The queried value was not modified after the snapshot was taken. Therefore, a snapshot entry was never - // created for this id, and all stored snapshot ids are smaller than the requested one. The value that corresponds - // to this id is the current one. - // b) The queried value was modified after the snapshot was taken. Therefore, there will be an entry with the - // requested id, and its value is the one to return. - // c) More snapshots were created after the requested one, and the queried value was later modified. There will be - // no entry for the requested id: the value that corresponds to it is that of the smallest snapshot id that is - // larger than the requested one. - // - // In summary, we need to find an element in an array, returning the index of the smallest value that is larger if - // it is not found, unless said value doesn't exist (e.g. when all values are smaller). Arrays.findUpperBound does - // exactly this. - - uint256 index = snapshots.ids.findUpperBound(snapshotId); - - if (index == snapshots.ids.length) { - return (false, 0); - } else { - return (true, snapshots.values[index]); - } - } - - function _updateAccountSnapshot(address account) private { - _updateSnapshot(_accountBalanceSnapshots[account], balanceOf(account)); - } - - function _updateTotalSupplySnapshot() private { - _updateSnapshot(_totalSupplySnapshots, totalSupply()); - } - - function _updateSnapshot(Snapshots storage snapshots, uint256 currentValue) private { - uint256 currentId = _getCurrentSnapshotId(); - if (_lastSnapshotId(snapshots.ids) < currentId) { - snapshots.ids.push(currentId); - snapshots.values.push(currentValue); - } - } - - function _lastSnapshotId(uint256[] storage ids) private view returns (uint256) { - if (ids.length == 0) { - return 0; - } else { - return ids[ids.length - 1]; - } - } -} diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index 24fc7c2e5..e7cda4f35 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -11,7 +11,7 @@ import "../../../utils/math/SafeCast.sol"; * @dev Extension of ERC20 to support Compound-like voting and delegation. This version is more generic than Compound's, * and supports token supply up to 2^224^ - 1, while COMP is limited to 2^96^ - 1. * - * NOTE: If exact COMP compatibility is required, use the {ERC20VotesComp} variant of this module. + * NOTE: This contract does not provide interface compatibility with Compound's COMP token. * * This extension keeps a history (checkpoints) of each account's vote power. Vote power can be delegated either * by calling the {delegate} function directly, or by providing a signature to be used with {delegateBySig}. Voting diff --git a/contracts/token/ERC20/extensions/ERC20VotesComp.sol b/contracts/token/ERC20/extensions/ERC20VotesComp.sol deleted file mode 100644 index 0461310a4..000000000 --- a/contracts/token/ERC20/extensions/ERC20VotesComp.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/extensions/ERC20VotesComp.sol) - -pragma solidity ^0.8.0; - -import "./ERC20Votes.sol"; - -/** - * @dev Extension of ERC20 to support Compound's voting and delegation. This version exactly matches Compound's - * interface, with the drawback of only supporting supply up to (2^96^ - 1). - * - * NOTE: You should use this contract if you need exact compatibility with COMP (for example in order to use your token - * with Governor Alpha or Bravo) and if you are sure the supply cap of 2^96^ is enough for you. Otherwise, use the - * {ERC20Votes} variant of this module. - * - * This extension keeps a history (checkpoints) of each account's vote power. Vote power can be delegated either - * by calling the {delegate} function directly, or by providing a signature to be used with {delegateBySig}. Voting - * power can be queried through the public accessors {getCurrentVotes} and {getPriorVotes}. - * - * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it - * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. - * - * _Available since v4.2._ - */ -abstract contract ERC20VotesComp is ERC20Votes { - /** - * @dev Comp version of the {getVotes} accessor, with `uint96` return type. - */ - function getCurrentVotes(address account) external view virtual returns (uint96) { - return SafeCast.toUint96(getVotes(account)); - } - - /** - * @dev Comp version of the {getPastVotes} accessor, with `uint96` return type. - */ - function getPriorVotes(address account, uint256 blockNumber) external view virtual returns (uint96) { - return SafeCast.toUint96(getPastVotes(account, blockNumber)); - } - - /** - * @dev Maximum token supply. Reduced to `type(uint96).max` (2^96^ - 1) to fit COMP interface. - */ - function _maxSupply() internal view virtual override returns (uint224) { - return type(uint96).max; - } -} diff --git a/contracts/token/ERC20/utils/TokenTimelock.sol b/contracts/token/ERC20/utils/TokenTimelock.sol deleted file mode 100644 index cffdac5d4..000000000 --- a/contracts/token/ERC20/utils/TokenTimelock.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/utils/TokenTimelock.sol) - -pragma solidity ^0.8.0; - -import "./SafeERC20.sol"; - -/** - * @dev A token holder contract that will allow a beneficiary to extract the - * tokens after a given release time. - * - * Useful for simple vesting schedules like "advisors get all of their tokens - * after 1 year". - */ -contract TokenTimelock { - using SafeERC20 for IERC20; - - // ERC20 basic token contract being held - IERC20 private immutable _token; - - // beneficiary of tokens after they are released - address private immutable _beneficiary; - - // timestamp when token release is enabled - uint256 private immutable _releaseTime; - - /** - * @dev Deploys a timelock instance that is able to hold the token specified, and will only release it to - * `beneficiary_` when {release} is invoked after `releaseTime_`. The release time is specified as a Unix timestamp - * (in seconds). - */ - constructor(IERC20 token_, address beneficiary_, uint256 releaseTime_) { - require(releaseTime_ > block.timestamp, "TokenTimelock: release time is before current time"); - _token = token_; - _beneficiary = beneficiary_; - _releaseTime = releaseTime_; - } - - /** - * @dev Returns the token being held. - */ - function token() public view virtual returns (IERC20) { - return _token; - } - - /** - * @dev Returns the beneficiary that will receive the tokens. - */ - function beneficiary() public view virtual returns (address) { - return _beneficiary; - } - - /** - * @dev Returns the time when the tokens are released in seconds since Unix epoch (i.e. Unix timestamp). - */ - function releaseTime() public view virtual returns (uint256) { - return _releaseTime; - } - - /** - * @dev Transfers tokens held by the timelock to the beneficiary. Will only succeed if invoked after the release - * time. - */ - function release() public virtual { - require(block.timestamp >= releaseTime(), "TokenTimelock: current time is before release time"); - - uint256 amount = token().balanceOf(address(this)); - require(amount > 0, "TokenTimelock: no tokens to release"); - - token().safeTransfer(beneficiary(), amount); - } -} diff --git a/scripts/upgradeable/upgradeable.patch b/scripts/upgradeable/upgradeable.patch index bc675ab8a..efc89cab5 100644 --- a/scripts/upgradeable/upgradeable.patch +++ b/scripts/upgradeable/upgradeable.patch @@ -101,13 +101,13 @@ index 9fc95518..53130e3c 100644 } ``` diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol -index fe67eb54..d26ea4e1 100644 +index bb70d19f..38513771 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol -@@ -15,6 +15,8 @@ import "../utils/Context.sol"; - * Any token transferred to this contract will follow the vesting schedule as if they were locked from the beginning. - * Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly) - * be immediately releasable. +@@ -18,6 +18,8 @@ import "../utils/Context.sol"; + * + * By setting the duration to 0, one can configure this contract to behave like an asset timelock that hold tokens for + * a beneficiary until a specified time. + * + * @custom:storage-size 52 */ @@ -126,19 +126,6 @@ index 64431711..885f0e42 100644 */ abstract contract GovernorVotes is Governor { IERC5805 public immutable token; -diff --git a/contracts/governance/extensions/GovernorVotesComp.sol b/contracts/governance/extensions/GovernorVotesComp.sol -index 17250ad7..1d26b72e 100644 ---- a/contracts/governance/extensions/GovernorVotesComp.sol -+++ b/contracts/governance/extensions/GovernorVotesComp.sol -@@ -10,6 +10,8 @@ import "../../token/ERC20/extensions/ERC20VotesComp.sol"; - * @dev Extension of {Governor} for voting weight extraction from a Comp token. - * - * _Available since v4.3._ -+ * -+ * @custom:storage-size 51 - */ - abstract contract GovernorVotesComp is Governor { - ERC20VotesComp public immutable token; diff --git a/contracts/package.json b/contracts/package.json index 55e70b17..ceefb984 100644 --- a/contracts/package.json @@ -198,19 +185,6 @@ index bfe782e4..7264fe32 100644 */ abstract contract ERC20Wrapper is ERC20 { IERC20 private immutable _underlying; -diff --git a/contracts/token/ERC20/utils/TokenTimelock.sol b/contracts/token/ERC20/utils/TokenTimelock.sol -index ed855b7b..3d30f59d 100644 ---- a/contracts/token/ERC20/utils/TokenTimelock.sol -+++ b/contracts/token/ERC20/utils/TokenTimelock.sol -@@ -11,6 +11,8 @@ import "./SafeERC20.sol"; - * - * Useful for simple vesting schedules like "advisors get all of their tokens - * after 1 year". -+ * -+ * @custom:storage-size 53 - */ - contract TokenTimelock { - using SafeERC20 for IERC20; diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol index 6a4e1cad..55d8eced 100644 --- a/contracts/utils/cryptography/EIP712.sol diff --git a/test/finance/PaymentSplitter.test.js b/test/finance/PaymentSplitter.test.js deleted file mode 100644 index 1408c9f51..000000000 --- a/test/finance/PaymentSplitter.test.js +++ /dev/null @@ -1,217 +0,0 @@ -const { balance, constants, ether, expectEvent, send, expectRevert } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS } = constants; - -const { expect } = require('chai'); - -const PaymentSplitter = artifacts.require('PaymentSplitter'); -const ERC20 = artifacts.require('$ERC20'); - -contract('PaymentSplitter', function (accounts) { - const [owner, payee1, payee2, payee3, nonpayee1, payer1] = accounts; - - const amount = ether('1'); - - it('rejects an empty set of payees', async function () { - await expectRevert(PaymentSplitter.new([], []), 'PaymentSplitter: no payees'); - }); - - it('rejects more payees than shares', async function () { - await expectRevert( - PaymentSplitter.new([payee1, payee2, payee3], [20, 30]), - 'PaymentSplitter: payees and shares length mismatch', - ); - }); - - it('rejects more shares than payees', async function () { - await expectRevert( - PaymentSplitter.new([payee1, payee2], [20, 30, 40]), - 'PaymentSplitter: payees and shares length mismatch', - ); - }); - - it('rejects null payees', async function () { - await expectRevert( - PaymentSplitter.new([payee1, ZERO_ADDRESS], [20, 30]), - 'PaymentSplitter: account is the zero address', - ); - }); - - it('rejects zero-valued shares', async function () { - await expectRevert(PaymentSplitter.new([payee1, payee2], [20, 0]), 'PaymentSplitter: shares are 0'); - }); - - it('rejects repeated payees', async function () { - await expectRevert(PaymentSplitter.new([payee1, payee1], [20, 30]), 'PaymentSplitter: account already has shares'); - }); - - context('once deployed', function () { - beforeEach(async function () { - this.payees = [payee1, payee2, payee3]; - this.shares = [20, 10, 70]; - - this.contract = await PaymentSplitter.new(this.payees, this.shares); - this.token = await ERC20.new('MyToken', 'MT'); - await this.token.$_mint(owner, ether('1000')); - }); - - it('has total shares', async function () { - expect(await this.contract.totalShares()).to.be.bignumber.equal('100'); - }); - - it('has payees', async function () { - await Promise.all( - this.payees.map(async (payee, index) => { - expect(await this.contract.payee(index)).to.equal(payee); - expect(await this.contract.released(payee)).to.be.bignumber.equal('0'); - expect(await this.contract.releasable(payee)).to.be.bignumber.equal('0'); - }), - ); - }); - - describe('accepts payments', function () { - it('Ether', async function () { - await send.ether(owner, this.contract.address, amount); - - expect(await balance.current(this.contract.address)).to.be.bignumber.equal(amount); - }); - - it('Token', async function () { - await this.token.transfer(this.contract.address, amount, { from: owner }); - - expect(await this.token.balanceOf(this.contract.address)).to.be.bignumber.equal(amount); - }); - }); - - describe('shares', function () { - it('stores shares if address is payee', async function () { - expect(await this.contract.shares(payee1)).to.be.bignumber.not.equal('0'); - }); - - it('does not store shares if address is not payee', async function () { - expect(await this.contract.shares(nonpayee1)).to.be.bignumber.equal('0'); - }); - }); - - describe('release', function () { - describe('Ether', function () { - it('reverts if no funds to claim', async function () { - await expectRevert(this.contract.release(payee1), 'PaymentSplitter: account is not due payment'); - }); - it('reverts if non-payee want to claim', async function () { - await send.ether(payer1, this.contract.address, amount); - await expectRevert(this.contract.release(nonpayee1), 'PaymentSplitter: account has no shares'); - }); - }); - - describe('Token', function () { - it('reverts if no funds to claim', async function () { - await expectRevert( - this.contract.release(this.token.address, payee1), - 'PaymentSplitter: account is not due payment', - ); - }); - it('reverts if non-payee want to claim', async function () { - await this.token.transfer(this.contract.address, amount, { from: owner }); - await expectRevert( - this.contract.release(this.token.address, nonpayee1), - 'PaymentSplitter: account has no shares', - ); - }); - }); - }); - - describe('tracks releasable and released', function () { - it('Ether', async function () { - await send.ether(payer1, this.contract.address, amount); - const payment = amount.divn(10); - expect(await this.contract.releasable(payee2)).to.be.bignumber.equal(payment); - await this.contract.release(payee2); - expect(await this.contract.releasable(payee2)).to.be.bignumber.equal('0'); - expect(await this.contract.released(payee2)).to.be.bignumber.equal(payment); - }); - - it('Token', async function () { - await this.token.transfer(this.contract.address, amount, { from: owner }); - const payment = amount.divn(10); - expect(await this.contract.releasable(this.token.address, payee2, {})).to.be.bignumber.equal(payment); - await this.contract.release(this.token.address, payee2); - expect(await this.contract.releasable(this.token.address, payee2, {})).to.be.bignumber.equal('0'); - expect(await this.contract.released(this.token.address, payee2)).to.be.bignumber.equal(payment); - }); - }); - - describe('distributes funds to payees', function () { - it('Ether', async function () { - await send.ether(payer1, this.contract.address, amount); - - // receive funds - const initBalance = await balance.current(this.contract.address); - expect(initBalance).to.be.bignumber.equal(amount); - - // distribute to payees - - const tracker1 = await balance.tracker(payee1); - const receipt1 = await this.contract.release(payee1); - const profit1 = await tracker1.delta(); - expect(profit1).to.be.bignumber.equal(ether('0.20')); - expectEvent(receipt1, 'PaymentReleased', { to: payee1, amount: profit1 }); - - const tracker2 = await balance.tracker(payee2); - const receipt2 = await this.contract.release(payee2); - const profit2 = await tracker2.delta(); - expect(profit2).to.be.bignumber.equal(ether('0.10')); - expectEvent(receipt2, 'PaymentReleased', { to: payee2, amount: profit2 }); - - const tracker3 = await balance.tracker(payee3); - const receipt3 = await this.contract.release(payee3); - const profit3 = await tracker3.delta(); - expect(profit3).to.be.bignumber.equal(ether('0.70')); - expectEvent(receipt3, 'PaymentReleased', { to: payee3, amount: profit3 }); - - // end balance should be zero - expect(await balance.current(this.contract.address)).to.be.bignumber.equal('0'); - - // check correct funds released accounting - expect(await this.contract.totalReleased()).to.be.bignumber.equal(initBalance); - }); - - it('Token', async function () { - expect(await this.token.balanceOf(payee1)).to.be.bignumber.equal('0'); - expect(await this.token.balanceOf(payee2)).to.be.bignumber.equal('0'); - expect(await this.token.balanceOf(payee3)).to.be.bignumber.equal('0'); - - await this.token.transfer(this.contract.address, amount, { from: owner }); - - expectEvent(await this.contract.release(this.token.address, payee1), 'ERC20PaymentReleased', { - token: this.token.address, - to: payee1, - amount: ether('0.20'), - }); - - await this.token.transfer(this.contract.address, amount, { from: owner }); - - expectEvent(await this.contract.release(this.token.address, payee1), 'ERC20PaymentReleased', { - token: this.token.address, - to: payee1, - amount: ether('0.20'), - }); - - expectEvent(await this.contract.release(this.token.address, payee2), 'ERC20PaymentReleased', { - token: this.token.address, - to: payee2, - amount: ether('0.20'), - }); - - expectEvent(await this.contract.release(this.token.address, payee3), 'ERC20PaymentReleased', { - token: this.token.address, - to: payee3, - amount: ether('1.40'), - }); - - expect(await this.token.balanceOf(payee1)).to.be.bignumber.equal(ether('0.40')); - expect(await this.token.balanceOf(payee2)).to.be.bignumber.equal(ether('0.20')); - expect(await this.token.balanceOf(payee3)).to.be.bignumber.equal(ether('1.40')); - }); - }); - }); -}); diff --git a/test/finance/VestingWallet.test.js b/test/finance/VestingWallet.test.js index a110b2cd9..09205a99c 100644 --- a/test/finance/VestingWallet.test.js +++ b/test/finance/VestingWallet.test.js @@ -30,6 +30,7 @@ contract('VestingWallet', function (accounts) { expect(await this.mock.beneficiary()).to.be.equal(beneficiary); expect(await this.mock.start()).to.be.bignumber.equal(this.start); expect(await this.mock.duration()).to.be.bignumber.equal(duration); + expect(await this.mock.end()).to.be.bignumber.equal(this.start.add(duration)); }); describe('vesting schedule', function () { diff --git a/test/governance/compatibility/GovernorCompatibilityBravo.test.js b/test/governance/compatibility/GovernorCompatibilityBravo.test.js index d6d9f6a3c..9c45277d1 100644 --- a/test/governance/compatibility/GovernorCompatibilityBravo.test.js +++ b/test/governance/compatibility/GovernorCompatibilityBravo.test.js @@ -21,8 +21,8 @@ function makeContractAddress(creator, nonce) { } const TOKENS = [ - { Token: artifacts.require('$ERC20VotesComp'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesCompTimestampMock'), mode: 'timestamp' }, + { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, ]; contract('GovernorCompatibilityBravo', function (accounts) { diff --git a/test/governance/extensions/GovernorComp.test.js b/test/governance/extensions/GovernorComp.test.js deleted file mode 100644 index 92a59d05f..000000000 --- a/test/governance/extensions/GovernorComp.test.js +++ /dev/null @@ -1,89 +0,0 @@ -const { expect } = require('chai'); - -const Enums = require('../../helpers/enums'); -const { GovernorHelper } = require('../../helpers/governance'); - -const Governor = artifacts.require('$GovernorCompMock'); -const CallReceiver = artifacts.require('CallReceiverMock'); - -const TOKENS = [ - { Token: artifacts.require('$ERC20VotesComp'), mode: 'blocknumber' }, - { Token: artifacts.require('$ERC20VotesCompTimestampMock'), mode: 'timestamp' }, -]; - -contract('GovernorComp', function (accounts) { - const [owner, voter1, voter2, voter3, voter4] = accounts; - - const name = 'OZ-Governor'; - const version = '1'; - const tokenName = 'MockToken'; - const tokenSymbol = 'MTKN'; - const tokenSupply = web3.utils.toWei('100'); - const votingDelay = web3.utils.toBN(4); - const votingPeriod = web3.utils.toBN(16); - const value = web3.utils.toWei('1'); - - for (const { mode, Token } of TOKENS) { - describe(`using ${Token._json.contractName}`, function () { - beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.mock = await Governor.new(name, this.token.address); - this.receiver = await CallReceiver.new(); - - this.helper = new GovernorHelper(this.mock, mode); - - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); - - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - }, - ], - '', - ); - }); - - it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - }); - - it('voting with comp token', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); - - await this.mock.proposalVotes(this.proposal.id).then(results => { - expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17')); - expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5')); - expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2')); - }); - }); - }); - } -}); diff --git a/test/token/ERC20/extensions/ERC20Snapshot.test.js b/test/token/ERC20/extensions/ERC20Snapshot.test.js deleted file mode 100644 index fb0bb31d3..000000000 --- a/test/token/ERC20/extensions/ERC20Snapshot.test.js +++ /dev/null @@ -1,207 +0,0 @@ -const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); -const ERC20Snapshot = artifacts.require('$ERC20Snapshot'); - -const { expect } = require('chai'); - -contract('ERC20Snapshot', function (accounts) { - const [initialHolder, recipient, other] = accounts; - - const initialSupply = new BN(100); - - const name = 'My Token'; - const symbol = 'MTKN'; - - beforeEach(async function () { - this.token = await ERC20Snapshot.new(name, symbol); - await this.token.$_mint(initialHolder, initialSupply); - }); - - describe('snapshot', function () { - it('emits a snapshot event', async function () { - const receipt = await this.token.$_snapshot(); - expectEvent(receipt, 'Snapshot'); - }); - - it('creates increasing snapshots ids, starting from 1', async function () { - for (const id of ['1', '2', '3', '4', '5']) { - const receipt = await this.token.$_snapshot(); - expectEvent(receipt, 'Snapshot', { id }); - } - }); - }); - - describe('totalSupplyAt', function () { - it('reverts with a snapshot id of 0', async function () { - await expectRevert(this.token.totalSupplyAt(0), 'ERC20Snapshot: id is 0'); - }); - - it('reverts with a not-yet-created snapshot id', async function () { - await expectRevert(this.token.totalSupplyAt(1), 'ERC20Snapshot: nonexistent id'); - }); - - context('with initial snapshot', function () { - beforeEach(async function () { - this.initialSnapshotId = new BN('1'); - - const receipt = await this.token.$_snapshot(); - expectEvent(receipt, 'Snapshot', { id: this.initialSnapshotId }); - }); - - context('with no supply changes after the snapshot', function () { - it('returns the current total supply', async function () { - expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply); - }); - }); - - context('with supply changes after the snapshot', function () { - beforeEach(async function () { - await this.token.$_mint(other, new BN('50')); - await this.token.$_burn(initialHolder, new BN('20')); - }); - - it('returns the total supply before the changes', async function () { - expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply); - }); - - context('with a second snapshot after supply changes', function () { - beforeEach(async function () { - this.secondSnapshotId = new BN('2'); - - const receipt = await this.token.$_snapshot(); - expectEvent(receipt, 'Snapshot', { id: this.secondSnapshotId }); - }); - - it('snapshots return the supply before and after the changes', async function () { - expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply); - - expect(await this.token.totalSupplyAt(this.secondSnapshotId)).to.be.bignumber.equal( - await this.token.totalSupply(), - ); - }); - }); - - context('with multiple snapshots after supply changes', function () { - beforeEach(async function () { - this.secondSnapshotIds = ['2', '3', '4']; - - for (const id of this.secondSnapshotIds) { - const receipt = await this.token.$_snapshot(); - expectEvent(receipt, 'Snapshot', { id }); - } - }); - - it('all posterior snapshots return the supply after the changes', async function () { - expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply); - - const currentSupply = await this.token.totalSupply(); - - for (const id of this.secondSnapshotIds) { - expect(await this.token.totalSupplyAt(id)).to.be.bignumber.equal(currentSupply); - } - }); - }); - }); - }); - }); - - describe('balanceOfAt', function () { - it('reverts with a snapshot id of 0', async function () { - await expectRevert(this.token.balanceOfAt(other, 0), 'ERC20Snapshot: id is 0'); - }); - - it('reverts with a not-yet-created snapshot id', async function () { - await expectRevert(this.token.balanceOfAt(other, 1), 'ERC20Snapshot: nonexistent id'); - }); - - context('with initial snapshot', function () { - beforeEach(async function () { - this.initialSnapshotId = new BN('1'); - - const receipt = await this.token.$_snapshot(); - expectEvent(receipt, 'Snapshot', { id: this.initialSnapshotId }); - }); - - context('with no balance changes after the snapshot', function () { - it('returns the current balance for all accounts', async function () { - expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal( - initialSupply, - ); - expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0'); - expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0'); - }); - }); - - context('with balance changes after the snapshot', function () { - beforeEach(async function () { - await this.token.transfer(recipient, new BN('10'), { from: initialHolder }); - await this.token.$_mint(other, new BN('50')); - await this.token.$_burn(initialHolder, new BN('20')); - }); - - it('returns the balances before the changes', async function () { - expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal( - initialSupply, - ); - expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0'); - expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0'); - }); - - context('with a second snapshot after supply changes', function () { - beforeEach(async function () { - this.secondSnapshotId = new BN('2'); - - const receipt = await this.token.$_snapshot(); - expectEvent(receipt, 'Snapshot', { id: this.secondSnapshotId }); - }); - - it('snapshots return the balances before and after the changes', async function () { - expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal( - initialSupply, - ); - expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0'); - expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0'); - - expect(await this.token.balanceOfAt(initialHolder, this.secondSnapshotId)).to.be.bignumber.equal( - await this.token.balanceOf(initialHolder), - ); - expect(await this.token.balanceOfAt(recipient, this.secondSnapshotId)).to.be.bignumber.equal( - await this.token.balanceOf(recipient), - ); - expect(await this.token.balanceOfAt(other, this.secondSnapshotId)).to.be.bignumber.equal( - await this.token.balanceOf(other), - ); - }); - }); - - context('with multiple snapshots after supply changes', function () { - beforeEach(async function () { - this.secondSnapshotIds = ['2', '3', '4']; - - for (const id of this.secondSnapshotIds) { - const receipt = await this.token.$_snapshot(); - expectEvent(receipt, 'Snapshot', { id }); - } - }); - - it('all posterior snapshots return the supply after the changes', async function () { - expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal( - initialSupply, - ); - expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0'); - expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0'); - - for (const id of this.secondSnapshotIds) { - expect(await this.token.balanceOfAt(initialHolder, id)).to.be.bignumber.equal( - await this.token.balanceOf(initialHolder), - ); - expect(await this.token.balanceOfAt(recipient, id)).to.be.bignumber.equal( - await this.token.balanceOf(recipient), - ); - expect(await this.token.balanceOfAt(other, id)).to.be.bignumber.equal(await this.token.balanceOf(other)); - } - }); - }); - }); - }); - }); -}); diff --git a/test/token/ERC20/extensions/ERC20VotesComp.test.js b/test/token/ERC20/extensions/ERC20VotesComp.test.js deleted file mode 100644 index 815f22de0..000000000 --- a/test/token/ERC20/extensions/ERC20VotesComp.test.js +++ /dev/null @@ -1,579 +0,0 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS } = constants; - -const { batchInBlock } = require('../../../helpers/txpool'); -const { shouldBehaveLikeVotes } = require('../../../governance/utils/Votes.behavior'); -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const { getDomain, domainType, domainSeparator } = require('../../../helpers/eip712'); -const { clock, clockFromReceipt } = require('../../../helpers/time'); - -const Delegation = [ - { name: 'delegatee', type: 'address' }, - { name: 'nonce', type: 'uint256' }, - { name: 'expiry', type: 'uint256' }, -]; - -const MODES = { - blocknumber: artifacts.require('$ERC20VotesComp'), - // no timestamp mode for ERC20VotesComp yet -}; - -contract('ERC20VotesComp', function (accounts) { - const [holder, recipient, holderDelegatee, other1, other2] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - const supply = new BN('10000000000000000000000000'); - - for (const [mode, artifact] of Object.entries(MODES)) { - describe(`vote with ${mode}`, function () { - beforeEach(async function () { - this.token = await artifact.new(name, symbol, name, version); - this.votes = this.token; - }); - - // includes EIP6372 behavior check - shouldBehaveLikeVotes(accounts, [1, 17, 42], { mode, fungible: true }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator)); - }); - - it('minting restriction', async function () { - const amount = new BN('2').pow(new BN('96')); - await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes'); - }); - - it('recent checkpoints', async function () { - await this.token.delegate(holder, { from: holder }); - for (let i = 0; i < 6; i++) { - await this.token.$_mint(holder, 1); - } - const timepoint = await clock[mode](); - expect(await this.token.numCheckpoints(holder)).to.be.bignumber.equal('6'); - // recent - expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal('5'); - // non-recent - expect(await this.token.getPastVotes(holder, timepoint - 6)).to.be.bignumber.equal('0'); - }); - - describe('set delegation', function () { - describe('call', function () { - it('delegation with balance', async function () { - await this.token.$_mint(holder, supply); - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - const timepoint = await clockFromReceipt[mode](receipt); - - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: '0', - newBalance: supply, - }); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - - expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(supply); - expect(await this.token.getPriorVotes(holder, timepoint - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal(supply); - }); - - it('delegation without balance', async function () { - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - }); - }); - - describe('with signature', function () { - const delegator = Wallet.generate(); - const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); - const nonce = 0; - - const buildData = (contract, message) => - getDomain(contract).then(domain => ({ - primaryType: 'Delegation', - types: { EIP712Domain: domainType(domain), Delegation }, - domain, - message, - })); - - beforeEach(async function () { - await this.token.$_mint(delegatorAddress, supply); - }); - - it('accept signed delegation', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - - expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - const timepoint = await clockFromReceipt[mode](receipt); - - expectEvent(receipt, 'DelegateChanged', { - delegator: delegatorAddress, - fromDelegate: ZERO_ADDRESS, - toDelegate: delegatorAddress, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: delegatorAddress, - previousBalance: '0', - newBalance: supply, - }); - - expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); - - expect(await this.token.getCurrentVotes(delegatorAddress)).to.be.bignumber.equal(supply); - expect(await this.token.getPriorVotes(delegatorAddress, timepoint - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPriorVotes(delegatorAddress, timepoint)).to.be.bignumber.equal(supply); - }); - - it('rejects reused signature', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - - await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), - 'Votes: invalid nonce', - ); - }); - - it('rejects bad delegatee', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - - const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); - const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged'); - expect(args.delegator).to.not.be.equal(delegatorAddress); - expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); - expect(args.toDelegate).to.be.equal(holderDelegatee); - }); - - it('rejects bad nonce', async function () { - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), - 'Votes: invalid nonce', - ); - }); - - it('rejects expired permit', async function () { - const expiry = (await time.latest()) - time.duration.weeks(1); - const { v, r, s } = await buildData(this.token, { - delegatee: delegatorAddress, - nonce, - expiry, - }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), - 'Votes: signature expired', - ); - }); - }); - }); - - describe('change delegation', function () { - beforeEach(async function () { - await this.token.$_mint(holder, supply); - await this.token.delegate(holder, { from: holder }); - }); - - it('call', async function () { - expect(await this.token.delegates(holder)).to.be.equal(holder); - - const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); - const timepoint = await clockFromReceipt[mode](receipt); - - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: holder, - toDelegate: holderDelegatee, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: '0', - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holderDelegatee, - previousBalance: '0', - newBalance: supply, - }); - - expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); - - expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal('0'); - expect(await this.token.getCurrentVotes(holderDelegatee)).to.be.bignumber.equal(supply); - expect(await this.token.getPriorVotes(holder, timepoint - 1)).to.be.bignumber.equal(supply); - expect(await this.token.getPriorVotes(holderDelegatee, timepoint - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal('0'); - expect(await this.token.getPriorVotes(holderDelegatee, timepoint)).to.be.bignumber.equal(supply); - }); - }); - - describe('transfers', function () { - beforeEach(async function () { - await this.token.$_mint(holder, supply); - }); - - it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); - - this.holderVotes = '0'; - this.recipientVotes = '0'; - }); - - it('sender delegation', async function () { - await this.token.delegate(holder, { from: holder }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: supply.subn(1), - }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.holderVotes = supply.subn(1); - this.recipientVotes = '0'; - }); - - it('receiver delegation', async function () { - await this.token.delegate(recipient, { from: recipient }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.holderVotes = '0'; - this.recipientVotes = '1'; - }); - - it('full delegation', async function () { - await this.token.delegate(holder, { from: holder }); - await this.token.delegate(recipient, { from: recipient }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: supply.subn(1), - }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.holderVotes = supply.subn(1); - this.recipientVotes = '1'; - }); - - afterEach(async function () { - expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getCurrentVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); - - // need to advance 2 blocks to see the effect of a transfer on "getPriorVotes" - const timepoint = await clock[mode](); - await time.advanceBlock(); - expect(await this.token.getPriorVotes(holder, timepoint)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getPriorVotes(recipient, timepoint)).to.be.bignumber.equal(this.recipientVotes); - }); - }); - - // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. - describe('Compound test suite', function () { - beforeEach(async function () { - await this.token.$_mint(holder, supply); - }); - - describe('balanceOf', function () { - it('grants to initial account', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); - }); - }); - - describe('numCheckpoints', function () { - it('returns the number of checkpoints for a delegate', async function () { - await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const t1 = await this.token.delegate(other1, { from: recipient }); - t1.timepoint = await clockFromReceipt[mode](t1.receipt); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - - const t2 = await this.token.transfer(other2, 10, { from: recipient }); - t2.timepoint = await clockFromReceipt[mode](t2.receipt); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transfer(other2, 10, { from: recipient }); - t3.timepoint = await clockFromReceipt[mode](t3.receipt); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - t4.timepoint = await clockFromReceipt[mode](t4.receipt); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '100']); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.timepoint.toString(), '90']); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.timepoint.toString(), '80']); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.timepoint.toString(), '100']); - - await time.advanceBlock(); - expect(await this.token.getPriorVotes(other1, t1.timepoint)).to.be.bignumber.equal('100'); - expect(await this.token.getPriorVotes(other1, t2.timepoint)).to.be.bignumber.equal('90'); - expect(await this.token.getPriorVotes(other1, t3.timepoint)).to.be.bignumber.equal('80'); - expect(await this.token.getPriorVotes(other1, t4.timepoint)).to.be.bignumber.equal('100'); - }); - - it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, '100', { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const [t1, t2, t3] = await batchInBlock([ - () => this.token.delegate(other1, { from: recipient, gas: 200000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 200000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 200000 }), - ]); - t1.timepoint = await clockFromReceipt[mode](t1.receipt); - t2.timepoint = await clockFromReceipt[mode](t2.receipt); - t3.timepoint = await clockFromReceipt[mode](t3.receipt); - - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '80']); - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - t4.timepoint = await clockFromReceipt[mode](t4.receipt); - - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.timepoint.toString(), '100']); - }); - }); - - describe('getPriorVotes', function () { - it('reverts if block number >= current block', async function () { - await expectRevert(this.token.getPriorVotes(other1, 5e10), 'Votes: future lookup'); - }); - - it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPriorVotes(other1, 0)).to.be.bignumber.equal('0'); - }); - - it('returns the latest block if >= last checkpoint block', async function () { - const { receipt } = await this.token.delegate(other1, { from: holder }); - const timepoint = await clockFromReceipt[mode](receipt); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPriorVotes(other1, timepoint)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPriorVotes(other1, timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - - it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const { receipt } = await this.token.delegate(other1, { from: holder }); - const timepoint = await clockFromReceipt[mode](receipt); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPriorVotes(other1, timepoint - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPriorVotes(other1, timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 20, { from: other2 }); - await time.advanceBlock(); - await time.advanceBlock(); - - t1.timepoint = await clockFromReceipt[mode](t1.receipt); - t2.timepoint = await clockFromReceipt[mode](t2.receipt); - t3.timepoint = await clockFromReceipt[mode](t3.receipt); - t4.timepoint = await clockFromReceipt[mode](t4.receipt); - - expect(await this.token.getPriorVotes(other1, t1.timepoint - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPriorVotes(other1, t1.timepoint)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPriorVotes(other1, t1.timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPriorVotes(other1, t2.timepoint)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPriorVotes(other1, t2.timepoint + 1)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPriorVotes(other1, t3.timepoint)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPriorVotes(other1, t3.timepoint + 1)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPriorVotes(other1, t4.timepoint)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPriorVotes(other1, t4.timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - }); - }); - - describe('getPastTotalSupply', function () { - beforeEach(async function () { - await this.token.delegate(holder, { from: holder }); - }); - - it('reverts if block number >= current block', async function () { - await expectRevert(this.token.getPastTotalSupply(5e10), 'Votes: future lookup'); - }); - - it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); - }); - - it('returns the latest block if >= last checkpoint block', async function () { - const { receipt } = await this.token.$_mint(holder, supply); - const timepoint = await clockFromReceipt[mode](receipt); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(timepoint)).to.be.bignumber.equal(supply); - expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(supply); - }); - - it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const { receipt } = await this.token.$_mint(holder, supply); - const timepoint = await clockFromReceipt[mode](receipt); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.$_mint(holder, supply); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.$_burn(holder, 10); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.$_burn(holder, 10); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.$_mint(holder, 20); - await time.advanceBlock(); - await time.advanceBlock(); - - t1.timepoint = await clockFromReceipt[mode](t1.receipt); - t2.timepoint = await clockFromReceipt[mode](t2.receipt); - t3.timepoint = await clockFromReceipt[mode](t3.receipt); - t4.timepoint = await clockFromReceipt[mode](t4.receipt); - - expect(await this.token.getPastTotalSupply(t1.timepoint - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal('9999999999999999999999990'); - expect(await this.token.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal('9999999999999999999999980'); - expect(await this.token.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal('10000000000000000000000000'); - expect(await this.token.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - }); - }); - } -}); diff --git a/test/token/ERC20/utils/TokenTimelock.test.js b/test/token/ERC20/utils/TokenTimelock.test.js deleted file mode 100644 index 22e8071eb..000000000 --- a/test/token/ERC20/utils/TokenTimelock.test.js +++ /dev/null @@ -1,71 +0,0 @@ -const { BN, expectRevert, time } = require('@openzeppelin/test-helpers'); - -const { expect } = require('chai'); - -const ERC20 = artifacts.require('$ERC20'); -const TokenTimelock = artifacts.require('TokenTimelock'); - -contract('TokenTimelock', function (accounts) { - const [beneficiary] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - - const amount = new BN(100); - - context('with token', function () { - beforeEach(async function () { - this.token = await ERC20.new(name, symbol); - }); - - it('rejects a release time in the past', async function () { - const pastReleaseTime = (await time.latest()).sub(time.duration.years(1)); - await expectRevert( - TokenTimelock.new(this.token.address, beneficiary, pastReleaseTime), - 'TokenTimelock: release time is before current time', - ); - }); - - context('once deployed', function () { - beforeEach(async function () { - this.releaseTime = (await time.latest()).add(time.duration.years(1)); - this.timelock = await TokenTimelock.new(this.token.address, beneficiary, this.releaseTime); - await this.token.$_mint(this.timelock.address, amount); - }); - - it('can get state', async function () { - expect(await this.timelock.token()).to.equal(this.token.address); - expect(await this.timelock.beneficiary()).to.equal(beneficiary); - expect(await this.timelock.releaseTime()).to.be.bignumber.equal(this.releaseTime); - }); - - it('cannot be released before time limit', async function () { - await expectRevert(this.timelock.release(), 'TokenTimelock: current time is before release time'); - }); - - it('cannot be released just before time limit', async function () { - await time.increaseTo(this.releaseTime.sub(time.duration.seconds(3))); - await expectRevert(this.timelock.release(), 'TokenTimelock: current time is before release time'); - }); - - it('can be released just after limit', async function () { - await time.increaseTo(this.releaseTime.add(time.duration.seconds(1))); - await this.timelock.release(); - expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(amount); - }); - - it('can be released after time limit', async function () { - await time.increaseTo(this.releaseTime.add(time.duration.years(1))); - await this.timelock.release(); - expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(amount); - }); - - it('cannot be released twice', async function () { - await time.increaseTo(this.releaseTime.add(time.duration.years(1))); - await this.timelock.release(); - await expectRevert(this.timelock.release(), 'TokenTimelock: no tokens to release'); - expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(amount); - }); - }); - }); -}); From 7bb5592ad5f7a1f28efbc358940f251ad0841366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Mon, 29 May 2023 13:37:37 -0600 Subject: [PATCH 147/197] Bump minimum pragma version to `0.8.19` (#4288) --- .changeset/short-eels-enjoy.md | 5 +++++ README.md | 2 +- .../AccessControlDefaultAdminRulesHarness.sol | 2 +- certora/harnesses/AccessControlHarness.sol | 2 +- certora/harnesses/DoubleEndedQueueHarness.sol | 2 +- certora/harnesses/ERC20FlashMintHarness.sol | 2 +- certora/harnesses/ERC20PermitHarness.sol | 2 +- certora/harnesses/ERC20WrapperHarness.sol | 2 +- certora/harnesses/ERC3156FlashBorrowerHarness.sol | 2 +- certora/harnesses/ERC721Harness.sol | 2 +- certora/harnesses/ERC721ReceiverHarness.sol | 2 +- certora/harnesses/EnumerableMapHarness.sol | 2 +- certora/harnesses/EnumerableSetHarness.sol | 2 +- certora/harnesses/InitializableHarness.sol | 2 +- certora/harnesses/Ownable2StepHarness.sol | 2 +- certora/harnesses/OwnableHarness.sol | 2 +- certora/harnesses/PausableHarness.sol | 2 +- certora/harnesses/TimelockControllerHarness.sol | 2 +- contracts/access/AccessControl.sol | 2 +- contracts/access/AccessControlDefaultAdminRules.sol | 2 +- contracts/access/AccessControlEnumerable.sol | 2 +- contracts/access/IAccessControl.sol | 2 +- contracts/access/IAccessControlDefaultAdminRules.sol | 2 +- contracts/access/IAccessControlEnumerable.sol | 2 +- contracts/access/Ownable.sol | 2 +- contracts/access/Ownable2Step.sol | 2 +- contracts/finance/VestingWallet.sol | 2 +- contracts/governance/Governor.sol | 2 +- contracts/governance/IGovernor.sol | 2 +- contracts/governance/TimelockController.sol | 2 +- .../compatibility/GovernorCompatibilityBravo.sol | 2 +- .../compatibility/IGovernorCompatibilityBravo.sol | 2 +- .../governance/extensions/GovernorCountingSimple.sol | 2 +- .../extensions/GovernorPreventLateQuorum.sol | 2 +- contracts/governance/extensions/GovernorSettings.sol | 2 +- .../governance/extensions/GovernorTimelockCompound.sol | 2 +- .../governance/extensions/GovernorTimelockControl.sol | 2 +- contracts/governance/extensions/GovernorVotes.sol | 2 +- .../extensions/GovernorVotesQuorumFraction.sol | 2 +- contracts/governance/extensions/IGovernorTimelock.sol | 2 +- contracts/governance/utils/IVotes.sol | 2 +- contracts/governance/utils/Votes.sol | 2 +- contracts/interfaces/IERC1155.sol | 2 +- contracts/interfaces/IERC1155MetadataURI.sol | 2 +- contracts/interfaces/IERC1155Receiver.sol | 2 +- contracts/interfaces/IERC1271.sol | 2 +- contracts/interfaces/IERC1363.sol | 2 +- contracts/interfaces/IERC1363Receiver.sol | 2 +- contracts/interfaces/IERC1363Spender.sol | 2 +- contracts/interfaces/IERC165.sol | 2 +- contracts/interfaces/IERC1820Implementer.sol | 2 +- contracts/interfaces/IERC1820Registry.sol | 2 +- contracts/interfaces/IERC1967.sol | 2 +- contracts/interfaces/IERC20.sol | 2 +- contracts/interfaces/IERC20Metadata.sol | 2 +- contracts/interfaces/IERC2309.sol | 2 +- contracts/interfaces/IERC2612.sol | 2 +- contracts/interfaces/IERC2981.sol | 2 +- contracts/interfaces/IERC3156.sol | 2 +- contracts/interfaces/IERC3156FlashBorrower.sol | 2 +- contracts/interfaces/IERC3156FlashLender.sol | 2 +- contracts/interfaces/IERC4626.sol | 2 +- contracts/interfaces/IERC4906.sol | 2 +- contracts/interfaces/IERC5267.sol | 2 +- contracts/interfaces/IERC5313.sol | 2 +- contracts/interfaces/IERC5805.sol | 2 +- contracts/interfaces/IERC6372.sol | 2 +- contracts/interfaces/IERC721.sol | 2 +- contracts/interfaces/IERC721Enumerable.sol | 2 +- contracts/interfaces/IERC721Metadata.sol | 2 +- contracts/interfaces/IERC721Receiver.sol | 2 +- contracts/interfaces/IERC777.sol | 2 +- contracts/interfaces/IERC777Recipient.sol | 2 +- contracts/interfaces/IERC777Sender.sol | 2 +- contracts/interfaces/draft-IERC1822.sol | 2 +- contracts/metatx/ERC2771Context.sol | 2 +- contracts/metatx/MinimalForwarder.sol | 2 +- contracts/mocks/ArraysMock.sol | 2 +- contracts/mocks/CallReceiverMock.sol | 2 +- contracts/mocks/ContextMock.sol | 2 +- contracts/mocks/DummyImplementation.sol | 2 +- contracts/mocks/EIP712Verifier.sol | 2 +- contracts/mocks/ERC1271WalletMock.sol | 2 +- contracts/mocks/ERC165/ERC165InterfacesSupported.sol | 2 +- contracts/mocks/ERC165/ERC165MaliciousData.sol | 2 +- contracts/mocks/ERC165/ERC165MissingData.sol | 2 +- contracts/mocks/ERC165/ERC165NotSupported.sol | 2 +- contracts/mocks/ERC165/ERC165ReturnBomb.sol | 2 +- contracts/mocks/ERC2771ContextMock.sol | 2 +- contracts/mocks/ERC3156FlashBorrowerMock.sol | 2 +- contracts/mocks/EtherReceiverMock.sol | 2 +- contracts/mocks/InitializableMock.sol | 2 +- contracts/mocks/MulticallTest.sol | 2 +- .../mocks/MultipleInheritanceInitializableMocks.sol | 2 +- contracts/mocks/PausableMock.sol | 2 +- contracts/mocks/ReentrancyAttack.sol | 2 +- contracts/mocks/ReentrancyMock.sol | 2 +- contracts/mocks/RegressionImplementation.sol | 2 +- .../mocks/SingleInheritanceInitializableMocks.sol | 2 +- contracts/mocks/StorageSlotMock.sol | 2 +- contracts/mocks/TimelockReentrant.sol | 2 +- contracts/mocks/VotesMock.sol | 2 +- contracts/mocks/compound/CompTimelock.sol | 2 +- contracts/mocks/docs/ERC4626Fees.sol | 2 +- .../governance/GovernorCompatibilityBravoMock.sol | 2 +- contracts/mocks/governance/GovernorMock.sol | 2 +- .../mocks/governance/GovernorPreventLateQuorumMock.sol | 2 +- .../mocks/governance/GovernorTimelockCompoundMock.sol | 2 +- .../mocks/governance/GovernorTimelockControlMock.sol | 2 +- contracts/mocks/governance/GovernorVoteMock.sol | 2 +- contracts/mocks/governance/GovernorWithParamsMock.sol | 2 +- contracts/mocks/proxy/BadBeacon.sol | 2 +- contracts/mocks/proxy/ClashingImplementation.sol | 2 +- contracts/mocks/proxy/UUPSLegacy.sol | 2 +- contracts/mocks/proxy/UUPSUpgradeableMock.sol | 2 +- contracts/mocks/token/ERC1155ReceiverMock.sol | 2 +- contracts/mocks/token/ERC20DecimalsMock.sol | 2 +- contracts/mocks/token/ERC20ExcessDecimalsMock.sol | 2 +- contracts/mocks/token/ERC20FlashMintMock.sol | 2 +- contracts/mocks/token/ERC20ForceApproveMock.sol | 2 +- contracts/mocks/token/ERC20Mock.sol | 2 +- contracts/mocks/token/ERC20MulticallMock.sol | 2 +- contracts/mocks/token/ERC20NoReturnMock.sol | 2 +- contracts/mocks/token/ERC20PermitNoRevertMock.sol | 2 +- contracts/mocks/token/ERC20Reentrant.sol | 2 +- contracts/mocks/token/ERC20ReturnFalseMock.sol | 2 +- contracts/mocks/token/ERC20VotesLegacyMock.sol | 2 +- contracts/mocks/token/ERC4626Mock.sol | 2 +- contracts/mocks/token/ERC4626OffsetMock.sol | 2 +- contracts/mocks/token/ERC4646FeesMock.sol | 2 +- .../mocks/token/ERC721ConsecutiveEnumerableMock.sol | 2 +- contracts/mocks/token/ERC721ConsecutiveMock.sol | 2 +- contracts/mocks/token/ERC721ReceiverMock.sol | 2 +- contracts/mocks/token/ERC721URIStorageMock.sol | 2 +- contracts/mocks/token/VotesTimestamp.sol | 2 +- contracts/proxy/Clones.sol | 2 +- contracts/proxy/ERC1967/ERC1967Proxy.sol | 2 +- contracts/proxy/ERC1967/ERC1967Upgrade.sol | 2 +- contracts/proxy/Proxy.sol | 2 +- contracts/proxy/beacon/BeaconProxy.sol | 2 +- contracts/proxy/beacon/IBeacon.sol | 2 +- contracts/proxy/beacon/UpgradeableBeacon.sol | 2 +- contracts/proxy/transparent/ProxyAdmin.sol | 2 +- .../proxy/transparent/TransparentUpgradeableProxy.sol | 2 +- contracts/proxy/utils/Initializable.sol | 2 +- contracts/proxy/utils/UUPSUpgradeable.sol | 2 +- contracts/security/Pausable.sol | 2 +- contracts/security/ReentrancyGuard.sol | 2 +- contracts/token/ERC1155/ERC1155.sol | 2 +- contracts/token/ERC1155/IERC1155.sol | 2 +- contracts/token/ERC1155/IERC1155Receiver.sol | 2 +- contracts/token/ERC1155/extensions/ERC1155Burnable.sol | 2 +- contracts/token/ERC1155/extensions/ERC1155Pausable.sol | 2 +- contracts/token/ERC1155/extensions/ERC1155Supply.sol | 2 +- .../token/ERC1155/extensions/ERC1155URIStorage.sol | 2 +- .../token/ERC1155/extensions/IERC1155MetadataURI.sol | 2 +- contracts/token/ERC1155/utils/ERC1155Holder.sol | 2 +- contracts/token/ERC1155/utils/ERC1155Receiver.sol | 2 +- contracts/token/ERC20/ERC20.sol | 2 +- contracts/token/ERC20/IERC20.sol | 2 +- contracts/token/ERC20/extensions/ERC20Burnable.sol | 2 +- contracts/token/ERC20/extensions/ERC20Capped.sol | 2 +- contracts/token/ERC20/extensions/ERC20FlashMint.sol | 2 +- contracts/token/ERC20/extensions/ERC20Pausable.sol | 2 +- contracts/token/ERC20/extensions/ERC20Permit.sol | 2 +- contracts/token/ERC20/extensions/ERC20Votes.sol | 2 +- contracts/token/ERC20/extensions/ERC20Wrapper.sol | 2 +- contracts/token/ERC20/extensions/ERC4626.sol | 2 +- contracts/token/ERC20/extensions/IERC20Metadata.sol | 2 +- contracts/token/ERC20/extensions/IERC20Permit.sol | 2 +- contracts/token/ERC20/utils/SafeERC20.sol | 2 +- contracts/token/ERC721/ERC721.sol | 2 +- contracts/token/ERC721/IERC721.sol | 2 +- contracts/token/ERC721/IERC721Receiver.sol | 2 +- contracts/token/ERC721/extensions/ERC721Burnable.sol | 2 +- .../token/ERC721/extensions/ERC721Consecutive.sol | 2 +- contracts/token/ERC721/extensions/ERC721Enumerable.sol | 2 +- contracts/token/ERC721/extensions/ERC721Pausable.sol | 2 +- contracts/token/ERC721/extensions/ERC721Royalty.sol | 2 +- contracts/token/ERC721/extensions/ERC721URIStorage.sol | 2 +- contracts/token/ERC721/extensions/ERC721Votes.sol | 2 +- contracts/token/ERC721/extensions/ERC721Wrapper.sol | 2 +- .../token/ERC721/extensions/IERC721Enumerable.sol | 2 +- contracts/token/ERC721/extensions/IERC721Metadata.sol | 2 +- contracts/token/ERC721/utils/ERC721Holder.sol | 2 +- contracts/token/common/ERC2981.sol | 2 +- contracts/utils/Address.sol | 2 +- contracts/utils/Arrays.sol | 2 +- contracts/utils/Base64.sol | 2 +- contracts/utils/Context.sol | 2 +- contracts/utils/Counters.sol | 2 +- contracts/utils/Create2.sol | 2 +- contracts/utils/Multicall.sol | 2 +- contracts/utils/Nonces.sol | 2 +- contracts/utils/ShortStrings.sol | 2 +- contracts/utils/StorageSlot.sol | 2 +- contracts/utils/Strings.sol | 2 +- contracts/utils/cryptography/ECDSA.sol | 2 +- contracts/utils/cryptography/EIP712.sol | 2 +- contracts/utils/cryptography/MerkleProof.sol | 2 +- contracts/utils/cryptography/SignatureChecker.sol | 2 +- contracts/utils/introspection/ERC165.sol | 2 +- contracts/utils/introspection/ERC165Checker.sol | 2 +- contracts/utils/introspection/IERC165.sol | 2 +- contracts/utils/math/Math.sol | 2 +- contracts/utils/math/SafeCast.sol | 2 +- contracts/utils/math/SignedMath.sol | 2 +- contracts/utils/structs/BitMaps.sol | 2 +- contracts/utils/structs/Checkpoints.sol | 2 +- contracts/utils/structs/DoubleEndedQueue.sol | 2 +- contracts/utils/structs/EnumerableMap.sol | 2 +- contracts/utils/structs/EnumerableSet.sol | 2 +- contracts/vendor/compound/ICompoundTimelock.sol | 2 +- docs/modules/ROOT/pages/access-control.adoc | 8 ++++---- docs/modules/ROOT/pages/erc1155.adoc | 4 ++-- docs/modules/ROOT/pages/erc20.adoc | 2 +- docs/modules/ROOT/pages/erc721.adoc | 2 +- docs/modules/ROOT/pages/extending-contracts.adoc | 6 +++--- docs/modules/ROOT/pages/governance.adoc | 10 +++++----- docs/modules/ROOT/pages/index.adoc | 2 +- docs/modules/ROOT/pages/utilities.adoc | 2 +- hardhat.config.js | 2 +- scripts/generate/templates/Checkpoints.js | 2 +- scripts/generate/templates/Checkpoints.t.js | 2 +- scripts/generate/templates/EnumerableMap.js | 2 +- scripts/generate/templates/EnumerableSet.js | 2 +- scripts/generate/templates/SafeCast.js | 2 +- scripts/generate/templates/StorageSlot.js | 2 +- scripts/upgradeable/upgradeable.patch | 4 ++-- test/token/ERC20/extensions/ERC4626.t.sol | 2 +- test/token/ERC721/extensions/ERC721Consecutive.t.sol | 2 +- test/utils/ShortStrings.t.sol | 2 +- test/utils/math/Math.t.sol | 2 +- test/utils/structs/Checkpoints.t.sol | 2 +- 234 files changed, 249 insertions(+), 244 deletions(-) create mode 100644 .changeset/short-eels-enjoy.md diff --git a/.changeset/short-eels-enjoy.md b/.changeset/short-eels-enjoy.md new file mode 100644 index 000000000..b7d74c1a2 --- /dev/null +++ b/.changeset/short-eels-enjoy.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Bump minimum compiler version required to 0.8.19 diff --git a/README.md b/README.md index 713ef6a68..aba991710 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ An alternative to npm is to use the GitHub repository (`openzeppelin/openzeppeli Once installed, you can use the contracts in the library by importing them: ```solidity -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; diff --git a/certora/harnesses/AccessControlDefaultAdminRulesHarness.sol b/certora/harnesses/AccessControlDefaultAdminRulesHarness.sol index 29fd3a709..3483c558f 100644 --- a/certora/harnesses/AccessControlDefaultAdminRulesHarness.sol +++ b/certora/harnesses/AccessControlDefaultAdminRulesHarness.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../patched/access/AccessControlDefaultAdminRules.sol"; diff --git a/certora/harnesses/AccessControlHarness.sol b/certora/harnesses/AccessControlHarness.sol index 0cb1e55d4..3363f3590 100644 --- a/certora/harnesses/AccessControlHarness.sol +++ b/certora/harnesses/AccessControlHarness.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../patched/access/AccessControl.sol"; diff --git a/certora/harnesses/DoubleEndedQueueHarness.sol b/certora/harnesses/DoubleEndedQueueHarness.sol index b4f0bc841..35dd4a54a 100644 --- a/certora/harnesses/DoubleEndedQueueHarness.sol +++ b/certora/harnesses/DoubleEndedQueueHarness.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../patched/utils/structs/DoubleEndedQueue.sol"; diff --git a/certora/harnesses/ERC20FlashMintHarness.sol b/certora/harnesses/ERC20FlashMintHarness.sol index 119eb4768..3599d8494 100644 --- a/certora/harnesses/ERC20FlashMintHarness.sol +++ b/certora/harnesses/ERC20FlashMintHarness.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../patched/token/ERC20/ERC20.sol"; import "../patched/token/ERC20/extensions/ERC20Permit.sol"; diff --git a/certora/harnesses/ERC20PermitHarness.sol b/certora/harnesses/ERC20PermitHarness.sol index dd0aacae2..dccc9cb91 100644 --- a/certora/harnesses/ERC20PermitHarness.sol +++ b/certora/harnesses/ERC20PermitHarness.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../patched/token/ERC20/extensions/ERC20Permit.sol"; diff --git a/certora/harnesses/ERC20WrapperHarness.sol b/certora/harnesses/ERC20WrapperHarness.sol index 50a96cc17..191c46cc4 100644 --- a/certora/harnesses/ERC20WrapperHarness.sol +++ b/certora/harnesses/ERC20WrapperHarness.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../patched/token/ERC20/extensions/ERC20Wrapper.sol"; diff --git a/certora/harnesses/ERC3156FlashBorrowerHarness.sol b/certora/harnesses/ERC3156FlashBorrowerHarness.sol index 0ad29a16e..d07d12e47 100644 --- a/certora/harnesses/ERC3156FlashBorrowerHarness.sol +++ b/certora/harnesses/ERC3156FlashBorrowerHarness.sol @@ -2,7 +2,7 @@ import "../patched/interfaces/IERC3156FlashBorrower.sol"; -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; contract ERC3156FlashBorrowerHarness is IERC3156FlashBorrower { bytes32 somethingToReturn; diff --git a/certora/harnesses/ERC721Harness.sol b/certora/harnesses/ERC721Harness.sol index 3307369a8..f22a6811d 100644 --- a/certora/harnesses/ERC721Harness.sol +++ b/certora/harnesses/ERC721Harness.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../patched/token/ERC721/ERC721.sol"; diff --git a/certora/harnesses/ERC721ReceiverHarness.sol b/certora/harnesses/ERC721ReceiverHarness.sol index 7e5739ee3..a0e9e247a 100644 --- a/certora/harnesses/ERC721ReceiverHarness.sol +++ b/certora/harnesses/ERC721ReceiverHarness.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../patched/interfaces/IERC721Receiver.sol"; diff --git a/certora/harnesses/EnumerableMapHarness.sol b/certora/harnesses/EnumerableMapHarness.sol index 3bcf1b50a..40b752487 100644 --- a/certora/harnesses/EnumerableMapHarness.sol +++ b/certora/harnesses/EnumerableMapHarness.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../patched/utils/structs/EnumerableMap.sol"; diff --git a/certora/harnesses/EnumerableSetHarness.sol b/certora/harnesses/EnumerableSetHarness.sol index 64383e6a4..21c7cc7c4 100644 --- a/certora/harnesses/EnumerableSetHarness.sol +++ b/certora/harnesses/EnumerableSetHarness.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../patched/utils/structs/EnumerableSet.sol"; diff --git a/certora/harnesses/InitializableHarness.sol b/certora/harnesses/InitializableHarness.sol index 19437e0fc..52b48b82d 100644 --- a/certora/harnesses/InitializableHarness.sol +++ b/certora/harnesses/InitializableHarness.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; +pragma solidity ^0.8.19; import "../patched/proxy/utils/Initializable.sol"; diff --git a/certora/harnesses/Ownable2StepHarness.sol b/certora/harnesses/Ownable2StepHarness.sol index 4d30e5041..1a9ed76e5 100644 --- a/certora/harnesses/Ownable2StepHarness.sol +++ b/certora/harnesses/Ownable2StepHarness.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../patched/access/Ownable2Step.sol"; diff --git a/certora/harnesses/OwnableHarness.sol b/certora/harnesses/OwnableHarness.sol index 93cbb4770..3113fa1bb 100644 --- a/certora/harnesses/OwnableHarness.sol +++ b/certora/harnesses/OwnableHarness.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../patched/access/Ownable.sol"; diff --git a/certora/harnesses/PausableHarness.sol b/certora/harnesses/PausableHarness.sol index 37c5d591d..34d6a82d8 100644 --- a/certora/harnesses/PausableHarness.sol +++ b/certora/harnesses/PausableHarness.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../patched/security/Pausable.sol"; diff --git a/certora/harnesses/TimelockControllerHarness.sol b/certora/harnesses/TimelockControllerHarness.sol index d1ea99065..f75dcfa45 100644 --- a/certora/harnesses/TimelockControllerHarness.sol +++ b/certora/harnesses/TimelockControllerHarness.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../patched/governance/TimelockController.sol"; diff --git a/contracts/access/AccessControl.sol b/contracts/access/AccessControl.sol index 94f9fbd26..82a43933d 100644 --- a/contracts/access/AccessControl.sol +++ b/contracts/access/AccessControl.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (access/AccessControl.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IAccessControl.sol"; import "../utils/Context.sol"; diff --git a/contracts/access/AccessControlDefaultAdminRules.sol b/contracts/access/AccessControlDefaultAdminRules.sol index 9de32002a..47df078c1 100644 --- a/contracts/access/AccessControlDefaultAdminRules.sol +++ b/contracts/access/AccessControlDefaultAdminRules.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (access/AccessControlDefaultAdminRules.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./AccessControl.sol"; import "./IAccessControlDefaultAdminRules.sol"; diff --git a/contracts/access/AccessControlEnumerable.sol b/contracts/access/AccessControlEnumerable.sol index 354e1bed2..1c37a30a9 100644 --- a/contracts/access/AccessControlEnumerable.sol +++ b/contracts/access/AccessControlEnumerable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.5.0) (access/AccessControlEnumerable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IAccessControlEnumerable.sol"; import "./AccessControl.sol"; diff --git a/contracts/access/IAccessControl.sol b/contracts/access/IAccessControl.sol index f773ecc63..34708b78d 100644 --- a/contracts/access/IAccessControl.sol +++ b/contracts/access/IAccessControl.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (access/IAccessControl.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev External interface of AccessControl declared to support ERC165 detection. diff --git a/contracts/access/IAccessControlDefaultAdminRules.sol b/contracts/access/IAccessControlDefaultAdminRules.sol index 434324e50..94cbe871d 100644 --- a/contracts/access/IAccessControlDefaultAdminRules.sol +++ b/contracts/access/IAccessControlDefaultAdminRules.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (access/IAccessControlDefaultAdminRules.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IAccessControl.sol"; diff --git a/contracts/access/IAccessControlEnumerable.sol b/contracts/access/IAccessControlEnumerable.sol index 61aaf57aa..240c61157 100644 --- a/contracts/access/IAccessControlEnumerable.sol +++ b/contracts/access/IAccessControlEnumerable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (access/IAccessControlEnumerable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IAccessControl.sol"; diff --git a/contracts/access/Ownable.sol b/contracts/access/Ownable.sol index 6ef38b89b..6c901b7a1 100644 --- a/contracts/access/Ownable.sol +++ b/contracts/access/Ownable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/Context.sol"; diff --git a/contracts/access/Ownable2Step.sol b/contracts/access/Ownable2Step.sol index e6f699881..59ffa3e0e 100644 --- a/contracts/access/Ownable2Step.sol +++ b/contracts/access/Ownable2Step.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable2Step.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./Ownable.sol"; diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol index bb70d19fb..5b7e1b150 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (finance/VestingWallet.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC20/utils/SafeERC20.sol"; import "../utils/Address.sol"; diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index 390d2b94a..4ff8c3492 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (governance/Governor.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC721/IERC721Receiver.sol"; import "../token/ERC1155/IERC1155Receiver.sol"; diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 492d95b9a..245c5d97a 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (governance/IGovernor.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../interfaces/IERC165.sol"; import "../interfaces/IERC6372.sol"; diff --git a/contracts/governance/TimelockController.sol b/contracts/governance/TimelockController.sol index a80a674a1..5f09161df 100644 --- a/contracts/governance/TimelockController.sol +++ b/contracts/governance/TimelockController.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (governance/TimelockController.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../access/AccessControl.sol"; import "../token/ERC721/IERC721Receiver.sol"; diff --git a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol index 1e18aeb6c..425ecad09 100644 --- a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (governance/compatibility/GovernorCompatibilityBravo.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../utils/math/SafeCast.sol"; import "../extensions/IGovernorTimelock.sol"; diff --git a/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol b/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol index e64a66a66..d69bbf619 100644 --- a/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (governance/compatibility/IGovernorCompatibilityBravo.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IGovernor.sol"; diff --git a/contracts/governance/extensions/GovernorCountingSimple.sol b/contracts/governance/extensions/GovernorCountingSimple.sol index b9517445a..d5c99e593 100644 --- a/contracts/governance/extensions/GovernorCountingSimple.sol +++ b/contracts/governance/extensions/GovernorCountingSimple.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorCountingSimple.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../Governor.sol"; diff --git a/contracts/governance/extensions/GovernorPreventLateQuorum.sol b/contracts/governance/extensions/GovernorPreventLateQuorum.sol index 752a92cf1..abb81128e 100644 --- a/contracts/governance/extensions/GovernorPreventLateQuorum.sol +++ b/contracts/governance/extensions/GovernorPreventLateQuorum.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorPreventLateQuorum.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../Governor.sol"; import "../../utils/math/Math.sol"; diff --git a/contracts/governance/extensions/GovernorSettings.sol b/contracts/governance/extensions/GovernorSettings.sol index ec6a98300..570c88c54 100644 --- a/contracts/governance/extensions/GovernorSettings.sol +++ b/contracts/governance/extensions/GovernorSettings.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorSettings.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../Governor.sol"; diff --git a/contracts/governance/extensions/GovernorTimelockCompound.sol b/contracts/governance/extensions/GovernorTimelockCompound.sol index a706da6a8..1efd3efff 100644 --- a/contracts/governance/extensions/GovernorTimelockCompound.sol +++ b/contracts/governance/extensions/GovernorTimelockCompound.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorTimelockCompound.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IGovernorTimelock.sol"; import "../Governor.sol"; diff --git a/contracts/governance/extensions/GovernorTimelockControl.sol b/contracts/governance/extensions/GovernorTimelockControl.sol index 7cb60bab1..3fbce763a 100644 --- a/contracts/governance/extensions/GovernorTimelockControl.sol +++ b/contracts/governance/extensions/GovernorTimelockControl.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorTimelockControl.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IGovernorTimelock.sol"; import "../Governor.sol"; diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol index c2e65cba9..5d8318f46 100644 --- a/contracts/governance/extensions/GovernorVotes.sol +++ b/contracts/governance/extensions/GovernorVotes.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorVotes.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../Governor.sol"; import "../../interfaces/IERC5805.sol"; diff --git a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol index d42bb2f47..6c10240ce 100644 --- a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol +++ b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorVotesQuorumFraction.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./GovernorVotes.sol"; import "../../utils/math/SafeCast.sol"; diff --git a/contracts/governance/extensions/IGovernorTimelock.sol b/contracts/governance/extensions/IGovernorTimelock.sol index 40402f614..570092bc5 100644 --- a/contracts/governance/extensions/IGovernorTimelock.sol +++ b/contracts/governance/extensions/IGovernorTimelock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (governance/extensions/IGovernorTimelock.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IGovernor.sol"; diff --git a/contracts/governance/utils/IVotes.sol b/contracts/governance/utils/IVotes.sol index 647f79655..a1e4fe63a 100644 --- a/contracts/governance/utils/IVotes.sol +++ b/contracts/governance/utils/IVotes.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (governance/utils/IVotes.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Common interface for {ERC20Votes}, {ERC721Votes}, and other {Votes}-enabled contracts. diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index cc1143fe2..5c855b5e4 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (governance/utils/Votes.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../interfaces/IERC5805.sol"; import "../../utils/Context.sol"; diff --git a/contracts/interfaces/IERC1155.sol b/contracts/interfaces/IERC1155.sol index f89113212..8f7527f91 100644 --- a/contracts/interfaces/IERC1155.sol +++ b/contracts/interfaces/IERC1155.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC1155.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC1155/IERC1155.sol"; diff --git a/contracts/interfaces/IERC1155MetadataURI.sol b/contracts/interfaces/IERC1155MetadataURI.sol index 2aa885feb..61b36c2e8 100644 --- a/contracts/interfaces/IERC1155MetadataURI.sol +++ b/contracts/interfaces/IERC1155MetadataURI.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC1155MetadataURI.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC1155/extensions/IERC1155MetadataURI.sol"; diff --git a/contracts/interfaces/IERC1155Receiver.sol b/contracts/interfaces/IERC1155Receiver.sol index a6d4ead16..b5cd186b7 100644 --- a/contracts/interfaces/IERC1155Receiver.sol +++ b/contracts/interfaces/IERC1155Receiver.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC1155Receiver.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC1155/IERC1155Receiver.sol"; diff --git a/contracts/interfaces/IERC1271.sol b/contracts/interfaces/IERC1271.sol index 5ec44c721..e7fca3079 100644 --- a/contracts/interfaces/IERC1271.sol +++ b/contracts/interfaces/IERC1271.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC1271.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface of the ERC1271 standard signature validation method for diff --git a/contracts/interfaces/IERC1363.sol b/contracts/interfaces/IERC1363.sol index 817f3dafc..95c5b8992 100644 --- a/contracts/interfaces/IERC1363.sol +++ b/contracts/interfaces/IERC1363.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC1363.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IERC20.sol"; import "./IERC165.sol"; diff --git a/contracts/interfaces/IERC1363Receiver.sol b/contracts/interfaces/IERC1363Receiver.sol index 382d7f111..70ed857e9 100644 --- a/contracts/interfaces/IERC1363Receiver.sol +++ b/contracts/interfaces/IERC1363Receiver.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC1363Receiver.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface for any contract that wants to support {IERC1363-transferAndCall} diff --git a/contracts/interfaces/IERC1363Spender.sol b/contracts/interfaces/IERC1363Spender.sol index 09a7bd278..0c89e58eb 100644 --- a/contracts/interfaces/IERC1363Spender.sol +++ b/contracts/interfaces/IERC1363Spender.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC1363Spender.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface for any contract that wants to support {IERC1363-approveAndCall} diff --git a/contracts/interfaces/IERC165.sol b/contracts/interfaces/IERC165.sol index b97c4daa2..f4d90264c 100644 --- a/contracts/interfaces/IERC165.sol +++ b/contracts/interfaces/IERC165.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC165.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/introspection/IERC165.sol"; diff --git a/contracts/interfaces/IERC1820Implementer.sol b/contracts/interfaces/IERC1820Implementer.sol index c4d0b3028..6f0ec661d 100644 --- a/contracts/interfaces/IERC1820Implementer.sol +++ b/contracts/interfaces/IERC1820Implementer.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC1820Implementer.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface for an ERC1820 implementer, as defined in the diff --git a/contracts/interfaces/IERC1820Registry.sol b/contracts/interfaces/IERC1820Registry.sol index a146bc2a6..d9dd49355 100644 --- a/contracts/interfaces/IERC1820Registry.sol +++ b/contracts/interfaces/IERC1820Registry.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (utils/introspection/IERC1820Registry.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface of the global ERC1820 Registry, as defined in the diff --git a/contracts/interfaces/IERC1967.sol b/contracts/interfaces/IERC1967.sol index 6fb112a2e..190d86b2c 100644 --- a/contracts/interfaces/IERC1967.sol +++ b/contracts/interfaces/IERC1967.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC1967.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev ERC-1967: Proxy Storage Slots. This interface contains the events defined in the ERC. diff --git a/contracts/interfaces/IERC20.sol b/contracts/interfaces/IERC20.sol index a819316d1..dd559e980 100644 --- a/contracts/interfaces/IERC20.sol +++ b/contracts/interfaces/IERC20.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC20.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC20/IERC20.sol"; diff --git a/contracts/interfaces/IERC20Metadata.sol b/contracts/interfaces/IERC20Metadata.sol index aa5c63910..061fd4a2f 100644 --- a/contracts/interfaces/IERC20Metadata.sol +++ b/contracts/interfaces/IERC20Metadata.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC20Metadata.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/contracts/interfaces/IERC2309.sol b/contracts/interfaces/IERC2309.sol index b3fec44e2..9d8886994 100644 --- a/contracts/interfaces/IERC2309.sol +++ b/contracts/interfaces/IERC2309.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (interfaces/IERC2309.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev ERC-2309: ERC-721 Consecutive Transfer Extension. diff --git a/contracts/interfaces/IERC2612.sol b/contracts/interfaces/IERC2612.sol index cd5fca4cc..582eea81d 100644 --- a/contracts/interfaces/IERC2612.sol +++ b/contracts/interfaces/IERC2612.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC2612.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC20/extensions/IERC20Permit.sol"; diff --git a/contracts/interfaces/IERC2981.sol b/contracts/interfaces/IERC2981.sol index 465b872ee..1b1476782 100644 --- a/contracts/interfaces/IERC2981.sol +++ b/contracts/interfaces/IERC2981.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC2981.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/introspection/IERC165.sol"; diff --git a/contracts/interfaces/IERC3156.sol b/contracts/interfaces/IERC3156.sol index 12381906d..280b25be1 100644 --- a/contracts/interfaces/IERC3156.sol +++ b/contracts/interfaces/IERC3156.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC3156.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IERC3156FlashBorrower.sol"; import "./IERC3156FlashLender.sol"; diff --git a/contracts/interfaces/IERC3156FlashBorrower.sol b/contracts/interfaces/IERC3156FlashBorrower.sol index 84bd72150..3f216194b 100644 --- a/contracts/interfaces/IERC3156FlashBorrower.sol +++ b/contracts/interfaces/IERC3156FlashBorrower.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC3156FlashBorrower.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface of the ERC3156 FlashBorrower, as defined in diff --git a/contracts/interfaces/IERC3156FlashLender.sol b/contracts/interfaces/IERC3156FlashLender.sol index 31012830f..89a486fdb 100644 --- a/contracts/interfaces/IERC3156FlashLender.sol +++ b/contracts/interfaces/IERC3156FlashLender.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC3156FlashLender.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IERC3156FlashBorrower.sol"; diff --git a/contracts/interfaces/IERC4626.sol b/contracts/interfaces/IERC4626.sol index 77dd96a05..946de9155 100644 --- a/contracts/interfaces/IERC4626.sol +++ b/contracts/interfaces/IERC4626.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC4626.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC20/IERC20.sol"; import "../token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/contracts/interfaces/IERC4906.sol b/contracts/interfaces/IERC4906.sol index f5a6e00f9..96e709228 100644 --- a/contracts/interfaces/IERC4906.sol +++ b/contracts/interfaces/IERC4906.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC4906.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IERC165.sol"; import "./IERC721.sol"; diff --git a/contracts/interfaces/IERC5267.sol b/contracts/interfaces/IERC5267.sol index 4d3a6b92a..c19b4a6da 100644 --- a/contracts/interfaces/IERC5267.sol +++ b/contracts/interfaces/IERC5267.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC5267.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; interface IERC5267 { /** diff --git a/contracts/interfaces/IERC5313.sol b/contracts/interfaces/IERC5313.sol index e26094c06..1d17080a1 100644 --- a/contracts/interfaces/IERC5313.sol +++ b/contracts/interfaces/IERC5313.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC5313.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface for the Light Contract Ownership Standard. diff --git a/contracts/interfaces/IERC5805.sol b/contracts/interfaces/IERC5805.sol index a012ccb15..3c0a3a6c6 100644 --- a/contracts/interfaces/IERC5805.sol +++ b/contracts/interfaces/IERC5805.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC5805.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../governance/utils/IVotes.sol"; import "./IERC6372.sol"; diff --git a/contracts/interfaces/IERC6372.sol b/contracts/interfaces/IERC6372.sol index 4c5fe039c..c6c78d10a 100644 --- a/contracts/interfaces/IERC6372.sol +++ b/contracts/interfaces/IERC6372.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC6372.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; interface IERC6372 { /** diff --git a/contracts/interfaces/IERC721.sol b/contracts/interfaces/IERC721.sol index 822b311c5..e840f28c7 100644 --- a/contracts/interfaces/IERC721.sol +++ b/contracts/interfaces/IERC721.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC721.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC721/IERC721.sol"; diff --git a/contracts/interfaces/IERC721Enumerable.sol b/contracts/interfaces/IERC721Enumerable.sol index e39a5a01b..fafda5998 100644 --- a/contracts/interfaces/IERC721Enumerable.sol +++ b/contracts/interfaces/IERC721Enumerable.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC721Enumerable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC721/extensions/IERC721Enumerable.sol"; diff --git a/contracts/interfaces/IERC721Metadata.sol b/contracts/interfaces/IERC721Metadata.sol index afe2707c9..f14433e8d 100644 --- a/contracts/interfaces/IERC721Metadata.sol +++ b/contracts/interfaces/IERC721Metadata.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC721Metadata.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC721/extensions/IERC721Metadata.sol"; diff --git a/contracts/interfaces/IERC721Receiver.sol b/contracts/interfaces/IERC721Receiver.sol index c9c153a24..9e62fa734 100644 --- a/contracts/interfaces/IERC721Receiver.sol +++ b/contracts/interfaces/IERC721Receiver.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC721Receiver.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC721/IERC721Receiver.sol"; diff --git a/contracts/interfaces/IERC777.sol b/contracts/interfaces/IERC777.sol index 4d36da52c..f868701ba 100644 --- a/contracts/interfaces/IERC777.sol +++ b/contracts/interfaces/IERC777.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface of the ERC777Token standard as defined in the EIP. diff --git a/contracts/interfaces/IERC777Recipient.sol b/contracts/interfaces/IERC777Recipient.sol index 069904855..be2ab871d 100644 --- a/contracts/interfaces/IERC777Recipient.sol +++ b/contracts/interfaces/IERC777Recipient.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface of the ERC777TokensRecipient standard as defined in the EIP. diff --git a/contracts/interfaces/IERC777Sender.sol b/contracts/interfaces/IERC777Sender.sol index c45477fcc..d4172b107 100644 --- a/contracts/interfaces/IERC777Sender.sol +++ b/contracts/interfaces/IERC777Sender.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface of the ERC777TokensSender standard as defined in the EIP. diff --git a/contracts/interfaces/draft-IERC1822.sol b/contracts/interfaces/draft-IERC1822.sol index 3b73d744c..f452b5e34 100644 --- a/contracts/interfaces/draft-IERC1822.sol +++ b/contracts/interfaces/draft-IERC1822.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.5.0) (interfaces/draft-IERC1822.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev ERC1822: Universal Upgradeable Proxy Standard (UUPS) documents a method for upgradeability through a simplified diff --git a/contracts/metatx/ERC2771Context.sol b/contracts/metatx/ERC2771Context.sol index 8cc14b9f4..e02ffcc19 100644 --- a/contracts/metatx/ERC2771Context.sol +++ b/contracts/metatx/ERC2771Context.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (metatx/ERC2771Context.sol) -pragma solidity ^0.8.9; +pragma solidity ^0.8.19; import "../utils/Context.sol"; diff --git a/contracts/metatx/MinimalForwarder.sol b/contracts/metatx/MinimalForwarder.sol index de2646269..8ea7a76e8 100644 --- a/contracts/metatx/MinimalForwarder.sol +++ b/contracts/metatx/MinimalForwarder.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (metatx/MinimalForwarder.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/cryptography/ECDSA.sol"; import "../utils/cryptography/EIP712.sol"; diff --git a/contracts/mocks/ArraysMock.sol b/contracts/mocks/ArraysMock.sol index 2ea17a09f..b341edc62 100644 --- a/contracts/mocks/ArraysMock.sol +++ b/contracts/mocks/ArraysMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/Arrays.sol"; diff --git a/contracts/mocks/CallReceiverMock.sol b/contracts/mocks/CallReceiverMock.sol index 492adbe92..b87f4e59e 100644 --- a/contracts/mocks/CallReceiverMock.sol +++ b/contracts/mocks/CallReceiverMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; contract CallReceiverMock { event MockFunctionCalled(); diff --git a/contracts/mocks/ContextMock.sol b/contracts/mocks/ContextMock.sol index 7759f3506..2e7751d0e 100644 --- a/contracts/mocks/ContextMock.sol +++ b/contracts/mocks/ContextMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/Context.sol"; diff --git a/contracts/mocks/DummyImplementation.sol b/contracts/mocks/DummyImplementation.sol index ddcca6604..85503c36e 100644 --- a/contracts/mocks/DummyImplementation.sol +++ b/contracts/mocks/DummyImplementation.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; abstract contract Impl { function version() public pure virtual returns (string memory); diff --git a/contracts/mocks/EIP712Verifier.sol b/contracts/mocks/EIP712Verifier.sol index dcef9efbb..ea28162ba 100644 --- a/contracts/mocks/EIP712Verifier.sol +++ b/contracts/mocks/EIP712Verifier.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/cryptography/ECDSA.sol"; import "../utils/cryptography/EIP712.sol"; diff --git a/contracts/mocks/ERC1271WalletMock.sol b/contracts/mocks/ERC1271WalletMock.sol index 4e6d3ce57..a28fd0fb9 100644 --- a/contracts/mocks/ERC1271WalletMock.sol +++ b/contracts/mocks/ERC1271WalletMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../access/Ownable.sol"; import "../interfaces/IERC1271.sol"; diff --git a/contracts/mocks/ERC165/ERC165InterfacesSupported.sol b/contracts/mocks/ERC165/ERC165InterfacesSupported.sol index 7a5e5bc67..d21d7c2d0 100644 --- a/contracts/mocks/ERC165/ERC165InterfacesSupported.sol +++ b/contracts/mocks/ERC165/ERC165InterfacesSupported.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../utils/introspection/IERC165.sol"; diff --git a/contracts/mocks/ERC165/ERC165MaliciousData.sol b/contracts/mocks/ERC165/ERC165MaliciousData.sol index 2446f3df2..2f3b57d66 100644 --- a/contracts/mocks/ERC165/ERC165MaliciousData.sol +++ b/contracts/mocks/ERC165/ERC165MaliciousData.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; contract ERC165MaliciousData { function supportsInterface(bytes4) public pure returns (bool) { diff --git a/contracts/mocks/ERC165/ERC165MissingData.sol b/contracts/mocks/ERC165/ERC165MissingData.sol index 59cd51ae6..c58ca8fd0 100644 --- a/contracts/mocks/ERC165/ERC165MissingData.sol +++ b/contracts/mocks/ERC165/ERC165MissingData.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; contract ERC165MissingData { function supportsInterface(bytes4 interfaceId) public view {} // missing return diff --git a/contracts/mocks/ERC165/ERC165NotSupported.sol b/contracts/mocks/ERC165/ERC165NotSupported.sol index 486c7f0a4..9cd21bc2d 100644 --- a/contracts/mocks/ERC165/ERC165NotSupported.sol +++ b/contracts/mocks/ERC165/ERC165NotSupported.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; contract ERC165NotSupported {} diff --git a/contracts/mocks/ERC165/ERC165ReturnBomb.sol b/contracts/mocks/ERC165/ERC165ReturnBomb.sol index e53235d2c..d2a64151f 100644 --- a/contracts/mocks/ERC165/ERC165ReturnBomb.sol +++ b/contracts/mocks/ERC165/ERC165ReturnBomb.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../utils/introspection/IERC165.sol"; diff --git a/contracts/mocks/ERC2771ContextMock.sol b/contracts/mocks/ERC2771ContextMock.sol index 387df785e..8c2cb43fd 100644 --- a/contracts/mocks/ERC2771ContextMock.sol +++ b/contracts/mocks/ERC2771ContextMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity ^0.8.19; import "./ContextMock.sol"; import "../metatx/ERC2771Context.sol"; diff --git a/contracts/mocks/ERC3156FlashBorrowerMock.sol b/contracts/mocks/ERC3156FlashBorrowerMock.sol index 6a4410fcb..8d24af6c1 100644 --- a/contracts/mocks/ERC3156FlashBorrowerMock.sol +++ b/contracts/mocks/ERC3156FlashBorrowerMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC20/IERC20.sol"; import "../interfaces/IERC3156.sol"; diff --git a/contracts/mocks/EtherReceiverMock.sol b/contracts/mocks/EtherReceiverMock.sol index a11e646fb..d06d35b21 100644 --- a/contracts/mocks/EtherReceiverMock.sol +++ b/contracts/mocks/EtherReceiverMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; contract EtherReceiverMock { bool private _acceptEther; diff --git a/contracts/mocks/InitializableMock.sol b/contracts/mocks/InitializableMock.sol index 34040b6e5..513aac052 100644 --- a/contracts/mocks/InitializableMock.sol +++ b/contracts/mocks/InitializableMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../proxy/utils/Initializable.sol"; diff --git a/contracts/mocks/MulticallTest.sol b/contracts/mocks/MulticallTest.sol index fcbec6ad8..090f73cb4 100644 --- a/contracts/mocks/MulticallTest.sol +++ b/contracts/mocks/MulticallTest.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./token/ERC20MulticallMock.sol"; diff --git a/contracts/mocks/MultipleInheritanceInitializableMocks.sol b/contracts/mocks/MultipleInheritanceInitializableMocks.sol index e79cd92c8..cb62942ce 100644 --- a/contracts/mocks/MultipleInheritanceInitializableMocks.sol +++ b/contracts/mocks/MultipleInheritanceInitializableMocks.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../proxy/utils/Initializable.sol"; diff --git a/contracts/mocks/PausableMock.sol b/contracts/mocks/PausableMock.sol index 98bcfd593..85d45a3af 100644 --- a/contracts/mocks/PausableMock.sol +++ b/contracts/mocks/PausableMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../security/Pausable.sol"; diff --git a/contracts/mocks/ReentrancyAttack.sol b/contracts/mocks/ReentrancyAttack.sol index 4de181205..2da8b1f1a 100644 --- a/contracts/mocks/ReentrancyAttack.sol +++ b/contracts/mocks/ReentrancyAttack.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/Context.sol"; diff --git a/contracts/mocks/ReentrancyMock.sol b/contracts/mocks/ReentrancyMock.sol index 161e1d3d8..104d4f42a 100644 --- a/contracts/mocks/ReentrancyMock.sol +++ b/contracts/mocks/ReentrancyMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../security/ReentrancyGuard.sol"; import "./ReentrancyAttack.sol"; diff --git a/contracts/mocks/RegressionImplementation.sol b/contracts/mocks/RegressionImplementation.sol index be6b501c1..f258bbc0e 100644 --- a/contracts/mocks/RegressionImplementation.sol +++ b/contracts/mocks/RegressionImplementation.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../proxy/utils/Initializable.sol"; diff --git a/contracts/mocks/SingleInheritanceInitializableMocks.sol b/contracts/mocks/SingleInheritanceInitializableMocks.sol index 6c82dd20c..2b5fad4eb 100644 --- a/contracts/mocks/SingleInheritanceInitializableMocks.sol +++ b/contracts/mocks/SingleInheritanceInitializableMocks.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../proxy/utils/Initializable.sol"; diff --git a/contracts/mocks/StorageSlotMock.sol b/contracts/mocks/StorageSlotMock.sol index 1da577c19..62dd23d6a 100644 --- a/contracts/mocks/StorageSlotMock.sol +++ b/contracts/mocks/StorageSlotMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/StorageSlot.sol"; diff --git a/contracts/mocks/TimelockReentrant.sol b/contracts/mocks/TimelockReentrant.sol index a9344f50d..803a2b037 100644 --- a/contracts/mocks/TimelockReentrant.sol +++ b/contracts/mocks/TimelockReentrant.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/Address.sol"; diff --git a/contracts/mocks/VotesMock.sol b/contracts/mocks/VotesMock.sol index c122f71e5..697d33448 100644 --- a/contracts/mocks/VotesMock.sol +++ b/contracts/mocks/VotesMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../governance/utils/Votes.sol"; diff --git a/contracts/mocks/compound/CompTimelock.sol b/contracts/mocks/compound/CompTimelock.sol index 49ffa4b77..9dc586ddb 100644 --- a/contracts/mocks/compound/CompTimelock.sol +++ b/contracts/mocks/compound/CompTimelock.sol @@ -24,7 +24,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; contract CompTimelock { event NewAdmin(address indexed newAdmin); diff --git a/contracts/mocks/docs/ERC4626Fees.sol b/contracts/mocks/docs/ERC4626Fees.sol index 8ff162953..703e4245a 100644 --- a/contracts/mocks/docs/ERC4626Fees.sol +++ b/contracts/mocks/docs/ERC4626Fees.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/extensions/ERC4626.sol"; diff --git a/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol b/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol index 4cdc0b888..6e14a357d 100644 --- a/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol +++ b/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../governance/compatibility/GovernorCompatibilityBravo.sol"; import "../../governance/extensions/GovernorTimelockCompound.sol"; diff --git a/contracts/mocks/governance/GovernorMock.sol b/contracts/mocks/governance/GovernorMock.sol index 9284a24fe..9104c3f32 100644 --- a/contracts/mocks/governance/GovernorMock.sol +++ b/contracts/mocks/governance/GovernorMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../governance/extensions/GovernorSettings.sol"; import "../../governance/extensions/GovernorCountingSimple.sol"; diff --git a/contracts/mocks/governance/GovernorPreventLateQuorumMock.sol b/contracts/mocks/governance/GovernorPreventLateQuorumMock.sol index 79d894896..4c1b408e6 100644 --- a/contracts/mocks/governance/GovernorPreventLateQuorumMock.sol +++ b/contracts/mocks/governance/GovernorPreventLateQuorumMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../governance/extensions/GovernorPreventLateQuorum.sol"; import "../../governance/extensions/GovernorSettings.sol"; diff --git a/contracts/mocks/governance/GovernorTimelockCompoundMock.sol b/contracts/mocks/governance/GovernorTimelockCompoundMock.sol index b37462819..134d66133 100644 --- a/contracts/mocks/governance/GovernorTimelockCompoundMock.sol +++ b/contracts/mocks/governance/GovernorTimelockCompoundMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../governance/extensions/GovernorTimelockCompound.sol"; import "../../governance/extensions/GovernorSettings.sol"; diff --git a/contracts/mocks/governance/GovernorTimelockControlMock.sol b/contracts/mocks/governance/GovernorTimelockControlMock.sol index 06309145a..28376835a 100644 --- a/contracts/mocks/governance/GovernorTimelockControlMock.sol +++ b/contracts/mocks/governance/GovernorTimelockControlMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../governance/extensions/GovernorTimelockControl.sol"; import "../../governance/extensions/GovernorSettings.sol"; diff --git a/contracts/mocks/governance/GovernorVoteMock.sol b/contracts/mocks/governance/GovernorVoteMock.sol index 9b533bddf..1d0722ecf 100644 --- a/contracts/mocks/governance/GovernorVoteMock.sol +++ b/contracts/mocks/governance/GovernorVoteMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../governance/extensions/GovernorCountingSimple.sol"; import "../../governance/extensions/GovernorVotes.sol"; diff --git a/contracts/mocks/governance/GovernorWithParamsMock.sol b/contracts/mocks/governance/GovernorWithParamsMock.sol index 361c2873e..f8de83710 100644 --- a/contracts/mocks/governance/GovernorWithParamsMock.sol +++ b/contracts/mocks/governance/GovernorWithParamsMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../governance/extensions/GovernorCountingSimple.sol"; import "../../governance/extensions/GovernorVotes.sol"; diff --git a/contracts/mocks/proxy/BadBeacon.sol b/contracts/mocks/proxy/BadBeacon.sol index bedcfed84..2c8ffe291 100644 --- a/contracts/mocks/proxy/BadBeacon.sol +++ b/contracts/mocks/proxy/BadBeacon.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; contract BadBeaconNoImpl {} diff --git a/contracts/mocks/proxy/ClashingImplementation.sol b/contracts/mocks/proxy/ClashingImplementation.sol index 4e33160ff..957bc34be 100644 --- a/contracts/mocks/proxy/ClashingImplementation.sol +++ b/contracts/mocks/proxy/ClashingImplementation.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Implementation contract with a payable changeAdmin(address) function made to clash with diff --git a/contracts/mocks/proxy/UUPSLegacy.sol b/contracts/mocks/proxy/UUPSLegacy.sol index ee0ed0bcc..ed243519e 100644 --- a/contracts/mocks/proxy/UUPSLegacy.sol +++ b/contracts/mocks/proxy/UUPSLegacy.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.1; +pragma solidity ^0.8.19; import "./UUPSUpgradeableMock.sol"; diff --git a/contracts/mocks/proxy/UUPSUpgradeableMock.sol b/contracts/mocks/proxy/UUPSUpgradeableMock.sol index f02271c49..83e6ef465 100644 --- a/contracts/mocks/proxy/UUPSUpgradeableMock.sol +++ b/contracts/mocks/proxy/UUPSUpgradeableMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../proxy/utils/UUPSUpgradeable.sol"; import "../../utils/Counters.sol"; diff --git a/contracts/mocks/token/ERC1155ReceiverMock.sol b/contracts/mocks/token/ERC1155ReceiverMock.sol index 317d72425..9f75f002b 100644 --- a/contracts/mocks/token/ERC1155ReceiverMock.sol +++ b/contracts/mocks/token/ERC1155ReceiverMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC1155/IERC1155Receiver.sol"; import "../../utils/introspection/ERC165.sol"; diff --git a/contracts/mocks/token/ERC20DecimalsMock.sol b/contracts/mocks/token/ERC20DecimalsMock.sol index 32f287470..5699d31d2 100644 --- a/contracts/mocks/token/ERC20DecimalsMock.sol +++ b/contracts/mocks/token/ERC20DecimalsMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/ERC20.sol"; diff --git a/contracts/mocks/token/ERC20ExcessDecimalsMock.sol b/contracts/mocks/token/ERC20ExcessDecimalsMock.sol index 0fb35a607..bed8c5fcf 100644 --- a/contracts/mocks/token/ERC20ExcessDecimalsMock.sol +++ b/contracts/mocks/token/ERC20ExcessDecimalsMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; contract ERC20ExcessDecimalsMock { function decimals() public pure returns (uint256) { diff --git a/contracts/mocks/token/ERC20FlashMintMock.sol b/contracts/mocks/token/ERC20FlashMintMock.sol index b4de7b771..05c4db57d 100644 --- a/contracts/mocks/token/ERC20FlashMintMock.sol +++ b/contracts/mocks/token/ERC20FlashMintMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/extensions/ERC20FlashMint.sol"; diff --git a/contracts/mocks/token/ERC20ForceApproveMock.sol b/contracts/mocks/token/ERC20ForceApproveMock.sol index 955224bcf..42b417f41 100644 --- a/contracts/mocks/token/ERC20ForceApproveMock.sol +++ b/contracts/mocks/token/ERC20ForceApproveMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/ERC20.sol"; diff --git a/contracts/mocks/token/ERC20Mock.sol b/contracts/mocks/token/ERC20Mock.sol index fc6841d0e..5fb134f60 100644 --- a/contracts/mocks/token/ERC20Mock.sol +++ b/contracts/mocks/token/ERC20Mock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/ERC20.sol"; diff --git a/contracts/mocks/token/ERC20MulticallMock.sol b/contracts/mocks/token/ERC20MulticallMock.sol index 145e97a62..b8259a75a 100644 --- a/contracts/mocks/token/ERC20MulticallMock.sol +++ b/contracts/mocks/token/ERC20MulticallMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/ERC20.sol"; import "../../utils/Multicall.sol"; diff --git a/contracts/mocks/token/ERC20NoReturnMock.sol b/contracts/mocks/token/ERC20NoReturnMock.sol index 348c0d6bb..7d597ad89 100644 --- a/contracts/mocks/token/ERC20NoReturnMock.sol +++ b/contracts/mocks/token/ERC20NoReturnMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/ERC20.sol"; diff --git a/contracts/mocks/token/ERC20PermitNoRevertMock.sol b/contracts/mocks/token/ERC20PermitNoRevertMock.sol index f2562d3cc..510176cf5 100644 --- a/contracts/mocks/token/ERC20PermitNoRevertMock.sol +++ b/contracts/mocks/token/ERC20PermitNoRevertMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/ERC20.sol"; import "../../token/ERC20/extensions/ERC20Permit.sol"; diff --git a/contracts/mocks/token/ERC20Reentrant.sol b/contracts/mocks/token/ERC20Reentrant.sol index bd67d940b..ee803b9e1 100644 --- a/contracts/mocks/token/ERC20Reentrant.sol +++ b/contracts/mocks/token/ERC20Reentrant.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/ERC20.sol"; import "../../token/ERC20/extensions/ERC4626.sol"; diff --git a/contracts/mocks/token/ERC20ReturnFalseMock.sol b/contracts/mocks/token/ERC20ReturnFalseMock.sol index c4dc6921f..763a120e9 100644 --- a/contracts/mocks/token/ERC20ReturnFalseMock.sol +++ b/contracts/mocks/token/ERC20ReturnFalseMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/ERC20.sol"; diff --git a/contracts/mocks/token/ERC20VotesLegacyMock.sol b/contracts/mocks/token/ERC20VotesLegacyMock.sol index 7ffa01022..cf8afa58a 100644 --- a/contracts/mocks/token/ERC20VotesLegacyMock.sol +++ b/contracts/mocks/token/ERC20VotesLegacyMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/extensions/ERC20Permit.sol"; import "../../utils/math/Math.sol"; diff --git a/contracts/mocks/token/ERC4626Mock.sol b/contracts/mocks/token/ERC4626Mock.sol index 40779d5c6..3713e0791 100644 --- a/contracts/mocks/token/ERC4626Mock.sol +++ b/contracts/mocks/token/ERC4626Mock.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/extensions/ERC4626.sol"; diff --git a/contracts/mocks/token/ERC4626OffsetMock.sol b/contracts/mocks/token/ERC4626OffsetMock.sol index 6e270ca73..b3c31cdd2 100644 --- a/contracts/mocks/token/ERC4626OffsetMock.sol +++ b/contracts/mocks/token/ERC4626OffsetMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/extensions/ERC4626.sol"; diff --git a/contracts/mocks/token/ERC4646FeesMock.sol b/contracts/mocks/token/ERC4646FeesMock.sol index cd8f41012..4c6c3c662 100644 --- a/contracts/mocks/token/ERC4646FeesMock.sol +++ b/contracts/mocks/token/ERC4646FeesMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../docs/ERC4626Fees.sol"; diff --git a/contracts/mocks/token/ERC721ConsecutiveEnumerableMock.sol b/contracts/mocks/token/ERC721ConsecutiveEnumerableMock.sol index 55c40ac17..a2ee8199a 100644 --- a/contracts/mocks/token/ERC721ConsecutiveEnumerableMock.sol +++ b/contracts/mocks/token/ERC721ConsecutiveEnumerableMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC721/extensions/ERC721Consecutive.sol"; import "../../token/ERC721/extensions/ERC721Enumerable.sol"; diff --git a/contracts/mocks/token/ERC721ConsecutiveMock.sol b/contracts/mocks/token/ERC721ConsecutiveMock.sol index 8bfa0cb9e..9fec7057a 100644 --- a/contracts/mocks/token/ERC721ConsecutiveMock.sol +++ b/contracts/mocks/token/ERC721ConsecutiveMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC721/extensions/ERC721Consecutive.sol"; import "../../token/ERC721/extensions/ERC721Enumerable.sol"; diff --git a/contracts/mocks/token/ERC721ReceiverMock.sol b/contracts/mocks/token/ERC721ReceiverMock.sol index dd25788d4..01526566a 100644 --- a/contracts/mocks/token/ERC721ReceiverMock.sol +++ b/contracts/mocks/token/ERC721ReceiverMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC721/IERC721Receiver.sol"; diff --git a/contracts/mocks/token/ERC721URIStorageMock.sol b/contracts/mocks/token/ERC721URIStorageMock.sol index 455c933c8..1808e47f7 100644 --- a/contracts/mocks/token/ERC721URIStorageMock.sol +++ b/contracts/mocks/token/ERC721URIStorageMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC721/extensions/ERC721URIStorage.sol"; diff --git a/contracts/mocks/token/VotesTimestamp.sol b/contracts/mocks/token/VotesTimestamp.sol index 630e778f1..7e3d8ca6d 100644 --- a/contracts/mocks/token/VotesTimestamp.sol +++ b/contracts/mocks/token/VotesTimestamp.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/extensions/ERC20Votes.sol"; import "../../token/ERC721/extensions/ERC721Votes.sol"; diff --git a/contracts/proxy/Clones.sol b/contracts/proxy/Clones.sol index 583e0223e..7cdab55f6 100644 --- a/contracts/proxy/Clones.sol +++ b/contracts/proxy/Clones.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (proxy/Clones.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev https://eips.ethereum.org/EIPS/eip-1167[EIP 1167] is a standard for diff --git a/contracts/proxy/ERC1967/ERC1967Proxy.sol b/contracts/proxy/ERC1967/ERC1967Proxy.sol index 642d811cc..320e026fa 100644 --- a/contracts/proxy/ERC1967/ERC1967Proxy.sol +++ b/contracts/proxy/ERC1967/ERC1967Proxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (proxy/ERC1967/ERC1967Proxy.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../Proxy.sol"; import "./ERC1967Upgrade.sol"; diff --git a/contracts/proxy/ERC1967/ERC1967Upgrade.sol b/contracts/proxy/ERC1967/ERC1967Upgrade.sol index 95173d6e3..e42a06eb1 100644 --- a/contracts/proxy/ERC1967/ERC1967Upgrade.sol +++ b/contracts/proxy/ERC1967/ERC1967Upgrade.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (proxy/ERC1967/ERC1967Upgrade.sol) -pragma solidity ^0.8.2; +pragma solidity ^0.8.19; import "../beacon/IBeacon.sol"; import "../../interfaces/IERC1967.sol"; diff --git a/contracts/proxy/Proxy.sol b/contracts/proxy/Proxy.sol index 988cf72a0..c2875aeac 100644 --- a/contracts/proxy/Proxy.sol +++ b/contracts/proxy/Proxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.6.0) (proxy/Proxy.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM diff --git a/contracts/proxy/beacon/BeaconProxy.sol b/contracts/proxy/beacon/BeaconProxy.sol index a278884f4..d603e38a7 100644 --- a/contracts/proxy/beacon/BeaconProxy.sol +++ b/contracts/proxy/beacon/BeaconProxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (proxy/beacon/BeaconProxy.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IBeacon.sol"; import "../Proxy.sol"; diff --git a/contracts/proxy/beacon/IBeacon.sol b/contracts/proxy/beacon/IBeacon.sol index fba3ee2ab..fcd655d6c 100644 --- a/contracts/proxy/beacon/IBeacon.sol +++ b/contracts/proxy/beacon/IBeacon.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (proxy/beacon/IBeacon.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev This is the interface that {BeaconProxy} expects of its beacon. diff --git a/contracts/proxy/beacon/UpgradeableBeacon.sol b/contracts/proxy/beacon/UpgradeableBeacon.sol index 557c58f67..24d167f60 100644 --- a/contracts/proxy/beacon/UpgradeableBeacon.sol +++ b/contracts/proxy/beacon/UpgradeableBeacon.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (proxy/beacon/UpgradeableBeacon.sol) -pragma solidity ^0.8.1; +pragma solidity ^0.8.19; import "./IBeacon.sol"; import "../../access/Ownable.sol"; diff --git a/contracts/proxy/transparent/ProxyAdmin.sol b/contracts/proxy/transparent/ProxyAdmin.sol index 3739ad85f..490f552f8 100644 --- a/contracts/proxy/transparent/ProxyAdmin.sol +++ b/contracts/proxy/transparent/ProxyAdmin.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.3) (proxy/transparent/ProxyAdmin.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./TransparentUpgradeableProxy.sol"; import "../../access/Ownable.sol"; diff --git a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol index 722abc8a3..01f55e99b 100644 --- a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (proxy/transparent/TransparentUpgradeableProxy.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC1967/ERC1967Proxy.sol"; diff --git a/contracts/proxy/utils/Initializable.sol b/contracts/proxy/utils/Initializable.sol index 94406a7a5..a42887918 100644 --- a/contracts/proxy/utils/Initializable.sol +++ b/contracts/proxy/utils/Initializable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/Initializable.sol) -pragma solidity ^0.8.2; +pragma solidity ^0.8.19; import "../../utils/Address.sol"; diff --git a/contracts/proxy/utils/UUPSUpgradeable.sol b/contracts/proxy/utils/UUPSUpgradeable.sol index 86684cfc3..c281c9091 100644 --- a/contracts/proxy/utils/UUPSUpgradeable.sol +++ b/contracts/proxy/utils/UUPSUpgradeable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/UUPSUpgradeable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../interfaces/draft-IERC1822.sol"; import "../ERC1967/ERC1967Upgrade.sol"; diff --git a/contracts/security/Pausable.sol b/contracts/security/Pausable.sol index bdd118432..cdf3ee2cd 100644 --- a/contracts/security/Pausable.sol +++ b/contracts/security/Pausable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/Context.sol"; diff --git a/contracts/security/ReentrancyGuard.sol b/contracts/security/ReentrancyGuard.sol index dac508b6a..88a86ae7e 100644 --- a/contracts/security/ReentrancyGuard.sol +++ b/contracts/security/ReentrancyGuard.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Contract module that helps prevent reentrant calls to a function. diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol index 31ddb6679..a2d7404bf 100644 --- a/contracts/token/ERC1155/ERC1155.sol +++ b/contracts/token/ERC1155/ERC1155.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC1155/ERC1155.sol) -pragma solidity ^0.8.1; +pragma solidity ^0.8.19; import "./IERC1155.sol"; import "./IERC1155Receiver.sol"; diff --git a/contracts/token/ERC1155/IERC1155.sol b/contracts/token/ERC1155/IERC1155.sol index 3446e604a..d7e25a5b1 100644 --- a/contracts/token/ERC1155/IERC1155.sol +++ b/contracts/token/ERC1155/IERC1155.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC1155/IERC1155.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../utils/introspection/IERC165.sol"; diff --git a/contracts/token/ERC1155/IERC1155Receiver.sol b/contracts/token/ERC1155/IERC1155Receiver.sol index 0dd271d04..a272f61b0 100644 --- a/contracts/token/ERC1155/IERC1155Receiver.sol +++ b/contracts/token/ERC1155/IERC1155Receiver.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/IERC1155Receiver.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../utils/introspection/IERC165.sol"; diff --git a/contracts/token/ERC1155/extensions/ERC1155Burnable.sol b/contracts/token/ERC1155/extensions/ERC1155Burnable.sol index 9ee371986..c079f07e1 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Burnable.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Burnable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC1155/extensions/ERC1155Burnable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC1155.sol"; diff --git a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol index cd12c2b4a..95f006e6f 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.2) (token/ERC1155/extensions/ERC1155Pausable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC1155.sol"; import "../../../security/Pausable.sol"; diff --git a/contracts/token/ERC1155/extensions/ERC1155Supply.sol b/contracts/token/ERC1155/extensions/ERC1155Supply.sol index fadc8f9ff..599df00f3 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Supply.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Supply.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.6.0) (token/ERC1155/extensions/ERC1155Supply.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC1155.sol"; diff --git a/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol b/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol index 623504f3d..5b0da2b33 100644 --- a/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol +++ b/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.6.0) (token/ERC1155/extensions/ERC1155URIStorage.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../../utils/Strings.sol"; import "../ERC1155.sol"; diff --git a/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol b/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol index 520a29715..2c998995e 100644 --- a/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol +++ b/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (token/ERC1155/extensions/IERC1155MetadataURI.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IERC1155.sol"; diff --git a/contracts/token/ERC1155/utils/ERC1155Holder.sol b/contracts/token/ERC1155/utils/ERC1155Holder.sol index 7249de841..b06a37f18 100644 --- a/contracts/token/ERC1155/utils/ERC1155Holder.sol +++ b/contracts/token/ERC1155/utils/ERC1155Holder.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/utils/ERC1155Holder.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./ERC1155Receiver.sol"; diff --git a/contracts/token/ERC1155/utils/ERC1155Receiver.sol b/contracts/token/ERC1155/utils/ERC1155Receiver.sol index 2e6804a2d..6fb21491f 100644 --- a/contracts/token/ERC1155/utils/ERC1155Receiver.sol +++ b/contracts/token/ERC1155/utils/ERC1155Receiver.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (token/ERC1155/utils/ERC1155Receiver.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IERC1155Receiver.sol"; import "../../../utils/introspection/ERC165.sol"; diff --git a/contracts/token/ERC20/ERC20.sol b/contracts/token/ERC20/ERC20.sol index 6e8da2801..2646a0ddc 100644 --- a/contracts/token/ERC20/ERC20.sol +++ b/contracts/token/ERC20/ERC20.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IERC20.sol"; import "./extensions/IERC20Metadata.sol"; diff --git a/contracts/token/ERC20/IERC20.sol b/contracts/token/ERC20/IERC20.sol index 6d5b4e9f1..a19535a30 100644 --- a/contracts/token/ERC20/IERC20.sol +++ b/contracts/token/ERC20/IERC20.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface of the ERC20 standard as defined in the EIP. diff --git a/contracts/token/ERC20/extensions/ERC20Burnable.sol b/contracts/token/ERC20/extensions/ERC20Burnable.sol index 1cd08ee81..cae186b64 100644 --- a/contracts/token/ERC20/extensions/ERC20Burnable.sol +++ b/contracts/token/ERC20/extensions/ERC20Burnable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/extensions/ERC20Burnable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC20.sol"; import "../../../utils/Context.sol"; diff --git a/contracts/token/ERC20/extensions/ERC20Capped.sol b/contracts/token/ERC20/extensions/ERC20Capped.sol index 3e2a9623e..cda072651 100644 --- a/contracts/token/ERC20/extensions/ERC20Capped.sol +++ b/contracts/token/ERC20/extensions/ERC20Capped.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/ERC20Capped.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC20.sol"; diff --git a/contracts/token/ERC20/extensions/ERC20FlashMint.sol b/contracts/token/ERC20/extensions/ERC20FlashMint.sol index 063fe99fb..ce68793b5 100644 --- a/contracts/token/ERC20/extensions/ERC20FlashMint.sol +++ b/contracts/token/ERC20/extensions/ERC20FlashMint.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/extensions/ERC20FlashMint.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../../interfaces/IERC3156FlashBorrower.sol"; import "../../../interfaces/IERC3156FlashLender.sol"; diff --git a/contracts/token/ERC20/extensions/ERC20Pausable.sol b/contracts/token/ERC20/extensions/ERC20Pausable.sol index 71302f978..b31cac7de 100644 --- a/contracts/token/ERC20/extensions/ERC20Pausable.sol +++ b/contracts/token/ERC20/extensions/ERC20Pausable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC20Pausable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC20.sol"; import "../../../security/Pausable.sol"; diff --git a/contracts/token/ERC20/extensions/ERC20Permit.sol b/contracts/token/ERC20/extensions/ERC20Permit.sol index 0f816d7bf..dbdc37053 100644 --- a/contracts/token/ERC20/extensions/ERC20Permit.sol +++ b/contracts/token/ERC20/extensions/ERC20Permit.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC20Permit.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IERC20Permit.sol"; import "../ERC20.sol"; diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index e7cda4f35..c078878ec 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC20Votes.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC20.sol"; import "../../../governance/utils/Votes.sol"; diff --git a/contracts/token/ERC20/extensions/ERC20Wrapper.sol b/contracts/token/ERC20/extensions/ERC20Wrapper.sol index bc85a0b37..bf2b225cf 100644 --- a/contracts/token/ERC20/extensions/ERC20Wrapper.sol +++ b/contracts/token/ERC20/extensions/ERC20Wrapper.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC20Wrapper.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC20.sol"; import "../utils/SafeERC20.sol"; diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index 4f6a2d64e..a706b5457 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC4626.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC20.sol"; import "../utils/SafeERC20.sol"; diff --git a/contracts/token/ERC20/extensions/IERC20Metadata.sol b/contracts/token/ERC20/extensions/IERC20Metadata.sol index 83ba6ac5e..1cf7e0b2e 100644 --- a/contracts/token/ERC20/extensions/IERC20Metadata.sol +++ b/contracts/token/ERC20/extensions/IERC20Metadata.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IERC20.sol"; diff --git a/contracts/token/ERC20/extensions/IERC20Permit.sol b/contracts/token/ERC20/extensions/IERC20Permit.sol index 0deb54b14..eb3e3f005 100644 --- a/contracts/token/ERC20/extensions/IERC20Permit.sol +++ b/contracts/token/ERC20/extensions/IERC20Permit.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/IERC20Permit.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index 7248134f2..751b82735 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/utils/SafeERC20.sol) -pragma solidity ^0.8.1; +pragma solidity ^0.8.19; import "../IERC20.sol"; import "../extensions/IERC20Permit.sol"; diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index d1d4a20dd..58a626cda 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/ERC721.sol) -pragma solidity ^0.8.1; +pragma solidity ^0.8.19; import "./IERC721.sol"; import "./IERC721Receiver.sol"; diff --git a/contracts/token/ERC721/IERC721.sol b/contracts/token/ERC721/IERC721.sol index 293f56683..ba4c108ef 100644 --- a/contracts/token/ERC721/IERC721.sol +++ b/contracts/token/ERC721/IERC721.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/IERC721.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../utils/introspection/IERC165.sol"; diff --git a/contracts/token/ERC721/IERC721Receiver.sol b/contracts/token/ERC721/IERC721Receiver.sol index de672099e..3839b6f6c 100644 --- a/contracts/token/ERC721/IERC721Receiver.sol +++ b/contracts/token/ERC721/IERC721Receiver.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.6.0) (token/ERC721/IERC721Receiver.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @title ERC721 token receiver interface diff --git a/contracts/token/ERC721/extensions/ERC721Burnable.sol b/contracts/token/ERC721/extensions/ERC721Burnable.sol index 0dc7dae2c..5489169e8 100644 --- a/contracts/token/ERC721/extensions/ERC721Burnable.sol +++ b/contracts/token/ERC721/extensions/ERC721Burnable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/extensions/ERC721Burnable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC721.sol"; import "../../../utils/Context.sol"; diff --git a/contracts/token/ERC721/extensions/ERC721Consecutive.sol b/contracts/token/ERC721/extensions/ERC721Consecutive.sol index 4191ad684..869d78d06 100644 --- a/contracts/token/ERC721/extensions/ERC721Consecutive.sol +++ b/contracts/token/ERC721/extensions/ERC721Consecutive.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/extensions/ERC721Consecutive.sol) -pragma solidity ^0.8.1; +pragma solidity ^0.8.19; import "../ERC721.sol"; import "../../../interfaces/IERC2309.sol"; diff --git a/contracts/token/ERC721/extensions/ERC721Enumerable.sol b/contracts/token/ERC721/extensions/ERC721Enumerable.sol index aab81a9f3..6b702d53b 100644 --- a/contracts/token/ERC721/extensions/ERC721Enumerable.sol +++ b/contracts/token/ERC721/extensions/ERC721Enumerable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/extensions/ERC721Enumerable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC721.sol"; import "./IERC721Enumerable.sol"; diff --git a/contracts/token/ERC721/extensions/ERC721Pausable.sol b/contracts/token/ERC721/extensions/ERC721Pausable.sol index e8eb4b103..0cadaa7c7 100644 --- a/contracts/token/ERC721/extensions/ERC721Pausable.sol +++ b/contracts/token/ERC721/extensions/ERC721Pausable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.2) (token/ERC721/extensions/ERC721Pausable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC721.sol"; import "../../../security/Pausable.sol"; diff --git a/contracts/token/ERC721/extensions/ERC721Royalty.sol b/contracts/token/ERC721/extensions/ERC721Royalty.sol index 298e34205..7d6ef6c04 100644 --- a/contracts/token/ERC721/extensions/ERC721Royalty.sol +++ b/contracts/token/ERC721/extensions/ERC721Royalty.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/extensions/ERC721Royalty.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC721.sol"; import "../../common/ERC2981.sol"; diff --git a/contracts/token/ERC721/extensions/ERC721URIStorage.sol b/contracts/token/ERC721/extensions/ERC721URIStorage.sol index c9d79b6b2..6350a0952 100644 --- a/contracts/token/ERC721/extensions/ERC721URIStorage.sol +++ b/contracts/token/ERC721/extensions/ERC721URIStorage.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/extensions/ERC721URIStorage.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC721.sol"; import "../../../interfaces/IERC4906.sol"; diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 55b82c1c1..89b2e073c 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/extensions/ERC721Votes.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC721.sol"; import "../../../governance/utils/Votes.sol"; diff --git a/contracts/token/ERC721/extensions/ERC721Wrapper.sol b/contracts/token/ERC721/extensions/ERC721Wrapper.sol index c7ff06dec..58346182b 100644 --- a/contracts/token/ERC721/extensions/ERC721Wrapper.sol +++ b/contracts/token/ERC721/extensions/ERC721Wrapper.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/extensions/ERC721Wrapper.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC721.sol"; diff --git a/contracts/token/ERC721/extensions/IERC721Enumerable.sol b/contracts/token/ERC721/extensions/IERC721Enumerable.sol index dfea427ba..d5fe6330c 100644 --- a/contracts/token/ERC721/extensions/IERC721Enumerable.sol +++ b/contracts/token/ERC721/extensions/IERC721Enumerable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.5.0) (token/ERC721/extensions/IERC721Enumerable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IERC721.sol"; diff --git a/contracts/token/ERC721/extensions/IERC721Metadata.sol b/contracts/token/ERC721/extensions/IERC721Metadata.sol index dca77ba5b..0b085812c 100644 --- a/contracts/token/ERC721/extensions/IERC721Metadata.sol +++ b/contracts/token/ERC721/extensions/IERC721Metadata.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/IERC721Metadata.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IERC721.sol"; diff --git a/contracts/token/ERC721/utils/ERC721Holder.sol b/contracts/token/ERC721/utils/ERC721Holder.sol index f18ad8a1d..41bee820c 100644 --- a/contracts/token/ERC721/utils/ERC721Holder.sol +++ b/contracts/token/ERC721/utils/ERC721Holder.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/utils/ERC721Holder.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IERC721Receiver.sol"; diff --git a/contracts/token/common/ERC2981.sol b/contracts/token/common/ERC2981.sol index 7d47b6c7e..5d40388aa 100644 --- a/contracts/token/common/ERC2981.sol +++ b/contracts/token/common/ERC2981.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (token/common/ERC2981.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../interfaces/IERC2981.sol"; import "../../utils/introspection/ERC165.sol"; diff --git a/contracts/utils/Address.sol b/contracts/utils/Address.sol index 8aada4b04..02f475620 100644 --- a/contracts/utils/Address.sol +++ b/contracts/utils/Address.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (utils/Address.sol) -pragma solidity ^0.8.1; +pragma solidity ^0.8.19; /** * @dev Collection of functions related to the address type diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index 66fe322b5..383a0820a 100644 --- a/contracts/utils/Arrays.sol +++ b/contracts/utils/Arrays.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (utils/Arrays.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./StorageSlot.sol"; import "./math/Math.sol"; diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index 4e08cd563..9ba6defe0 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (utils/Base64.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Provides a set of functions to operate with Base64 strings. diff --git a/contracts/utils/Context.sol b/contracts/utils/Context.sol index f304065b4..2d517987d 100644 --- a/contracts/utils/Context.sol +++ b/contracts/utils/Context.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Provides information about the current execution context, including the diff --git a/contracts/utils/Counters.sol b/contracts/utils/Counters.sol index 8a4f2a2e7..2c6951840 100644 --- a/contracts/utils/Counters.sol +++ b/contracts/utils/Counters.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (utils/Counters.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @title Counters diff --git a/contracts/utils/Create2.sol b/contracts/utils/Create2.sol index 29fa97329..d5776885f 100644 --- a/contracts/utils/Create2.sol +++ b/contracts/utils/Create2.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (utils/Create2.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Helper to make usage of the `CREATE2` EVM opcode easier and safer. diff --git a/contracts/utils/Multicall.sol b/contracts/utils/Multicall.sol index 5729f8452..8e0ef8195 100644 --- a/contracts/utils/Multicall.sol +++ b/contracts/utils/Multicall.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (utils/Multicall.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./Address.sol"; diff --git a/contracts/utils/Nonces.sol b/contracts/utils/Nonces.sol index ce65909ff..a908234ee 100644 --- a/contracts/utils/Nonces.sol +++ b/contracts/utils/Nonces.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./Counters.sol"; diff --git a/contracts/utils/ShortStrings.sol b/contracts/utils/ShortStrings.sol index 90a054999..a6cb1e620 100644 --- a/contracts/utils/ShortStrings.sol +++ b/contracts/utils/ShortStrings.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (utils/ShortStrings.sol) -pragma solidity ^0.8.8; +pragma solidity ^0.8.19; import "./StorageSlot.sol"; diff --git a/contracts/utils/StorageSlot.sol b/contracts/utils/StorageSlot.sol index 5bba0b0f8..f12640e7b 100644 --- a/contracts/utils/StorageSlot.sol +++ b/contracts/utils/StorageSlot.sol @@ -2,7 +2,7 @@ // OpenZeppelin Contracts (last updated v4.9.0) (utils/StorageSlot.sol) // This file was procedurally generated from scripts/generate/templates/StorageSlot.js. -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Library for reading and writing primitive types to specific storage slots. diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 657ebd663..33d7bbf59 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (utils/Strings.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./math/Math.sol"; import "./math/SignedMath.sol"; diff --git a/contracts/utils/cryptography/ECDSA.sol b/contracts/utils/cryptography/ECDSA.sol index f97922419..f12815d9d 100644 --- a/contracts/utils/cryptography/ECDSA.sol +++ b/contracts/utils/cryptography/ECDSA.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/ECDSA.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../Strings.sol"; diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol index 2a0e73486..526b13580 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/EIP712.sol) -pragma solidity ^0.8.8; +pragma solidity ^0.8.19; import "./ECDSA.sol"; import "../ShortStrings.sol"; diff --git a/contracts/utils/cryptography/MerkleProof.sol b/contracts/utils/cryptography/MerkleProof.sol index cd79e51cf..0bcdda2cd 100644 --- a/contracts/utils/cryptography/MerkleProof.sol +++ b/contracts/utils/cryptography/MerkleProof.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/MerkleProof.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev These functions deal with verification of Merkle Tree proofs. diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index 1815d27fe..ba8f7cf36 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/SignatureChecker.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./ECDSA.sol"; import "../../interfaces/IERC1271.sol"; diff --git a/contracts/utils/introspection/ERC165.sol b/contracts/utils/introspection/ERC165.sol index 49395f8a1..6849637e8 100644 --- a/contracts/utils/introspection/ERC165.sol +++ b/contracts/utils/introspection/ERC165.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IERC165.sol"; diff --git a/contracts/utils/introspection/ERC165Checker.sol b/contracts/utils/introspection/ERC165Checker.sol index 4831b6522..4dc84c163 100644 --- a/contracts/utils/introspection/ERC165Checker.sol +++ b/contracts/utils/introspection/ERC165Checker.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (utils/introspection/ERC165Checker.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IERC165.sol"; diff --git a/contracts/utils/introspection/IERC165.sol b/contracts/utils/introspection/IERC165.sol index e8cdbdbf6..da6f186d4 100644 --- a/contracts/utils/introspection/IERC165.sol +++ b/contracts/utils/introspection/IERC165.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface of the ERC165 standard, as defined in the diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index d62ad97d9..ff3dc8d9f 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (utils/math/Math.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Standard math utilities missing in the Solidity language. diff --git a/contracts/utils/math/SafeCast.sol b/contracts/utils/math/SafeCast.sol index 0744b3711..d9e21bb17 100644 --- a/contracts/utils/math/SafeCast.sol +++ b/contracts/utils/math/SafeCast.sol @@ -2,7 +2,7 @@ // OpenZeppelin Contracts (last updated v4.8.0) (utils/math/SafeCast.sol) // This file was procedurally generated from scripts/generate/templates/SafeCast.js. -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Wrappers over Solidity's uintXX/intXX casting operators with added overflow diff --git a/contracts/utils/math/SignedMath.sol b/contracts/utils/math/SignedMath.sol index 3ea9f8bf0..80413531d 100644 --- a/contracts/utils/math/SignedMath.sol +++ b/contracts/utils/math/SignedMath.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (utils/math/SignedMath.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Standard signed math utilities missing in the Solidity language. diff --git a/contracts/utils/structs/BitMaps.sol b/contracts/utils/structs/BitMaps.sol index 2567fce47..9786c7f8e 100644 --- a/contracts/utils/structs/BitMaps.sol +++ b/contracts/utils/structs/BitMaps.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/BitMaps.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Library for managing uint256 to bool mapping in a compact and efficient way, providing the keys are sequential. diff --git a/contracts/utils/structs/Checkpoints.sol b/contracts/utils/structs/Checkpoints.sol index f2ee45811..a0c45f659 100644 --- a/contracts/utils/structs/Checkpoints.sol +++ b/contracts/utils/structs/Checkpoints.sol @@ -2,7 +2,7 @@ // OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/Checkpoints.sol) // This file was procedurally generated from scripts/generate/templates/Checkpoints.js. -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../math/Math.sol"; import "../math/SafeCast.sol"; diff --git a/contracts/utils/structs/DoubleEndedQueue.sol b/contracts/utils/structs/DoubleEndedQueue.sol index 325918d2d..be6e3898f 100644 --- a/contracts/utils/structs/DoubleEndedQueue.sol +++ b/contracts/utils/structs/DoubleEndedQueue.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/DoubleEndedQueue.sol) -pragma solidity ^0.8.4; +pragma solidity ^0.8.19; import "../math/SafeCast.sol"; diff --git a/contracts/utils/structs/EnumerableMap.sol b/contracts/utils/structs/EnumerableMap.sol index f34481426..4bd18055d 100644 --- a/contracts/utils/structs/EnumerableMap.sol +++ b/contracts/utils/structs/EnumerableMap.sol @@ -2,7 +2,7 @@ // OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/EnumerableMap.sol) // This file was procedurally generated from scripts/generate/templates/EnumerableMap.js. -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./EnumerableSet.sol"; diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index 447f96302..b10b3cdcd 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -2,7 +2,7 @@ // OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/EnumerableSet.sol) // This file was procedurally generated from scripts/generate/templates/EnumerableSet.js. -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Library for managing diff --git a/contracts/vendor/compound/ICompoundTimelock.sol b/contracts/vendor/compound/ICompoundTimelock.sol index fb33a6805..1b04290b4 100644 --- a/contracts/vendor/compound/ICompoundTimelock.sol +++ b/contracts/vendor/compound/ICompoundTimelock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.6.0) (vendor/compound/ICompoundTimelock.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol[Compound's timelock] interface diff --git a/docs/modules/ROOT/pages/access-control.adoc b/docs/modules/ROOT/pages/access-control.adoc index e52c10401..11a5fab0c 100644 --- a/docs/modules/ROOT/pages/access-control.adoc +++ b/docs/modules/ROOT/pages/access-control.adoc @@ -13,7 +13,7 @@ OpenZeppelin Contracts provides xref:api:access.adoc#Ownable[`Ownable`] for impl ---- // contracts/MyContract.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/Ownable.sol"; @@ -62,7 +62,7 @@ Here's a simple example of using `AccessControl` in an xref:tokens.adoc#ERC20[`E ---- // contracts/MyToken.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -94,7 +94,7 @@ Let's augment our ERC20 token example by also defining a 'burner' role, which le ---- // contracts/MyToken.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -139,7 +139,7 @@ Let's take a look at the ERC20 token example, this time taking advantage of the ---- // contracts/MyToken.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/docs/modules/ROOT/pages/erc1155.adoc b/docs/modules/ROOT/pages/erc1155.adoc index d7976ca1f..067bde89e 100644 --- a/docs/modules/ROOT/pages/erc1155.adoc +++ b/docs/modules/ROOT/pages/erc1155.adoc @@ -34,7 +34,7 @@ Here's what a contract for tokenized items might look like: ---- // contracts/GameItems.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; @@ -134,7 +134,7 @@ In order for our contract to receive ERC1155 tokens we can inherit from the conv ---- // contracts/MyContract.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; diff --git a/docs/modules/ROOT/pages/erc20.adoc b/docs/modules/ROOT/pages/erc20.adoc index e72491ab6..817d364b1 100644 --- a/docs/modules/ROOT/pages/erc20.adoc +++ b/docs/modules/ROOT/pages/erc20.adoc @@ -15,7 +15,7 @@ Here's what our GLD token might look like. ---- // contracts/GLDToken.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/docs/modules/ROOT/pages/erc721.adoc b/docs/modules/ROOT/pages/erc721.adoc index 812630ab3..695f44355 100644 --- a/docs/modules/ROOT/pages/erc721.adoc +++ b/docs/modules/ROOT/pages/erc721.adoc @@ -14,7 +14,7 @@ Here's what a contract for tokenized items might look like: ---- // contracts/GameItem.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; diff --git a/docs/modules/ROOT/pages/extending-contracts.adoc b/docs/modules/ROOT/pages/extending-contracts.adoc index d12ef7585..df93f2133 100644 --- a/docs/modules/ROOT/pages/extending-contracts.adoc +++ b/docs/modules/ROOT/pages/extending-contracts.adoc @@ -20,7 +20,7 @@ For example, imagine you want to change xref:api:access.adoc#AccessControl[`Acce ```solidity // contracts/ModifiedAccessControl.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/AccessControl.sol"; @@ -48,7 +48,7 @@ Here is a modified version of xref:api:access.adoc#AccessControl[`AccessControl` ```solidity // contracts/ModifiedAccessControl.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/AccessControl.sol"; @@ -80,7 +80,7 @@ Hooks are simply functions that are called before or after some action takes pla Here's how you would implement the `IERC721Receiver` pattern in `ERC20`, using the xref:api:token/ERC20.adoc#ERC20-_beforeTokenTransfer-address-address-uint256-[`_beforeTokenTransfer`] hook: ```solidity -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index c5bcf58cd..a40275b07 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -44,7 +44,7 @@ The voting power of each account in our governance setup will be determined by a ```solidity // SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; @@ -82,7 +82,7 @@ If your project already has a live token that does not include ERC20Votes and is ```solidity // SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; @@ -147,7 +147,7 @@ We can optionally set a proposal threshold as well. This restricts proposal crea ```solidity // SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/governance/Governor.sol"; import "@openzeppelin/contracts/governance/compatibility/GovernorCompatibilityBravo.sol"; @@ -339,7 +339,7 @@ Since v4.9, all voting contracts (including xref:api:token/ERC20.adoc#ERC20Votes ```solidity // SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; +pragma solidity ^0.8.19; import "github.com/openzeppelin/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; import "github.com/openzeppelin/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol"; @@ -389,7 +389,7 @@ The Governor will automatically detect the clock mode used by the token and adap ```solidity // SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/governance/Governor.sol"; import "@openzeppelin/contracts/governance/compatibility/GovernorCompatibilityBravo.sol"; diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index 5b64f0508..b3b51abfc 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -26,7 +26,7 @@ Once installed, you can use the contracts in the library by importing them: ---- // contracts/MyNFT.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index eecb8293e..efe59efcd 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -156,7 +156,7 @@ Consider this dummy contract: ---- // contracts/Box.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/utils/Multicall.sol"; diff --git a/hardhat.config.js b/hardhat.config.js index 17d2c47b2..66367e96e 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -40,7 +40,7 @@ const argv = require('yargs/yargs')() compiler: { alias: 'compileVersion', type: 'string', - default: '0.8.13', + default: '0.8.19', }, coinmarketcap: { alias: 'coinmarketcapApiKey', diff --git a/scripts/generate/templates/Checkpoints.js b/scripts/generate/templates/Checkpoints.js index f339fde22..c85cfcbb3 100644 --- a/scripts/generate/templates/Checkpoints.js +++ b/scripts/generate/templates/Checkpoints.js @@ -3,7 +3,7 @@ const { OPTS } = require('./Checkpoints.opts.js'); // TEMPLATE const header = `\ -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../math/Math.sol"; import "../math/SafeCast.sol"; diff --git a/scripts/generate/templates/Checkpoints.t.js b/scripts/generate/templates/Checkpoints.t.js index 2b8c7b67c..c5b225f38 100644 --- a/scripts/generate/templates/Checkpoints.t.js +++ b/scripts/generate/templates/Checkpoints.t.js @@ -4,7 +4,7 @@ const { OPTS } = require('./Checkpoints.opts.js'); // TEMPLATE const header = `\ -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "forge-std/Test.sol"; import "../../../contracts/utils/math/SafeCast.sol"; diff --git a/scripts/generate/templates/EnumerableMap.js b/scripts/generate/templates/EnumerableMap.js index 61e23cfb4..13d3d8686 100644 --- a/scripts/generate/templates/EnumerableMap.js +++ b/scripts/generate/templates/EnumerableMap.js @@ -10,7 +10,7 @@ const TYPES = [ /* eslint-disable max-len */ const header = `\ -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./EnumerableSet.sol"; diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index 2b6a9c64d..1edab9162 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -9,7 +9,7 @@ const TYPES = [ /* eslint-disable max-len */ const header = `\ -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Library for managing diff --git a/scripts/generate/templates/SafeCast.js b/scripts/generate/templates/SafeCast.js index b47193be6..0d78a2ca3 100644 --- a/scripts/generate/templates/SafeCast.js +++ b/scripts/generate/templates/SafeCast.js @@ -61,7 +61,7 @@ const version = (selector, length) => { }; const header = `\ -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Wrappers over Solidity's uintXX/intXX casting operators with added overflow diff --git a/scripts/generate/templates/StorageSlot.js b/scripts/generate/templates/StorageSlot.js index f985f2242..b51affc22 100644 --- a/scripts/generate/templates/StorageSlot.js +++ b/scripts/generate/templates/StorageSlot.js @@ -18,7 +18,7 @@ const VERSIONS = unique(TYPES.map(t => t.version)).map( ); const header = `\ -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Library for reading and writing primitive types to specific storage slots. diff --git a/scripts/upgradeable/upgradeable.patch b/scripts/upgradeable/upgradeable.patch index efc89cab5..b29e7d12b 100644 --- a/scripts/upgradeable/upgradeable.patch +++ b/scripts/upgradeable/upgradeable.patch @@ -87,7 +87,7 @@ index 9fc95518..53130e3c 100644 @@ -35,10 +38,11 @@ Once installed, you can use the contracts in the library by importing them: ```solidity - pragma solidity ^0.8.0; + pragma solidity ^0.8.19; -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; @@ -190,7 +190,7 @@ index 6a4e1cad..55d8eced 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -4,7 +4,6 @@ - pragma solidity ^0.8.8; + pragma solidity ^0.8.19; import "./ECDSA.sol"; -import "../ShortStrings.sol"; diff --git a/test/token/ERC20/extensions/ERC4626.t.sol b/test/token/ERC20/extensions/ERC4626.t.sol index 956dd2ce0..da01c7d81 100644 --- a/test/token/ERC20/extensions/ERC4626.t.sol +++ b/test/token/ERC20/extensions/ERC4626.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import {ERC4626Test} from "erc4626-tests/ERC4626.test.sol"; diff --git a/test/token/ERC721/extensions/ERC721Consecutive.t.sol b/test/token/ERC721/extensions/ERC721Consecutive.t.sol index 3cc868929..6b70ab028 100644 --- a/test/token/ERC721/extensions/ERC721Consecutive.t.sol +++ b/test/token/ERC721/extensions/ERC721Consecutive.t.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; // solhint-disable func-name-mixedcase diff --git a/test/utils/ShortStrings.t.sol b/test/utils/ShortStrings.t.sol index 7c4faa89d..b70793bd7 100644 --- a/test/utils/ShortStrings.t.sol +++ b/test/utils/ShortStrings.t.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "forge-std/Test.sol"; diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index 9e7e118df..61f558e0e 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "forge-std/Test.sol"; diff --git a/test/utils/structs/Checkpoints.t.sol b/test/utils/structs/Checkpoints.t.sol index e3f4bd28b..1fb6dcb4e 100644 --- a/test/utils/structs/Checkpoints.t.sol +++ b/test/utils/structs/Checkpoints.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // This file was procedurally generated from scripts/generate/templates/Checkpoints.t.js. -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "forge-std/Test.sol"; import "../../../contracts/utils/math/SafeCast.sol"; From 2ee1da12c4498fc130b7770905d0b4c4f91f6a9d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 31 May 2023 16:40:28 +0200 Subject: [PATCH 148/197] Remove utils/Counters.sol (#4289) Co-authored-by: Francisco Giordano --- CHANGELOG.md | 3 +- contracts/mocks/proxy/UUPSUpgradeableMock.sol | 7 +- contracts/utils/Counters.sol | 43 ---------- contracts/utils/Nonces.sol | 19 ++--- contracts/utils/README.adoc | 3 - docs/modules/ROOT/pages/erc721.adoc | 13 ++- docs/modules/ROOT/pages/utilities.adoc | 2 - test/utils/Counters.test.js | 84 ------------------- test/utils/Nonces.test.js | 2 +- 9 files changed, 20 insertions(+), 156 deletions(-) delete mode 100644 contracts/utils/Counters.sol delete mode 100644 test/utils/Counters.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d2853cf8..2fc4cc5e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ ### Removals -The following contracts were removed: +The following contracts and libraries were removed: +- `Counters` - `ERC20Snapshot` - `ERC20VotesComp` - `GovernorVotesComp` diff --git a/contracts/mocks/proxy/UUPSUpgradeableMock.sol b/contracts/mocks/proxy/UUPSUpgradeableMock.sol index 83e6ef465..567f3b536 100644 --- a/contracts/mocks/proxy/UUPSUpgradeableMock.sol +++ b/contracts/mocks/proxy/UUPSUpgradeableMock.sol @@ -3,17 +3,16 @@ pragma solidity ^0.8.19; import "../../proxy/utils/UUPSUpgradeable.sol"; -import "../../utils/Counters.sol"; contract NonUpgradeableMock { - Counters.Counter internal _counter; + uint256 internal _counter; function current() external view returns (uint256) { - return Counters.current(_counter); + return _counter; } function increment() external { - return Counters.increment(_counter); + ++_counter; } } diff --git a/contracts/utils/Counters.sol b/contracts/utils/Counters.sol deleted file mode 100644 index 2c6951840..000000000 --- a/contracts/utils/Counters.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/Counters.sol) - -pragma solidity ^0.8.19; - -/** - * @title Counters - * @author Matt Condon (@shrugs) - * @dev Provides counters that can only be incremented, decremented or reset. This can be used e.g. to track the number - * of elements in a mapping, issuing ERC721 ids, or counting request ids. - * - * Include with `using Counters for Counters.Counter;` - */ -library Counters { - struct Counter { - // This variable should never be directly accessed by users of the library: interactions must be restricted to - // the library's function. As of Solidity v0.5.2, this cannot be enforced, though there is a proposal to add - // this feature: see https://github.com/ethereum/solidity/issues/4637 - uint256 _value; // default: 0 - } - - function current(Counter storage counter) internal view returns (uint256) { - return counter._value; - } - - function increment(Counter storage counter) internal { - unchecked { - counter._value += 1; - } - } - - function decrement(Counter storage counter) internal { - uint256 value = counter._value; - require(value > 0, "Counter: decrement overflow"); - unchecked { - counter._value = value - 1; - } - } - - function reset(Counter storage counter) internal { - counter._value = 0; - } -} diff --git a/contracts/utils/Nonces.sol b/contracts/utils/Nonces.sol index a908234ee..04b884797 100644 --- a/contracts/utils/Nonces.sol +++ b/contracts/utils/Nonces.sol @@ -1,21 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import "./Counters.sol"; - /** * @dev Provides tracking nonces for addresses. Nonces will only increment. */ abstract contract Nonces { - using Counters for Counters.Counter; - - mapping(address => Counters.Counter) private _nonces; + mapping(address => uint256) private _nonces; /** * @dev Returns an address nonce. */ function nonces(address owner) public view virtual returns (uint256) { - return _nonces[owner].current(); + return _nonces[owner]; } /** @@ -23,9 +19,12 @@ abstract contract Nonces { * * Returns the current value and increments nonce. */ - function _useNonce(address owner) internal virtual returns (uint256 current) { - Counters.Counter storage nonce = _nonces[owner]; - current = nonce.current(); - nonce.increment(); + function _useNonce(address owner) internal virtual returns (uint256) { + // For each account, the nonce has an initial value of 0, can only be incremented by one, and cannot be + // decremented or reset. This guarantees that the nonce never overflows. + unchecked { + // It is important to do x++ and not ++x here. + return _nonces[owner]++; + } } } diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 07cee6427..aa5182bd1 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -10,7 +10,6 @@ The {Address}, {Arrays}, {Base64} and {Strings} libraries provide more operation For new data types: - * {Counters}: a simple way to get a counter that can only be incremented, decremented or reset. Very useful for ID generation, counting contract activity, among others. * {EnumerableMap}: like Solidity's https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`] type, but with key-value _enumeration_: this will let you know how many entries a mapping has, and iterate over them (which is not possible with `mapping`). * {EnumerableSet}: like {EnumerableMap}, but for https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets]. Can be used to store privileged accounts, issued IDs, etc. @@ -75,8 +74,6 @@ Ethereum contracts have no native concept of an interface, so applications must {{Base64}} -{{Counters}} - {{Strings}} {{ShortStrings}} diff --git a/docs/modules/ROOT/pages/erc721.adoc b/docs/modules/ROOT/pages/erc721.adoc index 695f44355..6d839c81e 100644 --- a/docs/modules/ROOT/pages/erc721.adoc +++ b/docs/modules/ROOT/pages/erc721.adoc @@ -17,11 +17,9 @@ Here's what a contract for tokenized items might look like: pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; -import "@openzeppelin/contracts/utils/Counters.sol"; contract GameItem is ERC721URIStorage { - using Counters for Counters.Counter; - Counters.Counter private _tokenIds; + uint256 private _nextTokenId; constructor() ERC721("GameItem", "ITM") {} @@ -29,12 +27,11 @@ contract GameItem is ERC721URIStorage { public returns (uint256) { - uint256 newItemId = _tokenIds.current(); - _mint(player, newItemId); - _setTokenURI(newItemId, tokenURI); + uint256 tokenId = _nextTokenId++; + _mint(player, tokenId); + _setTokenURI(tokenId, tokenURI); - _tokenIds.increment(); - return newItemId; + return tokenId; } } ---- diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index efe59efcd..0b178d9fe 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -97,8 +97,6 @@ If you need support for more powerful collections than Solidity's native arrays [[misc]] == Misc -Want to keep track of some numbers that increment by 1 every time you want another one? Check out xref:api:utils.adoc#Counters[`Counters`]. This is useful for lots of things, like creating incremental identifiers, as shown on the xref:erc721.adoc[ERC721 guide]. - === Base64 xref:api:utils.adoc#Base64[`Base64`] util allows you to transform `bytes32` data into its Base64 `string` representation. diff --git a/test/utils/Counters.test.js b/test/utils/Counters.test.js deleted file mode 100644 index 6fb89922d..000000000 --- a/test/utils/Counters.test.js +++ /dev/null @@ -1,84 +0,0 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); - -const { expect } = require('chai'); - -const Counters = artifacts.require('$Counters'); - -contract('Counters', function () { - beforeEach(async function () { - this.counter = await Counters.new(); - }); - - it('starts at zero', async function () { - expect(await this.counter.$current(0)).to.be.bignumber.equal('0'); - }); - - describe('increment', function () { - context('starting from 0', function () { - it('increments the current value by one', async function () { - await this.counter.$increment(0); - expect(await this.counter.$current(0)).to.be.bignumber.equal('1'); - }); - - it('can be called multiple times', async function () { - await this.counter.$increment(0); - await this.counter.$increment(0); - await this.counter.$increment(0); - - expect(await this.counter.$current(0)).to.be.bignumber.equal('3'); - }); - }); - }); - - describe('decrement', function () { - beforeEach(async function () { - await this.counter.$increment(0); - expect(await this.counter.$current(0)).to.be.bignumber.equal('1'); - }); - context('starting from 1', function () { - it('decrements the current value by one', async function () { - await this.counter.$decrement(0); - expect(await this.counter.$current(0)).to.be.bignumber.equal('0'); - }); - - it('reverts if the current value is 0', async function () { - await this.counter.$decrement(0); - await expectRevert(this.counter.$decrement(0), 'Counter: decrement overflow'); - }); - }); - context('after incremented to 3', function () { - it('can be called multiple times', async function () { - await this.counter.$increment(0); - await this.counter.$increment(0); - - expect(await this.counter.$current(0)).to.be.bignumber.equal('3'); - - await this.counter.$decrement(0); - await this.counter.$decrement(0); - await this.counter.$decrement(0); - - expect(await this.counter.$current(0)).to.be.bignumber.equal('0'); - }); - }); - }); - - describe('reset', function () { - context('null counter', function () { - it('does not throw', async function () { - await this.counter.$reset(0); - expect(await this.counter.$current(0)).to.be.bignumber.equal('0'); - }); - }); - - context('non null counter', function () { - beforeEach(async function () { - await this.counter.$increment(0); - expect(await this.counter.$current(0)).to.be.bignumber.equal('1'); - }); - it('reset to 0', async function () { - await this.counter.$reset(0); - expect(await this.counter.$current(0)).to.be.bignumber.equal('0'); - }); - }); - }); -}); diff --git a/test/utils/Nonces.test.js b/test/utils/Nonces.test.js index a09cddb1f..4a01bb1bc 100644 --- a/test/utils/Nonces.test.js +++ b/test/utils/Nonces.test.js @@ -19,7 +19,7 @@ contract('Nonces', function (accounts) { expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); const { receipt } = await this.nonces.$_useNonce(sender); - expectEvent(receipt, 'return$_useNonce', { current: '0' }); + expectEvent(receipt, 'return$_useNonce', ['0']); expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); }); From 30256fa8384cd8e95de7413454bb74c913ed9f3f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 20:15:51 -0300 Subject: [PATCH 149/197] Update lockfile (#4294) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 2407 +++++++++------------------------------------ 1 file changed, 472 insertions(+), 1935 deletions(-) diff --git a/package-lock.json b/package-lock.json index ddfe359ef..edf1e9284 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openzeppelin-solidity", - "version": "4.8.2", + "version": "4.9.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "openzeppelin-solidity", - "version": "4.8.2", + "version": "4.9.0", "license": "MIT", "devDependencies": { "@changesets/changelog-github": "^0.4.8", @@ -87,9 +87,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", - "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.3.tgz", + "integrity": "sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==", "dev": true, "dependencies": { "regenerator-runtime": "^0.13.11" @@ -549,14 +549,14 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", - "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.1", + "espree": "^9.5.2", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -590,9 +590,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", - "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.41.0.tgz", + "integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2257,9 +2257,9 @@ } }, "node_modules/@openzeppelin/upgrades-core": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.25.0.tgz", - "integrity": "sha512-vSxOSm1k+P156nNm15ydhOmSPGC37mnl092FMVOH+eGaoqLjr2Za6ULVjDMFzvMnG+sGE1UoDOqBNPfTm0ch8w==", + "version": "1.26.2", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.26.2.tgz", + "integrity": "sha512-TJORrgyun5qflPos/47P3j61gDw+7W+tEirSBOYRxfVL1WGjX1n8iaLrijPIqzyeS1MKguN1nckAMspQ4SKrxw==", "dev": true, "dependencies": { "cbor": "^8.0.0", @@ -2522,38 +2522,14 @@ } }, "node_modules/@truffle/abi-utils": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@truffle/abi-utils/-/abi-utils-0.3.9.tgz", - "integrity": "sha512-G5dqgwRHx5zwlXjz3QT8OJVfB2cOqWwD6DwKso0KttUt/zejhCjnkKq72rSgyeLMkz7wBB9ERLOsupLBILM8MA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@truffle/abi-utils/-/abi-utils-1.0.0.tgz", + "integrity": "sha512-h1wGFB28YfByAWm/uBeMCwqDlGsrcMYTumLC/sB/qYhHisi1LK6tV47FEF7zKyf6Al2CtsO28v02+wfLXbUVRg==", "dev": true, "dependencies": { "change-case": "3.0.2", "fast-check": "3.1.1", - "web3-utils": "1.8.2" - } - }, - "node_modules/@truffle/abi-utils/node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - }, - "node_modules/@truffle/abi-utils/node_modules/web3-utils": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", - "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", - "dev": true, - "dependencies": { - "bn.js": "^5.2.1", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" - }, - "engines": { - "node": ">=8.0.0" + "web3-utils": "1.10.0" } }, "node_modules/@truffle/blockchain-utils": { @@ -2563,13 +2539,13 @@ "dev": true }, "node_modules/@truffle/codec": { - "version": "0.14.17", - "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.14.17.tgz", - "integrity": "sha512-kD4dD86huLeaBEq5R8D1zleJEu6NsXbyYLdXl1V1TKdiO8odw5CBC6Y/+wdu5d3t1dyEYrTbhn1dqknZa52pmw==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.15.1.tgz", + "integrity": "sha512-OBANcmefxEXLApWl/uU1SOHQJixO8pDaRTybP0YMvxPhgyj7G7+wC+fUvnZdmrTJD2WJFLuoYvZbxILmycEvPg==", "dev": true, "dependencies": { - "@truffle/abi-utils": "^0.3.9", - "@truffle/compile-common": "^0.9.4", + "@truffle/abi-utils": "^1.0.0", + "@truffle/compile-common": "^0.9.5", "big.js": "^6.0.3", "bn.js": "^5.1.3", "cbor": "^5.2.0", @@ -2577,7 +2553,7 @@ "lodash": "^4.17.21", "semver": "7.3.7", "utf8": "^3.0.0", - "web3-utils": "1.8.2" + "web3-utils": "1.10.0" } }, "node_modules/@truffle/codec/node_modules/bignumber.js": { @@ -2644,24 +2620,6 @@ "node": ">=10" } }, - "node_modules/@truffle/codec/node_modules/web3-utils": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", - "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", - "dev": true, - "dependencies": { - "bn.js": "^5.2.1", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@truffle/codec/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -2669,9 +2627,9 @@ "dev": true }, "node_modules/@truffle/compile-common": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/@truffle/compile-common/-/compile-common-0.9.4.tgz", - "integrity": "sha512-mnqJB/hLiPHNf+WKwt/2MH6lv34xSG/SFCib7+ckAklutUqVLeFo8EwQxinuHNkU7LY0C+YgZXhK1WTCO5YRJQ==", + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@truffle/compile-common/-/compile-common-0.9.5.tgz", + "integrity": "sha512-qOIT7nYzQsrnpjk8LurKE6EYYvvJIk3rCHfn+xed88aG6F1l4WYtkUKl+Dcwgxgv3LH0khcQOpjqkXK5kUIJ8A==", "dev": true, "dependencies": { "@truffle/error": "^0.2.0", @@ -2685,31 +2643,31 @@ "dev": true }, "node_modules/@truffle/contract": { - "version": "4.6.20", - "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.20.tgz", - "integrity": "sha512-s7Mbc37L/CF5Apy/cjPnalkgACmG9tTAmcIW28cIZLRLOUAze18pqhtdHryxAQhEOtKGaDAho6TriqL7/74uHw==", + "version": "4.6.22", + "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.22.tgz", + "integrity": "sha512-081tM5CBBLgTQX0Fhzp0nlZHnfgojRXweV7/d6v7LHe6QGrGBmgvUy3EIbO+R3P1uaxeGVijMvB4Ok8md9IpYQ==", "dev": true, "dependencies": { "@ensdomains/ensjs": "^2.1.0", "@truffle/blockchain-utils": "^0.1.7", - "@truffle/contract-schema": "^3.4.13", - "@truffle/debug-utils": "^6.0.48", + "@truffle/contract-schema": "^3.4.14", + "@truffle/debug-utils": "^6.0.50", "@truffle/error": "^0.2.0", - "@truffle/interface-adapter": "^0.5.32", + "@truffle/interface-adapter": "^0.5.33", "bignumber.js": "^7.2.1", "debug": "^4.3.1", "ethers": "^4.0.32", - "web3": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-utils": "1.8.2" + "web3": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-promievent": "1.10.0", + "web3-eth-abi": "1.10.0", + "web3-utils": "1.10.0" } }, "node_modules/@truffle/contract-schema": { - "version": "3.4.13", - "resolved": "https://registry.npmjs.org/@truffle/contract-schema/-/contract-schema-3.4.13.tgz", - "integrity": "sha512-emG7upuryYFrsPDbHqeASPWXL824M1tinhQwSPG0phSoa3g+RX9fUNNN/VPmF3tSkXLWUMhRnb7ehxnaCuRbZg==", + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/@truffle/contract-schema/-/contract-schema-3.4.14.tgz", + "integrity": "sha512-IwVQZG9RVNwTdn321+jbFIcky3/kZLkCtq8tqil4jZwivvmZQg8rIVC8GJ7Lkrmixl9/yTyQNL6GtIUUvkZxyA==", "dev": true, "dependencies": { "ajv": "^6.10.0", @@ -2722,391 +2680,13 @@ "integrity": "sha512-Fe0/z4WWb7IP2gBnv3l6zqP87Y0kSMs7oiSLakKJq17q3GUunrHSdioKuNspdggxkXIBhEQLhi8C+LJdwmHKWQ==", "dev": true }, - "node_modules/@truffle/contract/node_modules/eth-lib": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.2.8.tgz", - "integrity": "sha512-ArJ7x1WcWOlSpzdoTBX8vkwlkSQ85CjjifSZtV4co64vWxSV8geWfPI9x4SVYu3DSxnX4yWFVTtGL+j9DUFLNw==", - "dev": true, - "dependencies": { - "bn.js": "^4.11.6", - "elliptic": "^6.4.0", - "xhr-request-promise": "^0.1.2" - } - }, - "node_modules/@truffle/contract/node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@truffle/contract/node_modules/web3": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.8.2.tgz", - "integrity": "sha512-92h0GdEHW9wqDICQQKyG4foZBYi0OQkyg4CRml2F7XBl/NG+fu9o6J19kzfFXzSBoA4DnJXbyRgj/RHZv5LRiw==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "web3-bzz": "1.8.2", - "web3-core": "1.8.2", - "web3-eth": "1.8.2", - "web3-eth-personal": "1.8.2", - "web3-net": "1.8.2", - "web3-shh": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-bzz": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.8.2.tgz", - "integrity": "sha512-1EEnxjPnFnvNWw3XeeKuTR8PBxYd0+XWzvaLK7OJC/Go9O8llLGxrxICbKV+8cgIE0sDRBxiYx02X+6OhoAQ9w==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@types/node": "^12.12.6", - "got": "12.1.0", - "swarm-js": "^0.1.40" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-core": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.8.2.tgz", - "integrity": "sha512-DJTVEAYcNqxkqruJE+Rxp3CIv0y5AZMwPHQmOkz/cz+MM75SIzMTc0AUdXzGyTS8xMF8h3YWMQGgGEy8SBf1PQ==", - "dev": true, - "dependencies": { - "@types/bn.js": "^5.1.0", - "@types/node": "^12.12.6", - "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-requestmanager": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-core-helpers": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.8.2.tgz", - "integrity": "sha512-6B1eLlq9JFrfealZBomd1fmlq1o4A09vrCVQSa51ANoib/jllT3atZrRDr0zt1rfI7TSZTZBXdN/aTdeN99DWw==", - "dev": true, - "dependencies": { - "web3-eth-iban": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-core-method": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.8.2.tgz", - "integrity": "sha512-1qnr5mw5wVyULzLOrk4B+ryO3gfGjGd/fx8NR+J2xCGLf1e6OSjxT9vbfuQ3fErk/NjSTWWreieYWLMhaogcRA==", - "dev": true, - "dependencies": { - "@ethersproject/transactions": "^5.6.2", - "web3-core-helpers": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-core-promievent": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.8.2.tgz", - "integrity": "sha512-nvkJWDVgoOSsolJldN33tKW6bKKRJX3MCPDYMwP5SUFOA/mCzDEoI88N0JFofDTXkh1k7gOqp1pvwi9heuaxGg==", - "dev": true, - "dependencies": { - "eventemitter3": "4.0.4" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-core-requestmanager": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.8.2.tgz", - "integrity": "sha512-p1d090RYs5Mu7DK1yyc3GCBVZB/03rBtFhYFoS2EruGzOWs/5Q0grgtpwS/DScdRAm8wB8mYEBhY/RKJWF6B2g==", - "dev": true, - "dependencies": { - "util": "^0.12.5", - "web3-core-helpers": "1.8.2", - "web3-providers-http": "1.8.2", - "web3-providers-ipc": "1.8.2", - "web3-providers-ws": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-core-subscriptions": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.8.2.tgz", - "integrity": "sha512-vXQogHDmAIQcKpXvGiMddBUeP9lnKgYF64+yQJhPNE5PnWr1sAibXuIPV7mIPihpFr/n/DORRj6Wh1pUv9zaTw==", - "dev": true, - "dependencies": { - "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-core/node_modules/bignumber.js": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", - "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/@truffle/contract/node_modules/web3-eth": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.8.2.tgz", - "integrity": "sha512-JoTiWWc4F4TInpbvDUGb0WgDYJsFhuIjJlinc5ByjWD88Gvh+GKLsRjjFdbqe5YtwIGT4NymwoC5LQd1K6u/QQ==", - "dev": true, - "dependencies": { - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-eth-accounts": "1.8.2", - "web3-eth-contract": "1.8.2", - "web3-eth-ens": "1.8.2", - "web3-eth-iban": "1.8.2", - "web3-eth-personal": "1.8.2", - "web3-net": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-eth-abi": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.8.2.tgz", - "integrity": "sha512-Om9g3kaRNjqiNPAgKwGT16y+ZwtBzRe4ZJFGjLiSs6v5I7TPNF+rRMWuKnR6jq0azQZDj6rblvKFMA49/k48Og==", - "dev": true, - "dependencies": { - "@ethersproject/abi": "^5.6.3", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-eth-accounts": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.8.2.tgz", - "integrity": "sha512-c367Ij63VCz9YdyjiHHWLFtN85l6QghgwMQH2B1eM/p9Y5lTlTX7t/Eg/8+f1yoIStXbk2w/PYM2lk+IkbqdLA==", - "dev": true, - "dependencies": { - "@ethereumjs/common": "2.5.0", - "@ethereumjs/tx": "3.3.2", - "eth-lib": "0.2.8", - "ethereumjs-util": "^7.1.5", - "scrypt-js": "^3.0.1", - "uuid": "^9.0.0", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-eth-contract": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.8.2.tgz", - "integrity": "sha512-ID5A25tHTSBNwOPjiXSVzxruz006ULRIDbzWTYIFTp7NJ7vXu/kynKK2ag/ObuTqBpMbobP8nXcA9b5EDkIdQA==", - "dev": true, - "dependencies": { - "@types/bn.js": "^5.1.0", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-eth-ens": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.8.2.tgz", - "integrity": "sha512-PWph7C/CnqdWuu1+SH4U4zdrK4t2HNt0I4XzPYFdv9ugE8EuojselioPQXsVGvjql+Nt3jDLvQvggPqlMbvwRw==", - "dev": true, - "dependencies": { - "content-hash": "^2.5.2", - "eth-ens-namehash": "2.0.8", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-eth-contract": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-eth-iban": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.8.2.tgz", - "integrity": "sha512-h3vNblDWkWMuYx93Q27TAJz6lhzpP93EiC3+45D6xoz983p6si773vntoQ+H+5aZhwglBtoiBzdh7PSSOnP/xQ==", - "dev": true, - "dependencies": { - "bn.js": "^5.2.1", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-eth-iban/node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - }, - "node_modules/@truffle/contract/node_modules/web3-eth-personal": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.8.2.tgz", - "integrity": "sha512-Vg4HfwCr7doiUF/RC+Jz0wT4+cYaXcOWMAW2AHIjHX6Z7Xwa8nrURIeQgeEE62qcEHAzajyAdB1u6bJyTfuCXw==", - "dev": true, - "dependencies": { - "@types/node": "^12.12.6", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-net": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-net": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.8.2.tgz", - "integrity": "sha512-1itkDMGmbgb83Dg9nporFes9/fxsU7smJ3oRXlFkg4ZHn8YJyP1MSQFPJWWwSc+GrcCFt4O5IrUTvEkHqE3xag==", - "dev": true, - "dependencies": { - "web3-core": "1.8.2", - "web3-core-method": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-providers-http": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.8.2.tgz", - "integrity": "sha512-2xY94IIEQd16+b+vIBF4IC1p7GVaz9q4EUFscvMUjtEq4ru4Atdzjs9GP+jmcoo49p70II0UV3bqQcz0TQfVyQ==", - "dev": true, - "dependencies": { - "abortcontroller-polyfill": "^1.7.3", - "cross-fetch": "^3.1.4", - "es6-promise": "^4.2.8", - "web3-core-helpers": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-providers-ipc": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.8.2.tgz", - "integrity": "sha512-p6fqKVGFg+WiXGHWnB1hu43PbvPkDHTz4RgoEzbXugv5rtv5zfYLqm8Ba6lrJOS5ks9kGKR21a0y3NzE3u7V4w==", - "dev": true, - "dependencies": { - "oboe": "2.1.5", - "web3-core-helpers": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-providers-ws": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.8.2.tgz", - "integrity": "sha512-3s/4K+wHgbiN+Zrp9YjMq2eqAF6QGABw7wFftPdx+m5hWImV27/MoIx57c6HffNRqZXmCHnfWWFCNHHsi7wXnA==", - "dev": true, - "dependencies": { - "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.2", - "websocket": "^1.0.32" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-shh": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.8.2.tgz", - "integrity": "sha512-uZ+3MAoNcaJsXXNCDnizKJ5viBNeHOFYsCbFhV755Uu52FswzTOw6DtE7yK9nYXMtIhiSgi7nwl1RYzP8pystw==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "web3-core": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-net": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-utils": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", - "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", - "dev": true, - "dependencies": { - "bn.js": "^5.2.1", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/contract/node_modules/web3-utils/node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - }, "node_modules/@truffle/debug-utils": { - "version": "6.0.48", - "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.48.tgz", - "integrity": "sha512-HdK/7eH5EFrcTPeZVEgKaKkkzuZ4xsrH8yw+EoLEsScLsOEuQeKynY61NctjuU93voATWrYmV99Sfb/MRq2i2g==", + "version": "6.0.50", + "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.50.tgz", + "integrity": "sha512-OWdSoOsPW7/jvcO7ASBRzXDzXQNb7dg8UqwoBAI7j7UpdQoCAhz7JQsusSNiFN6g1qrxEpGzeh5iIMDq9WxO3w==", "dev": true, "dependencies": { - "@truffle/codec": "^0.14.17", + "@truffle/codec": "^0.15.1", "@trufflesuite/chromafi": "^3.0.0", "bn.js": "^5.1.3", "chalk": "^2.4.2", @@ -3127,23 +2707,14 @@ "dev": true }, "node_modules/@truffle/interface-adapter": { - "version": "0.5.32", - "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.32.tgz", - "integrity": "sha512-7Hgmdb4nJUcWtq4vvgWY7Mr2RLOTOp5FZaWyMiFmjkcEEhDlezm2QstALWAXgT0W6q7tDmDBpw3vTIFenRhHBA==", + "version": "0.5.33", + "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.33.tgz", + "integrity": "sha512-vbVcH2I8hX+wM0Xj9uAjpgxMHqfT+y6m26zSkOVvZ2wo9Ez1slaOJkK1/TZK+7nJitGZSXeJeB4purMDuADvGA==", "dev": true, "dependencies": { "bn.js": "^5.1.3", "ethers": "^4.0.32", - "web3": "1.8.2" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/bignumber.js": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", - "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", - "dev": true, - "engines": { - "node": "*" + "web3": "1.10.0" } }, "node_modules/@truffle/interface-adapter/node_modules/bn.js": { @@ -3152,369 +2723,6 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", "dev": true }, - "node_modules/@truffle/interface-adapter/node_modules/eth-lib": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.2.8.tgz", - "integrity": "sha512-ArJ7x1WcWOlSpzdoTBX8vkwlkSQ85CjjifSZtV4co64vWxSV8geWfPI9x4SVYu3DSxnX4yWFVTtGL+j9DUFLNw==", - "dev": true, - "dependencies": { - "bn.js": "^4.11.6", - "elliptic": "^6.4.0", - "xhr-request-promise": "^0.1.2" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/eth-lib/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/@truffle/interface-adapter/node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.8.2.tgz", - "integrity": "sha512-92h0GdEHW9wqDICQQKyG4foZBYi0OQkyg4CRml2F7XBl/NG+fu9o6J19kzfFXzSBoA4DnJXbyRgj/RHZv5LRiw==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "web3-bzz": "1.8.2", - "web3-core": "1.8.2", - "web3-eth": "1.8.2", - "web3-eth-personal": "1.8.2", - "web3-net": "1.8.2", - "web3-shh": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-bzz": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.8.2.tgz", - "integrity": "sha512-1EEnxjPnFnvNWw3XeeKuTR8PBxYd0+XWzvaLK7OJC/Go9O8llLGxrxICbKV+8cgIE0sDRBxiYx02X+6OhoAQ9w==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@types/node": "^12.12.6", - "got": "12.1.0", - "swarm-js": "^0.1.40" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-core": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.8.2.tgz", - "integrity": "sha512-DJTVEAYcNqxkqruJE+Rxp3CIv0y5AZMwPHQmOkz/cz+MM75SIzMTc0AUdXzGyTS8xMF8h3YWMQGgGEy8SBf1PQ==", - "dev": true, - "dependencies": { - "@types/bn.js": "^5.1.0", - "@types/node": "^12.12.6", - "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-requestmanager": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-core-helpers": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.8.2.tgz", - "integrity": "sha512-6B1eLlq9JFrfealZBomd1fmlq1o4A09vrCVQSa51ANoib/jllT3atZrRDr0zt1rfI7TSZTZBXdN/aTdeN99DWw==", - "dev": true, - "dependencies": { - "web3-eth-iban": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-core-method": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.8.2.tgz", - "integrity": "sha512-1qnr5mw5wVyULzLOrk4B+ryO3gfGjGd/fx8NR+J2xCGLf1e6OSjxT9vbfuQ3fErk/NjSTWWreieYWLMhaogcRA==", - "dev": true, - "dependencies": { - "@ethersproject/transactions": "^5.6.2", - "web3-core-helpers": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-core-promievent": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.8.2.tgz", - "integrity": "sha512-nvkJWDVgoOSsolJldN33tKW6bKKRJX3MCPDYMwP5SUFOA/mCzDEoI88N0JFofDTXkh1k7gOqp1pvwi9heuaxGg==", - "dev": true, - "dependencies": { - "eventemitter3": "4.0.4" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-core-requestmanager": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.8.2.tgz", - "integrity": "sha512-p1d090RYs5Mu7DK1yyc3GCBVZB/03rBtFhYFoS2EruGzOWs/5Q0grgtpwS/DScdRAm8wB8mYEBhY/RKJWF6B2g==", - "dev": true, - "dependencies": { - "util": "^0.12.5", - "web3-core-helpers": "1.8.2", - "web3-providers-http": "1.8.2", - "web3-providers-ipc": "1.8.2", - "web3-providers-ws": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-core-subscriptions": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.8.2.tgz", - "integrity": "sha512-vXQogHDmAIQcKpXvGiMddBUeP9lnKgYF64+yQJhPNE5PnWr1sAibXuIPV7mIPihpFr/n/DORRj6Wh1pUv9zaTw==", - "dev": true, - "dependencies": { - "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-eth": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.8.2.tgz", - "integrity": "sha512-JoTiWWc4F4TInpbvDUGb0WgDYJsFhuIjJlinc5ByjWD88Gvh+GKLsRjjFdbqe5YtwIGT4NymwoC5LQd1K6u/QQ==", - "dev": true, - "dependencies": { - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-eth-accounts": "1.8.2", - "web3-eth-contract": "1.8.2", - "web3-eth-ens": "1.8.2", - "web3-eth-iban": "1.8.2", - "web3-eth-personal": "1.8.2", - "web3-net": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-eth-abi": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.8.2.tgz", - "integrity": "sha512-Om9g3kaRNjqiNPAgKwGT16y+ZwtBzRe4ZJFGjLiSs6v5I7TPNF+rRMWuKnR6jq0azQZDj6rblvKFMA49/k48Og==", - "dev": true, - "dependencies": { - "@ethersproject/abi": "^5.6.3", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-eth-accounts": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.8.2.tgz", - "integrity": "sha512-c367Ij63VCz9YdyjiHHWLFtN85l6QghgwMQH2B1eM/p9Y5lTlTX7t/Eg/8+f1yoIStXbk2w/PYM2lk+IkbqdLA==", - "dev": true, - "dependencies": { - "@ethereumjs/common": "2.5.0", - "@ethereumjs/tx": "3.3.2", - "eth-lib": "0.2.8", - "ethereumjs-util": "^7.1.5", - "scrypt-js": "^3.0.1", - "uuid": "^9.0.0", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-eth-contract": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.8.2.tgz", - "integrity": "sha512-ID5A25tHTSBNwOPjiXSVzxruz006ULRIDbzWTYIFTp7NJ7vXu/kynKK2ag/ObuTqBpMbobP8nXcA9b5EDkIdQA==", - "dev": true, - "dependencies": { - "@types/bn.js": "^5.1.0", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-eth-ens": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.8.2.tgz", - "integrity": "sha512-PWph7C/CnqdWuu1+SH4U4zdrK4t2HNt0I4XzPYFdv9ugE8EuojselioPQXsVGvjql+Nt3jDLvQvggPqlMbvwRw==", - "dev": true, - "dependencies": { - "content-hash": "^2.5.2", - "eth-ens-namehash": "2.0.8", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-eth-contract": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-eth-iban": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.8.2.tgz", - "integrity": "sha512-h3vNblDWkWMuYx93Q27TAJz6lhzpP93EiC3+45D6xoz983p6si773vntoQ+H+5aZhwglBtoiBzdh7PSSOnP/xQ==", - "dev": true, - "dependencies": { - "bn.js": "^5.2.1", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-eth-personal": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.8.2.tgz", - "integrity": "sha512-Vg4HfwCr7doiUF/RC+Jz0wT4+cYaXcOWMAW2AHIjHX6Z7Xwa8nrURIeQgeEE62qcEHAzajyAdB1u6bJyTfuCXw==", - "dev": true, - "dependencies": { - "@types/node": "^12.12.6", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-net": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-net": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.8.2.tgz", - "integrity": "sha512-1itkDMGmbgb83Dg9nporFes9/fxsU7smJ3oRXlFkg4ZHn8YJyP1MSQFPJWWwSc+GrcCFt4O5IrUTvEkHqE3xag==", - "dev": true, - "dependencies": { - "web3-core": "1.8.2", - "web3-core-method": "1.8.2", - "web3-utils": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-providers-http": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.8.2.tgz", - "integrity": "sha512-2xY94IIEQd16+b+vIBF4IC1p7GVaz9q4EUFscvMUjtEq4ru4Atdzjs9GP+jmcoo49p70II0UV3bqQcz0TQfVyQ==", - "dev": true, - "dependencies": { - "abortcontroller-polyfill": "^1.7.3", - "cross-fetch": "^3.1.4", - "es6-promise": "^4.2.8", - "web3-core-helpers": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-providers-ipc": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.8.2.tgz", - "integrity": "sha512-p6fqKVGFg+WiXGHWnB1hu43PbvPkDHTz4RgoEzbXugv5rtv5zfYLqm8Ba6lrJOS5ks9kGKR21a0y3NzE3u7V4w==", - "dev": true, - "dependencies": { - "oboe": "2.1.5", - "web3-core-helpers": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-providers-ws": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.8.2.tgz", - "integrity": "sha512-3s/4K+wHgbiN+Zrp9YjMq2eqAF6QGABw7wFftPdx+m5hWImV27/MoIx57c6HffNRqZXmCHnfWWFCNHHsi7wXnA==", - "dev": true, - "dependencies": { - "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.2", - "websocket": "^1.0.32" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-shh": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.8.2.tgz", - "integrity": "sha512-uZ+3MAoNcaJsXXNCDnizKJ5viBNeHOFYsCbFhV755Uu52FswzTOw6DtE7yK9nYXMtIhiSgi7nwl1RYzP8pystw==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "web3-core": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-net": "1.8.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@truffle/interface-adapter/node_modules/web3-utils": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", - "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", - "dev": true, - "dependencies": { - "bn.js": "^5.2.1", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@trufflesuite/chromafi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@trufflesuite/chromafi/-/chromafi-3.0.0.tgz", @@ -3947,9 +3155,9 @@ } }, "node_modules/antlr4": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.12.0.tgz", - "integrity": "sha512-23iB5IzXJZRZeK9TigzUyrNc9pSmNqAerJRBcNq1ETrmttMWRgaYZzC561IgEO3ygKsDJTYDTozABXa4b/fTQQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.13.0.tgz", + "integrity": "sha512-zooUbt+UscjnWyOrsuY/tVFL4rwrAGwOivpQmvmUDE22hy/lUA467Rc1rcixyRwcRUIXFYBwv7+dClDSHdmmew==", "dev": true, "engines": { "node": ">=16" @@ -4392,9 +3600,9 @@ } }, "node_modules/breakword": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/breakword/-/breakword-1.0.5.tgz", - "integrity": "sha512-ex5W9DoOQ/LUEU3PMdLs9ua/CYZl1678NUkKOdUSi8Aw5F1idieaiRURCBFJCwVcrD1J8Iy3vfWSloaMwO2qFg==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/breakword/-/breakword-1.0.6.tgz", + "integrity": "sha512-yjxDAYyK/pBvws9H4xKYpLDpYKEH6CzrBPAuXq3x18I+c/2MkVtT3qAr7Oloi6Dss9qNhPVueAAVU1CSeNDIXw==", "dev": true, "dependencies": { "wcwidth": "^1.0.1" @@ -5307,32 +4515,12 @@ } }, "node_modules/cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", + "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", "dev": true, "dependencies": { - "node-fetch": "2.6.7" - } - }, - "node_modules/cross-fetch/node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node-fetch": "^2.6.11" } }, "node_modules/cross-spawn": { @@ -6144,15 +5332,15 @@ } }, "node_modules/eslint": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", - "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz", + "integrity": "sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.39.0", + "@eslint/eslintrc": "^2.0.3", + "@eslint/js": "8.41.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -6163,8 +5351,8 @@ "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.0", - "espree": "^9.5.1", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.5.2", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -6172,13 +5360,12 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", @@ -6229,9 +5416,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -6420,14 +5607,14 @@ } }, "node_modules/espree": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", - "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", "dev": true, "dependencies": { "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.0" + "eslint-visitor-keys": "^3.4.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -7514,9 +6701,9 @@ "dev": true }, "node_modules/fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, "node_modules/fast-glob": { @@ -7853,13 +7040,14 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" }, "funding": { @@ -8126,6 +7314,12 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/graphlib": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", @@ -8207,9 +7401,9 @@ } }, "node_modules/hardhat": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.14.0.tgz", - "integrity": "sha512-73jsInY4zZahMSVFurSK+5TNCJTXMv+vemvGia0Ac34Mm19fYp6vEPVGF3sucbumszsYxiTT2TbS8Ii2dsDSoQ==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.14.1.tgz", + "integrity": "sha512-H3Qp/UKyQGmPDDBSfMoSyH18rRnac90rsb0LNer+sKe6at6rxLe4D5j+M+1icqZQF02iLPjNRwc/PA8OPf757A==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.1.2", @@ -8283,9 +7477,9 @@ } }, "node_modules/hardhat-exposed": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.3.tgz", - "integrity": "sha512-AFBM3IUlSU9wkt3886kYbCSzZteB4OQt0ciehPUrCgY/Tazn6g2cxsmoIZT8O8va1R2PtXd9PRC4KZ6WiSVXOw==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.6.tgz", + "integrity": "sha512-jb03M+GolhEfjDYJyPkGJzcAs1/eZrVufMADXQDwng2iuUJD1zgzM3k1oq2YQihRxguQDsLE1nuHIn6N8o5GmQ==", "dev": true, "dependencies": { "micromatch": "^4.0.4", @@ -10784,9 +9978,9 @@ } }, "node_modules/node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", "dev": true, "dependencies": { "whatwg-url": "^5.0.0" @@ -11638,9 +10832,9 @@ ] }, "node_modules/qs": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", - "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "dev": true, "dependencies": { "side-channel": "^1.0.4" @@ -12402,9 +11596,9 @@ } }, "node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -13409,9 +12603,9 @@ } }, "node_modules/solidity-ast": { - "version": "0.4.46", - "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.46.tgz", - "integrity": "sha512-MlPZQfPhjWXqh7YxWcBGDXaPZIfMYCOHYoLEhGDWulNwEPIQQZuB7mA9eP17CU0jY/bGR4avCEUVVpvHtT2gbA==", + "version": "0.4.49", + "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.49.tgz", + "integrity": "sha512-Pr5sCAj1SFqzwFZw1HPKSq0PehlQNdM8GwKyAVYh2DOn7/cCK8LUKD1HeHnKtTgBW7hi9h4nnnan7hpAg5RhWQ==", "dev": true }, "node_modules/solidity-comments": { @@ -15353,28 +14547,28 @@ } }, "node_modules/web3": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.9.0.tgz", - "integrity": "sha512-E9IvVy/d2ozfQQsCiV+zh/LmlZGv9fQxI0UedDVjm87yOKf4AYbBNEn1iWtHveiGzAk2CEMZMUzAZzaQNSSYog==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.10.0.tgz", + "integrity": "sha512-YfKY9wSkGcM8seO+daR89oVTcbu18NsVfvOngzqMYGUU0pPSQmE57qQDvQzUeoIOHAnXEBNzrhjQJmm8ER0rng==", "dev": true, "hasInstallScript": true, "dependencies": { - "web3-bzz": "1.9.0", - "web3-core": "1.9.0", - "web3-eth": "1.9.0", - "web3-eth-personal": "1.9.0", - "web3-net": "1.9.0", - "web3-shh": "1.9.0", - "web3-utils": "1.9.0" + "web3-bzz": "1.10.0", + "web3-core": "1.10.0", + "web3-eth": "1.10.0", + "web3-eth-personal": "1.10.0", + "web3-net": "1.10.0", + "web3-shh": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-bzz": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.9.0.tgz", - "integrity": "sha512-9Zli9dikX8GdHwBb5/WPzpSVuy3EWMKY3P4EokCQra31fD7DLizqAAaTUsFwnK7xYkw5ogpHgelw9uKHHzNajg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.10.0.tgz", + "integrity": "sha512-o9IR59io3pDUsXTsps5pO5hW1D5zBmg46iNc2t4j2DkaYHNdDLwk2IP9ukoM2wg47QILfPEJYzhTfkS/CcX0KA==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -15387,56 +14581,56 @@ } }, "node_modules/web3-core": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.9.0.tgz", - "integrity": "sha512-DZ+TPmq/ZLlx4LSVzFgrHCP/QFpKDbGWO4HoquZSdu24cjk5SZ+FEU1SZB2OaK3/bgBh+25mRbmv8y56ysUu1w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.10.0.tgz", + "integrity": "sha512-fWySwqy2hn3TL89w5TM8wXF1Z2Q6frQTKHWmP0ppRQorEK8NcHJRfeMiv/mQlSKoTS1F6n/nv2uyZsixFycjYQ==", "dev": true, "dependencies": { "@types/bn.js": "^5.1.1", "@types/node": "^12.12.6", "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.9.0", - "web3-core-method": "1.9.0", - "web3-core-requestmanager": "1.9.0", - "web3-utils": "1.9.0" + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-core-requestmanager": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-helpers": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.9.0.tgz", - "integrity": "sha512-NeJzylAp9Yj9xAt2uTT+kyug3X0DLnfBdnAcGZuY6HhoNPDIfQRA9CkJjLngVRlGTLZGjNp9x9eR+RyZQgUlXg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.10.0.tgz", + "integrity": "sha512-pIxAzFDS5vnbXvfvLSpaA1tfRykAe9adw43YCKsEYQwH0gCLL0kMLkaCX3q+Q8EVmAh+e1jWL/nl9U0de1+++g==", "dev": true, "dependencies": { - "web3-eth-iban": "1.9.0", - "web3-utils": "1.9.0" + "web3-eth-iban": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-method": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.9.0.tgz", - "integrity": "sha512-sswbNsY2xRBBhGeaLt9c/eDc+0yDDhi6keUBAkgIRa9ueSx/VKzUY9HMqiV6bXDcGT2fJyejq74FfEB4lc/+/w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.10.0.tgz", + "integrity": "sha512-4R700jTLAMKDMhQ+nsVfIXvH6IGJlJzGisIfMKWAIswH31h5AZz7uDUW2YctI+HrYd+5uOAlS4OJeeT9bIpvkA==", "dev": true, "dependencies": { "@ethersproject/transactions": "^5.6.2", - "web3-core-helpers": "1.9.0", - "web3-core-promievent": "1.9.0", - "web3-core-subscriptions": "1.9.0", - "web3-utils": "1.9.0" + "web3-core-helpers": "1.10.0", + "web3-core-promievent": "1.10.0", + "web3-core-subscriptions": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-promievent": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.9.0.tgz", - "integrity": "sha512-PHG1Mn23IGwMZhnPDN8dETKypqsFbHfiyRqP+XsVMPmTHkVfzDQTCBU/c2r6hUktBDoGKut5xZQpGfhFk71KbQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.10.0.tgz", + "integrity": "sha512-68N7k5LWL5R38xRaKFrTFT2pm2jBNFaM4GioS00YjAKXRQ3KjmhijOMG3TICz6Aa5+6GDWYelDNx21YAeZ4YTg==", "dev": true, "dependencies": { "eventemitter3": "4.0.4" @@ -15446,29 +14640,29 @@ } }, "node_modules/web3-core-requestmanager": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.9.0.tgz", - "integrity": "sha512-hcJ5PCtTIJpj+8qWxoseqlCovDo94JJjTX7dZOLXgwp8ah7E3WRYozhGyZocerx+KebKyg1mCQIhkDpMwjfo9Q==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.10.0.tgz", + "integrity": "sha512-3z/JKE++Os62APml4dvBM+GAuId4h3L9ckUrj7ebEtS2AR0ixyQPbrBodgL91Sv7j7cQ3Y+hllaluqjguxvSaQ==", "dev": true, "dependencies": { "util": "^0.12.5", - "web3-core-helpers": "1.9.0", - "web3-providers-http": "1.9.0", - "web3-providers-ipc": "1.9.0", - "web3-providers-ws": "1.9.0" + "web3-core-helpers": "1.10.0", + "web3-providers-http": "1.10.0", + "web3-providers-ipc": "1.10.0", + "web3-providers-ws": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-subscriptions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.9.0.tgz", - "integrity": "sha512-MaIo29yz7hTV8X8bioclPDbHFOVuHmnbMv+D3PDH12ceJFJAXGyW8GL5KU1DYyWIj4TD1HM4WknyVA/YWBiiLA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.10.0.tgz", + "integrity": "sha512-HGm1PbDqsxejI075gxBc5OSkwymilRWZufIy9zEpnWKNmfbuv5FfHgW1/chtJP6aP3Uq2vHkvTDl3smQBb8l+g==", "dev": true, "dependencies": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.9.0" + "web3-core-helpers": "1.10.0" }, "engines": { "node": ">=8.0.0" @@ -15484,45 +14678,45 @@ } }, "node_modules/web3-eth": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.9.0.tgz", - "integrity": "sha512-c5gSWk9bLNr6VPATHmZ1n7LTIefIZQnJMzfnvkoBcIFGKJbGmsuRhv6lEXsKdAO/FlqYnSbaw3fOq1fVFiIOFQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.10.0.tgz", + "integrity": "sha512-Z5vT6slNMLPKuwRyKGbqeGYC87OAy8bOblaqRTgg94CXcn/mmqU7iPIlG4506YdcdK3x6cfEDG7B6w+jRxypKA==", "dev": true, "dependencies": { - "web3-core": "1.9.0", - "web3-core-helpers": "1.9.0", - "web3-core-method": "1.9.0", - "web3-core-subscriptions": "1.9.0", - "web3-eth-abi": "1.9.0", - "web3-eth-accounts": "1.9.0", - "web3-eth-contract": "1.9.0", - "web3-eth-ens": "1.9.0", - "web3-eth-iban": "1.9.0", - "web3-eth-personal": "1.9.0", - "web3-net": "1.9.0", - "web3-utils": "1.9.0" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-core-subscriptions": "1.10.0", + "web3-eth-abi": "1.10.0", + "web3-eth-accounts": "1.10.0", + "web3-eth-contract": "1.10.0", + "web3-eth-ens": "1.10.0", + "web3-eth-iban": "1.10.0", + "web3-eth-personal": "1.10.0", + "web3-net": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-abi": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.9.0.tgz", - "integrity": "sha512-0BLQ3FKMrzJkA930jOX3fMaybAyubk06HChclLpiR0NWmgWXm1tmBrJdkyRy2ZTZpmfuZc9xTFRfl0yZID1voA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.10.0.tgz", + "integrity": "sha512-cwS+qRBWpJ43aI9L3JS88QYPfFcSJJ3XapxOQ4j40v6mk7ATpA8CVK1vGTzpihNlOfMVRBkR95oAj7oL6aiDOg==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.6.3", - "web3-utils": "1.9.0" + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-accounts": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.9.0.tgz", - "integrity": "sha512-VeIZVevmnSll0AC1k5F/y398ZE89d1SRuYk8IewLUhL/tVAsFEsjl2SGgm0+aDcHmgPrkW+qsCJ+C7rWg/N4ZA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.10.0.tgz", + "integrity": "sha512-wiq39Uc3mOI8rw24wE2n15hboLE0E9BsQLdlmsL4Zua9diDS6B5abXG0XhFcoNsXIGMWXVZz4TOq3u4EdpXF/Q==", "dev": true, "dependencies": { "@ethereumjs/common": "2.5.0", @@ -15531,10 +14725,10 @@ "ethereumjs-util": "^7.1.5", "scrypt-js": "^3.0.1", "uuid": "^9.0.0", - "web3-core": "1.9.0", - "web3-core-helpers": "1.9.0", - "web3-core-method": "1.9.0", - "web3-utils": "1.9.0" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" @@ -15561,51 +14755,51 @@ } }, "node_modules/web3-eth-contract": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.9.0.tgz", - "integrity": "sha512-+j26hpSaEtAdUed0TN5rnc+YZOcjPxMjFX4ZBKatvFkImdbVv/tzTvcHlltubSpgb2ZLyZ89lSL6phKYwd2zNQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.10.0.tgz", + "integrity": "sha512-MIC5FOzP/+2evDksQQ/dpcXhSqa/2hFNytdl/x61IeWxhh6vlFeSjq0YVTAyIzdjwnL7nEmZpjfI6y6/Ufhy7w==", "dev": true, "dependencies": { "@types/bn.js": "^5.1.1", - "web3-core": "1.9.0", - "web3-core-helpers": "1.9.0", - "web3-core-method": "1.9.0", - "web3-core-promievent": "1.9.0", - "web3-core-subscriptions": "1.9.0", - "web3-eth-abi": "1.9.0", - "web3-utils": "1.9.0" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-core-promievent": "1.10.0", + "web3-core-subscriptions": "1.10.0", + "web3-eth-abi": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-ens": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.9.0.tgz", - "integrity": "sha512-LOJZeN+AGe9arhuExnrPPFYQr4WSxXEkpvYIlst/joOEUNLDwfndHnJIK6PI5mXaYSROBtTx6erv+HupzGo7vA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.10.0.tgz", + "integrity": "sha512-3hpGgzX3qjgxNAmqdrC2YUQMTfnZbs4GeLEmy8aCWziVwogbuqQZ+Gzdfrym45eOZodk+lmXyLuAdqkNlvkc1g==", "dev": true, "dependencies": { "content-hash": "^2.5.2", "eth-ens-namehash": "2.0.8", - "web3-core": "1.9.0", - "web3-core-helpers": "1.9.0", - "web3-core-promievent": "1.9.0", - "web3-eth-abi": "1.9.0", - "web3-eth-contract": "1.9.0", - "web3-utils": "1.9.0" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-promievent": "1.10.0", + "web3-eth-abi": "1.10.0", + "web3-eth-contract": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-iban": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.9.0.tgz", - "integrity": "sha512-jPAm77PuEs1kE/UrrBFJdPD2PN42pwfXA0gFuuw35bZezhskYML9W4QCxcqnUtceyEA4FUn7K2qTMuCk+23fog==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.10.0.tgz", + "integrity": "sha512-0l+SP3IGhInw7Q20LY3IVafYEuufo4Dn75jAHT7c2aDJsIolvf2Lc6ugHkBajlwUneGfbRQs/ccYPQ9JeMUbrg==", "dev": true, "dependencies": { "bn.js": "^5.2.1", - "web3-utils": "1.9.0" + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" @@ -15618,72 +14812,72 @@ "dev": true }, "node_modules/web3-eth-personal": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.9.0.tgz", - "integrity": "sha512-r9Ldo/luBqJlv1vCUEQnUS+C3a3ZdbYxVHyfDkj6RWMyCqqo8JE41HWE+pfa0RmB1xnGL2g8TbYcHcqItck/qg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.10.0.tgz", + "integrity": "sha512-anseKn98w/d703eWq52uNuZi7GhQeVjTC5/svrBWEKob0WZ5kPdo+EZoFN0sp5a5ubbrk/E0xSl1/M5yORMtpg==", "dev": true, "dependencies": { "@types/node": "^12.12.6", - "web3-core": "1.9.0", - "web3-core-helpers": "1.9.0", - "web3-core-method": "1.9.0", - "web3-net": "1.9.0", - "web3-utils": "1.9.0" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-net": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-net": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.9.0.tgz", - "integrity": "sha512-L+fDZFgrLM5Y15aonl2q6L+RvfaImAngmC0Jv45hV2FJ5IfRT0/2ob9etxZmvEBWvOpbqSvghfOhJIT3XZ37Pg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.10.0.tgz", + "integrity": "sha512-NLH/N3IshYWASpxk4/18Ge6n60GEvWBVeM8inx2dmZJVmRI6SJIlUxbL8jySgiTn3MMZlhbdvrGo8fpUW7a1GA==", "dev": true, "dependencies": { - "web3-core": "1.9.0", - "web3-core-method": "1.9.0", - "web3-utils": "1.9.0" + "web3-core": "1.10.0", + "web3-core-method": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-providers-http": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.9.0.tgz", - "integrity": "sha512-5+dMNDAE0rRFz6SJpfnBqlVi2J5bB/Ivr2SanMt2YUrkxW5t8betZbzVwRkTbwtUvkqgj3xeUQzqpOttiv+IqQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.10.0.tgz", + "integrity": "sha512-eNr965YB8a9mLiNrkjAWNAPXgmQWfpBfkkn7tpEFlghfww0u3I0tktMZiaToJVcL2+Xq+81cxbkpeWJ5XQDwOA==", "dev": true, "dependencies": { "abortcontroller-polyfill": "^1.7.3", "cross-fetch": "^3.1.4", "es6-promise": "^4.2.8", - "web3-core-helpers": "1.9.0" + "web3-core-helpers": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-providers-ipc": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.9.0.tgz", - "integrity": "sha512-cPXU93Du40HCylvjaa5x62DbnGqH+86HpK/+kMcFIzF6sDUBhKpag2tSbYhGbj7GMpfkmDTUiiMLdWnFV6+uBA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.10.0.tgz", + "integrity": "sha512-OfXG1aWN8L1OUqppshzq8YISkWrYHaATW9H8eh0p89TlWMc1KZOL9vttBuaBEi96D/n0eYDn2trzt22bqHWfXA==", "dev": true, "dependencies": { "oboe": "2.1.5", - "web3-core-helpers": "1.9.0" + "web3-core-helpers": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-providers-ws": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.9.0.tgz", - "integrity": "sha512-JRVsnQZ7j2k1a2yzBNHe39xqk1ijOv01dfIBFw52VeEkSRzvrOcsPIM/ttSyBuJqt70ntMxXY0ekCrqfleKH/w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.10.0.tgz", + "integrity": "sha512-sK0fNcglW36yD5xjnjtSGBnEtf59cbw4vZzJ+CmOWIKGIR96mP5l684g0WD0Eo+f4NQc2anWWXG74lRc9OVMCQ==", "dev": true, "dependencies": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.9.0", + "web3-core-helpers": "1.10.0", "websocket": "^1.0.32" }, "engines": { @@ -15691,25 +14885,25 @@ } }, "node_modules/web3-shh": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.9.0.tgz", - "integrity": "sha512-bIBZlralgz4ICCrwkefB2nPPJWfx28NuHIpjB7d9ADKynElubQuqudYhKtSEkKXACuME/BJm0pIFJcJs/gDnMg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.10.0.tgz", + "integrity": "sha512-uNUUuNsO2AjX41GJARV9zJibs11eq6HtOe6Wr0FtRUcj8SN6nHeYIzwstAvJ4fXA53gRqFMTxdntHEt9aXVjpg==", "dev": true, "hasInstallScript": true, "dependencies": { - "web3-core": "1.9.0", - "web3-core-method": "1.9.0", - "web3-core-subscriptions": "1.9.0", - "web3-net": "1.9.0" + "web3-core": "1.10.0", + "web3-core-method": "1.10.0", + "web3-core-subscriptions": "1.10.0", + "web3-net": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-utils": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.9.0.tgz", - "integrity": "sha512-p++69rCNNfu2jM9n5+VD/g26l+qkEOQ1m6cfRQCbH8ZRrtquTmrirJMgTmyOoax5a5XRYOuws14aypCOs51pdQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.0.tgz", + "integrity": "sha512-kSaCM0uMcZTNUSmn5vMEhlo02RObGNRRCkdX0V9UTAU0+lrvn0HSaudyCo6CQzuXUsnuY2ERJGCGPfeWmv19Rg==", "dev": true, "dependencies": { "bn.js": "^5.2.1", @@ -16271,9 +15465,9 @@ } }, "@babel/runtime": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", - "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.3.tgz", + "integrity": "sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==", "dev": true, "requires": { "regenerator-runtime": "^0.13.11" @@ -16698,14 +15892,14 @@ "dev": true }, "@eslint/eslintrc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", - "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.1", + "espree": "^9.5.2", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -16732,9 +15926,9 @@ } }, "@eslint/js": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", - "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.41.0.tgz", + "integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==", "dev": true }, "@ethereumjs/common": { @@ -17872,9 +17066,9 @@ } }, "@openzeppelin/upgrades-core": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.25.0.tgz", - "integrity": "sha512-vSxOSm1k+P156nNm15ydhOmSPGC37mnl092FMVOH+eGaoqLjr2Za6ULVjDMFzvMnG+sGE1UoDOqBNPfTm0ch8w==", + "version": "1.26.2", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.26.2.tgz", + "integrity": "sha512-TJORrgyun5qflPos/47P3j61gDw+7W+tEirSBOYRxfVL1WGjX1n8iaLrijPIqzyeS1MKguN1nckAMspQ4SKrxw==", "dev": true, "requires": { "cbor": "^8.0.0", @@ -18070,37 +17264,14 @@ } }, "@truffle/abi-utils": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@truffle/abi-utils/-/abi-utils-0.3.9.tgz", - "integrity": "sha512-G5dqgwRHx5zwlXjz3QT8OJVfB2cOqWwD6DwKso0KttUt/zejhCjnkKq72rSgyeLMkz7wBB9ERLOsupLBILM8MA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@truffle/abi-utils/-/abi-utils-1.0.0.tgz", + "integrity": "sha512-h1wGFB28YfByAWm/uBeMCwqDlGsrcMYTumLC/sB/qYhHisi1LK6tV47FEF7zKyf6Al2CtsO28v02+wfLXbUVRg==", "dev": true, "requires": { "change-case": "3.0.2", "fast-check": "3.1.1", - "web3-utils": "1.8.2" - }, - "dependencies": { - "bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - }, - "web3-utils": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", - "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", - "dev": true, - "requires": { - "bn.js": "^5.2.1", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" - } - } + "web3-utils": "1.10.0" } }, "@truffle/blockchain-utils": { @@ -18110,13 +17281,13 @@ "dev": true }, "@truffle/codec": { - "version": "0.14.17", - "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.14.17.tgz", - "integrity": "sha512-kD4dD86huLeaBEq5R8D1zleJEu6NsXbyYLdXl1V1TKdiO8odw5CBC6Y/+wdu5d3t1dyEYrTbhn1dqknZa52pmw==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.15.1.tgz", + "integrity": "sha512-OBANcmefxEXLApWl/uU1SOHQJixO8pDaRTybP0YMvxPhgyj7G7+wC+fUvnZdmrTJD2WJFLuoYvZbxILmycEvPg==", "dev": true, "requires": { - "@truffle/abi-utils": "^0.3.9", - "@truffle/compile-common": "^0.9.4", + "@truffle/abi-utils": "^1.0.0", + "@truffle/compile-common": "^0.9.5", "big.js": "^6.0.3", "bn.js": "^5.1.3", "cbor": "^5.2.0", @@ -18124,7 +17295,7 @@ "lodash": "^4.17.21", "semver": "7.3.7", "utf8": "^3.0.0", - "web3-utils": "1.8.2" + "web3-utils": "1.10.0" }, "dependencies": { "bignumber.js": { @@ -18173,21 +17344,6 @@ "lru-cache": "^6.0.0" } }, - "web3-utils": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", - "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", - "dev": true, - "requires": { - "bn.js": "^5.2.1", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" - } - }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -18197,9 +17353,9 @@ } }, "@truffle/compile-common": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/@truffle/compile-common/-/compile-common-0.9.4.tgz", - "integrity": "sha512-mnqJB/hLiPHNf+WKwt/2MH6lv34xSG/SFCib7+ckAklutUqVLeFo8EwQxinuHNkU7LY0C+YgZXhK1WTCO5YRJQ==", + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@truffle/compile-common/-/compile-common-0.9.5.tgz", + "integrity": "sha512-qOIT7nYzQsrnpjk8LurKE6EYYvvJIk3rCHfn+xed88aG6F1l4WYtkUKl+Dcwgxgv3LH0khcQOpjqkXK5kUIJ8A==", "dev": true, "requires": { "@truffle/error": "^0.2.0", @@ -18215,25 +17371,25 @@ } }, "@truffle/contract": { - "version": "4.6.20", - "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.20.tgz", - "integrity": "sha512-s7Mbc37L/CF5Apy/cjPnalkgACmG9tTAmcIW28cIZLRLOUAze18pqhtdHryxAQhEOtKGaDAho6TriqL7/74uHw==", + "version": "4.6.22", + "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.22.tgz", + "integrity": "sha512-081tM5CBBLgTQX0Fhzp0nlZHnfgojRXweV7/d6v7LHe6QGrGBmgvUy3EIbO+R3P1uaxeGVijMvB4Ok8md9IpYQ==", "dev": true, "requires": { "@ensdomains/ensjs": "^2.1.0", "@truffle/blockchain-utils": "^0.1.7", - "@truffle/contract-schema": "^3.4.13", - "@truffle/debug-utils": "^6.0.48", + "@truffle/contract-schema": "^3.4.14", + "@truffle/debug-utils": "^6.0.50", "@truffle/error": "^0.2.0", - "@truffle/interface-adapter": "^0.5.32", + "@truffle/interface-adapter": "^0.5.33", "bignumber.js": "^7.2.1", "debug": "^4.3.1", "ethers": "^4.0.32", - "web3": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-utils": "1.8.2" + "web3": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-promievent": "1.10.0", + "web3-eth-abi": "1.10.0", + "web3-utils": "1.10.0" }, "dependencies": { "@truffle/error": { @@ -18241,325 +17397,13 @@ "resolved": "https://registry.npmjs.org/@truffle/error/-/error-0.2.0.tgz", "integrity": "sha512-Fe0/z4WWb7IP2gBnv3l6zqP87Y0kSMs7oiSLakKJq17q3GUunrHSdioKuNspdggxkXIBhEQLhi8C+LJdwmHKWQ==", "dev": true - }, - "eth-lib": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.2.8.tgz", - "integrity": "sha512-ArJ7x1WcWOlSpzdoTBX8vkwlkSQ85CjjifSZtV4co64vWxSV8geWfPI9x4SVYu3DSxnX4yWFVTtGL+j9DUFLNw==", - "dev": true, - "requires": { - "bn.js": "^4.11.6", - "elliptic": "^6.4.0", - "xhr-request-promise": "^0.1.2" - } - }, - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "dev": true - }, - "web3": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.8.2.tgz", - "integrity": "sha512-92h0GdEHW9wqDICQQKyG4foZBYi0OQkyg4CRml2F7XBl/NG+fu9o6J19kzfFXzSBoA4DnJXbyRgj/RHZv5LRiw==", - "dev": true, - "requires": { - "web3-bzz": "1.8.2", - "web3-core": "1.8.2", - "web3-eth": "1.8.2", - "web3-eth-personal": "1.8.2", - "web3-net": "1.8.2", - "web3-shh": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-bzz": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.8.2.tgz", - "integrity": "sha512-1EEnxjPnFnvNWw3XeeKuTR8PBxYd0+XWzvaLK7OJC/Go9O8llLGxrxICbKV+8cgIE0sDRBxiYx02X+6OhoAQ9w==", - "dev": true, - "requires": { - "@types/node": "^12.12.6", - "got": "12.1.0", - "swarm-js": "^0.1.40" - } - }, - "web3-core": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.8.2.tgz", - "integrity": "sha512-DJTVEAYcNqxkqruJE+Rxp3CIv0y5AZMwPHQmOkz/cz+MM75SIzMTc0AUdXzGyTS8xMF8h3YWMQGgGEy8SBf1PQ==", - "dev": true, - "requires": { - "@types/bn.js": "^5.1.0", - "@types/node": "^12.12.6", - "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-requestmanager": "1.8.2", - "web3-utils": "1.8.2" - }, - "dependencies": { - "bignumber.js": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", - "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", - "dev": true - } - } - }, - "web3-core-helpers": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.8.2.tgz", - "integrity": "sha512-6B1eLlq9JFrfealZBomd1fmlq1o4A09vrCVQSa51ANoib/jllT3atZrRDr0zt1rfI7TSZTZBXdN/aTdeN99DWw==", - "dev": true, - "requires": { - "web3-eth-iban": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-core-method": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.8.2.tgz", - "integrity": "sha512-1qnr5mw5wVyULzLOrk4B+ryO3gfGjGd/fx8NR+J2xCGLf1e6OSjxT9vbfuQ3fErk/NjSTWWreieYWLMhaogcRA==", - "dev": true, - "requires": { - "@ethersproject/transactions": "^5.6.2", - "web3-core-helpers": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-core-promievent": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.8.2.tgz", - "integrity": "sha512-nvkJWDVgoOSsolJldN33tKW6bKKRJX3MCPDYMwP5SUFOA/mCzDEoI88N0JFofDTXkh1k7gOqp1pvwi9heuaxGg==", - "dev": true, - "requires": { - "eventemitter3": "4.0.4" - } - }, - "web3-core-requestmanager": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.8.2.tgz", - "integrity": "sha512-p1d090RYs5Mu7DK1yyc3GCBVZB/03rBtFhYFoS2EruGzOWs/5Q0grgtpwS/DScdRAm8wB8mYEBhY/RKJWF6B2g==", - "dev": true, - "requires": { - "util": "^0.12.5", - "web3-core-helpers": "1.8.2", - "web3-providers-http": "1.8.2", - "web3-providers-ipc": "1.8.2", - "web3-providers-ws": "1.8.2" - } - }, - "web3-core-subscriptions": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.8.2.tgz", - "integrity": "sha512-vXQogHDmAIQcKpXvGiMddBUeP9lnKgYF64+yQJhPNE5PnWr1sAibXuIPV7mIPihpFr/n/DORRj6Wh1pUv9zaTw==", - "dev": true, - "requires": { - "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.2" - } - }, - "web3-eth": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.8.2.tgz", - "integrity": "sha512-JoTiWWc4F4TInpbvDUGb0WgDYJsFhuIjJlinc5ByjWD88Gvh+GKLsRjjFdbqe5YtwIGT4NymwoC5LQd1K6u/QQ==", - "dev": true, - "requires": { - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-eth-accounts": "1.8.2", - "web3-eth-contract": "1.8.2", - "web3-eth-ens": "1.8.2", - "web3-eth-iban": "1.8.2", - "web3-eth-personal": "1.8.2", - "web3-net": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-eth-abi": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.8.2.tgz", - "integrity": "sha512-Om9g3kaRNjqiNPAgKwGT16y+ZwtBzRe4ZJFGjLiSs6v5I7TPNF+rRMWuKnR6jq0azQZDj6rblvKFMA49/k48Og==", - "dev": true, - "requires": { - "@ethersproject/abi": "^5.6.3", - "web3-utils": "1.8.2" - } - }, - "web3-eth-accounts": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.8.2.tgz", - "integrity": "sha512-c367Ij63VCz9YdyjiHHWLFtN85l6QghgwMQH2B1eM/p9Y5lTlTX7t/Eg/8+f1yoIStXbk2w/PYM2lk+IkbqdLA==", - "dev": true, - "requires": { - "@ethereumjs/common": "2.5.0", - "@ethereumjs/tx": "3.3.2", - "eth-lib": "0.2.8", - "ethereumjs-util": "^7.1.5", - "scrypt-js": "^3.0.1", - "uuid": "^9.0.0", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-eth-contract": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.8.2.tgz", - "integrity": "sha512-ID5A25tHTSBNwOPjiXSVzxruz006ULRIDbzWTYIFTp7NJ7vXu/kynKK2ag/ObuTqBpMbobP8nXcA9b5EDkIdQA==", - "dev": true, - "requires": { - "@types/bn.js": "^5.1.0", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-eth-ens": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.8.2.tgz", - "integrity": "sha512-PWph7C/CnqdWuu1+SH4U4zdrK4t2HNt0I4XzPYFdv9ugE8EuojselioPQXsVGvjql+Nt3jDLvQvggPqlMbvwRw==", - "dev": true, - "requires": { - "content-hash": "^2.5.2", - "eth-ens-namehash": "2.0.8", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-eth-contract": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-eth-iban": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.8.2.tgz", - "integrity": "sha512-h3vNblDWkWMuYx93Q27TAJz6lhzpP93EiC3+45D6xoz983p6si773vntoQ+H+5aZhwglBtoiBzdh7PSSOnP/xQ==", - "dev": true, - "requires": { - "bn.js": "^5.2.1", - "web3-utils": "1.8.2" - }, - "dependencies": { - "bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - } - } - }, - "web3-eth-personal": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.8.2.tgz", - "integrity": "sha512-Vg4HfwCr7doiUF/RC+Jz0wT4+cYaXcOWMAW2AHIjHX6Z7Xwa8nrURIeQgeEE62qcEHAzajyAdB1u6bJyTfuCXw==", - "dev": true, - "requires": { - "@types/node": "^12.12.6", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-net": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-net": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.8.2.tgz", - "integrity": "sha512-1itkDMGmbgb83Dg9nporFes9/fxsU7smJ3oRXlFkg4ZHn8YJyP1MSQFPJWWwSc+GrcCFt4O5IrUTvEkHqE3xag==", - "dev": true, - "requires": { - "web3-core": "1.8.2", - "web3-core-method": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-providers-http": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.8.2.tgz", - "integrity": "sha512-2xY94IIEQd16+b+vIBF4IC1p7GVaz9q4EUFscvMUjtEq4ru4Atdzjs9GP+jmcoo49p70II0UV3bqQcz0TQfVyQ==", - "dev": true, - "requires": { - "abortcontroller-polyfill": "^1.7.3", - "cross-fetch": "^3.1.4", - "es6-promise": "^4.2.8", - "web3-core-helpers": "1.8.2" - } - }, - "web3-providers-ipc": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.8.2.tgz", - "integrity": "sha512-p6fqKVGFg+WiXGHWnB1hu43PbvPkDHTz4RgoEzbXugv5rtv5zfYLqm8Ba6lrJOS5ks9kGKR21a0y3NzE3u7V4w==", - "dev": true, - "requires": { - "oboe": "2.1.5", - "web3-core-helpers": "1.8.2" - } - }, - "web3-providers-ws": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.8.2.tgz", - "integrity": "sha512-3s/4K+wHgbiN+Zrp9YjMq2eqAF6QGABw7wFftPdx+m5hWImV27/MoIx57c6HffNRqZXmCHnfWWFCNHHsi7wXnA==", - "dev": true, - "requires": { - "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.2", - "websocket": "^1.0.32" - } - }, - "web3-shh": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.8.2.tgz", - "integrity": "sha512-uZ+3MAoNcaJsXXNCDnizKJ5viBNeHOFYsCbFhV755Uu52FswzTOw6DtE7yK9nYXMtIhiSgi7nwl1RYzP8pystw==", - "dev": true, - "requires": { - "web3-core": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-net": "1.8.2" - } - }, - "web3-utils": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", - "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", - "dev": true, - "requires": { - "bn.js": "^5.2.1", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" - }, - "dependencies": { - "bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - } - } } } }, "@truffle/contract-schema": { - "version": "3.4.13", - "resolved": "https://registry.npmjs.org/@truffle/contract-schema/-/contract-schema-3.4.13.tgz", - "integrity": "sha512-emG7upuryYFrsPDbHqeASPWXL824M1tinhQwSPG0phSoa3g+RX9fUNNN/VPmF3tSkXLWUMhRnb7ehxnaCuRbZg==", + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/@truffle/contract-schema/-/contract-schema-3.4.14.tgz", + "integrity": "sha512-IwVQZG9RVNwTdn321+jbFIcky3/kZLkCtq8tqil4jZwivvmZQg8rIVC8GJ7Lkrmixl9/yTyQNL6GtIUUvkZxyA==", "dev": true, "requires": { "ajv": "^6.10.0", @@ -18567,12 +17411,12 @@ } }, "@truffle/debug-utils": { - "version": "6.0.48", - "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.48.tgz", - "integrity": "sha512-HdK/7eH5EFrcTPeZVEgKaKkkzuZ4xsrH8yw+EoLEsScLsOEuQeKynY61NctjuU93voATWrYmV99Sfb/MRq2i2g==", + "version": "6.0.50", + "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.50.tgz", + "integrity": "sha512-OWdSoOsPW7/jvcO7ASBRzXDzXQNb7dg8UqwoBAI7j7UpdQoCAhz7JQsusSNiFN6g1qrxEpGzeh5iIMDq9WxO3w==", "dev": true, "requires": { - "@truffle/codec": "^0.14.17", + "@truffle/codec": "^0.15.1", "@trufflesuite/chromafi": "^3.0.0", "bn.js": "^5.1.3", "chalk": "^2.4.2", @@ -18595,323 +17439,21 @@ "dev": true }, "@truffle/interface-adapter": { - "version": "0.5.32", - "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.32.tgz", - "integrity": "sha512-7Hgmdb4nJUcWtq4vvgWY7Mr2RLOTOp5FZaWyMiFmjkcEEhDlezm2QstALWAXgT0W6q7tDmDBpw3vTIFenRhHBA==", + "version": "0.5.33", + "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.33.tgz", + "integrity": "sha512-vbVcH2I8hX+wM0Xj9uAjpgxMHqfT+y6m26zSkOVvZ2wo9Ez1slaOJkK1/TZK+7nJitGZSXeJeB4purMDuADvGA==", "dev": true, "requires": { "bn.js": "^5.1.3", "ethers": "^4.0.32", - "web3": "1.8.2" + "web3": "1.10.0" }, "dependencies": { - "bignumber.js": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", - "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", - "dev": true - }, "bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", "dev": true - }, - "eth-lib": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.2.8.tgz", - "integrity": "sha512-ArJ7x1WcWOlSpzdoTBX8vkwlkSQ85CjjifSZtV4co64vWxSV8geWfPI9x4SVYu3DSxnX4yWFVTtGL+j9DUFLNw==", - "dev": true, - "requires": { - "bn.js": "^4.11.6", - "elliptic": "^6.4.0", - "xhr-request-promise": "^0.1.2" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "dev": true - }, - "web3": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.8.2.tgz", - "integrity": "sha512-92h0GdEHW9wqDICQQKyG4foZBYi0OQkyg4CRml2F7XBl/NG+fu9o6J19kzfFXzSBoA4DnJXbyRgj/RHZv5LRiw==", - "dev": true, - "requires": { - "web3-bzz": "1.8.2", - "web3-core": "1.8.2", - "web3-eth": "1.8.2", - "web3-eth-personal": "1.8.2", - "web3-net": "1.8.2", - "web3-shh": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-bzz": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.8.2.tgz", - "integrity": "sha512-1EEnxjPnFnvNWw3XeeKuTR8PBxYd0+XWzvaLK7OJC/Go9O8llLGxrxICbKV+8cgIE0sDRBxiYx02X+6OhoAQ9w==", - "dev": true, - "requires": { - "@types/node": "^12.12.6", - "got": "12.1.0", - "swarm-js": "^0.1.40" - } - }, - "web3-core": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.8.2.tgz", - "integrity": "sha512-DJTVEAYcNqxkqruJE+Rxp3CIv0y5AZMwPHQmOkz/cz+MM75SIzMTc0AUdXzGyTS8xMF8h3YWMQGgGEy8SBf1PQ==", - "dev": true, - "requires": { - "@types/bn.js": "^5.1.0", - "@types/node": "^12.12.6", - "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-requestmanager": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-core-helpers": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.8.2.tgz", - "integrity": "sha512-6B1eLlq9JFrfealZBomd1fmlq1o4A09vrCVQSa51ANoib/jllT3atZrRDr0zt1rfI7TSZTZBXdN/aTdeN99DWw==", - "dev": true, - "requires": { - "web3-eth-iban": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-core-method": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.8.2.tgz", - "integrity": "sha512-1qnr5mw5wVyULzLOrk4B+ryO3gfGjGd/fx8NR+J2xCGLf1e6OSjxT9vbfuQ3fErk/NjSTWWreieYWLMhaogcRA==", - "dev": true, - "requires": { - "@ethersproject/transactions": "^5.6.2", - "web3-core-helpers": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-core-promievent": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.8.2.tgz", - "integrity": "sha512-nvkJWDVgoOSsolJldN33tKW6bKKRJX3MCPDYMwP5SUFOA/mCzDEoI88N0JFofDTXkh1k7gOqp1pvwi9heuaxGg==", - "dev": true, - "requires": { - "eventemitter3": "4.0.4" - } - }, - "web3-core-requestmanager": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.8.2.tgz", - "integrity": "sha512-p1d090RYs5Mu7DK1yyc3GCBVZB/03rBtFhYFoS2EruGzOWs/5Q0grgtpwS/DScdRAm8wB8mYEBhY/RKJWF6B2g==", - "dev": true, - "requires": { - "util": "^0.12.5", - "web3-core-helpers": "1.8.2", - "web3-providers-http": "1.8.2", - "web3-providers-ipc": "1.8.2", - "web3-providers-ws": "1.8.2" - } - }, - "web3-core-subscriptions": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.8.2.tgz", - "integrity": "sha512-vXQogHDmAIQcKpXvGiMddBUeP9lnKgYF64+yQJhPNE5PnWr1sAibXuIPV7mIPihpFr/n/DORRj6Wh1pUv9zaTw==", - "dev": true, - "requires": { - "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.2" - } - }, - "web3-eth": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.8.2.tgz", - "integrity": "sha512-JoTiWWc4F4TInpbvDUGb0WgDYJsFhuIjJlinc5ByjWD88Gvh+GKLsRjjFdbqe5YtwIGT4NymwoC5LQd1K6u/QQ==", - "dev": true, - "requires": { - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-eth-accounts": "1.8.2", - "web3-eth-contract": "1.8.2", - "web3-eth-ens": "1.8.2", - "web3-eth-iban": "1.8.2", - "web3-eth-personal": "1.8.2", - "web3-net": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-eth-abi": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.8.2.tgz", - "integrity": "sha512-Om9g3kaRNjqiNPAgKwGT16y+ZwtBzRe4ZJFGjLiSs6v5I7TPNF+rRMWuKnR6jq0azQZDj6rblvKFMA49/k48Og==", - "dev": true, - "requires": { - "@ethersproject/abi": "^5.6.3", - "web3-utils": "1.8.2" - } - }, - "web3-eth-accounts": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.8.2.tgz", - "integrity": "sha512-c367Ij63VCz9YdyjiHHWLFtN85l6QghgwMQH2B1eM/p9Y5lTlTX7t/Eg/8+f1yoIStXbk2w/PYM2lk+IkbqdLA==", - "dev": true, - "requires": { - "@ethereumjs/common": "2.5.0", - "@ethereumjs/tx": "3.3.2", - "eth-lib": "0.2.8", - "ethereumjs-util": "^7.1.5", - "scrypt-js": "^3.0.1", - "uuid": "^9.0.0", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-eth-contract": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.8.2.tgz", - "integrity": "sha512-ID5A25tHTSBNwOPjiXSVzxruz006ULRIDbzWTYIFTp7NJ7vXu/kynKK2ag/ObuTqBpMbobP8nXcA9b5EDkIdQA==", - "dev": true, - "requires": { - "@types/bn.js": "^5.1.0", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-eth-ens": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.8.2.tgz", - "integrity": "sha512-PWph7C/CnqdWuu1+SH4U4zdrK4t2HNt0I4XzPYFdv9ugE8EuojselioPQXsVGvjql+Nt3jDLvQvggPqlMbvwRw==", - "dev": true, - "requires": { - "content-hash": "^2.5.2", - "eth-ens-namehash": "2.0.8", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-eth-contract": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-eth-iban": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.8.2.tgz", - "integrity": "sha512-h3vNblDWkWMuYx93Q27TAJz6lhzpP93EiC3+45D6xoz983p6si773vntoQ+H+5aZhwglBtoiBzdh7PSSOnP/xQ==", - "dev": true, - "requires": { - "bn.js": "^5.2.1", - "web3-utils": "1.8.2" - } - }, - "web3-eth-personal": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.8.2.tgz", - "integrity": "sha512-Vg4HfwCr7doiUF/RC+Jz0wT4+cYaXcOWMAW2AHIjHX6Z7Xwa8nrURIeQgeEE62qcEHAzajyAdB1u6bJyTfuCXw==", - "dev": true, - "requires": { - "@types/node": "^12.12.6", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-net": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-net": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.8.2.tgz", - "integrity": "sha512-1itkDMGmbgb83Dg9nporFes9/fxsU7smJ3oRXlFkg4ZHn8YJyP1MSQFPJWWwSc+GrcCFt4O5IrUTvEkHqE3xag==", - "dev": true, - "requires": { - "web3-core": "1.8.2", - "web3-core-method": "1.8.2", - "web3-utils": "1.8.2" - } - }, - "web3-providers-http": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.8.2.tgz", - "integrity": "sha512-2xY94IIEQd16+b+vIBF4IC1p7GVaz9q4EUFscvMUjtEq4ru4Atdzjs9GP+jmcoo49p70II0UV3bqQcz0TQfVyQ==", - "dev": true, - "requires": { - "abortcontroller-polyfill": "^1.7.3", - "cross-fetch": "^3.1.4", - "es6-promise": "^4.2.8", - "web3-core-helpers": "1.8.2" - } - }, - "web3-providers-ipc": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.8.2.tgz", - "integrity": "sha512-p6fqKVGFg+WiXGHWnB1hu43PbvPkDHTz4RgoEzbXugv5rtv5zfYLqm8Ba6lrJOS5ks9kGKR21a0y3NzE3u7V4w==", - "dev": true, - "requires": { - "oboe": "2.1.5", - "web3-core-helpers": "1.8.2" - } - }, - "web3-providers-ws": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.8.2.tgz", - "integrity": "sha512-3s/4K+wHgbiN+Zrp9YjMq2eqAF6QGABw7wFftPdx+m5hWImV27/MoIx57c6HffNRqZXmCHnfWWFCNHHsi7wXnA==", - "dev": true, - "requires": { - "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.2", - "websocket": "^1.0.32" - } - }, - "web3-shh": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.8.2.tgz", - "integrity": "sha512-uZ+3MAoNcaJsXXNCDnizKJ5viBNeHOFYsCbFhV755Uu52FswzTOw6DtE7yK9nYXMtIhiSgi7nwl1RYzP8pystw==", - "dev": true, - "requires": { - "web3-core": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-net": "1.8.2" - } - }, - "web3-utils": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", - "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", - "dev": true, - "requires": { - "bn.js": "^5.2.1", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" - } } } }, @@ -19280,9 +17822,9 @@ } }, "antlr4": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.12.0.tgz", - "integrity": "sha512-23iB5IzXJZRZeK9TigzUyrNc9pSmNqAerJRBcNq1ETrmttMWRgaYZzC561IgEO3ygKsDJTYDTozABXa4b/fTQQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.13.0.tgz", + "integrity": "sha512-zooUbt+UscjnWyOrsuY/tVFL4rwrAGwOivpQmvmUDE22hy/lUA467Rc1rcixyRwcRUIXFYBwv7+dClDSHdmmew==", "dev": true }, "antlr4ts": { @@ -19629,9 +18171,9 @@ } }, "breakword": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/breakword/-/breakword-1.0.5.tgz", - "integrity": "sha512-ex5W9DoOQ/LUEU3PMdLs9ua/CYZl1678NUkKOdUSi8Aw5F1idieaiRURCBFJCwVcrD1J8Iy3vfWSloaMwO2qFg==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/breakword/-/breakword-1.0.6.tgz", + "integrity": "sha512-yjxDAYyK/pBvws9H4xKYpLDpYKEH6CzrBPAuXq3x18I+c/2MkVtT3qAr7Oloi6Dss9qNhPVueAAVU1CSeNDIXw==", "dev": true, "requires": { "wcwidth": "^1.0.1" @@ -20365,23 +18907,12 @@ } }, "cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", + "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", "dev": true, "requires": { - "node-fetch": "2.6.7" - }, - "dependencies": { - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, - "requires": { - "whatwg-url": "^5.0.0" - } - } + "node-fetch": "^2.6.11" } }, "cross-spawn": { @@ -21007,15 +19538,15 @@ } }, "eslint": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", - "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz", + "integrity": "sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.39.0", + "@eslint/eslintrc": "^2.0.3", + "@eslint/js": "8.41.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -21026,8 +19557,8 @@ "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.0", - "espree": "^9.5.1", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.5.2", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -21035,13 +19566,12 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", @@ -21196,20 +19726,20 @@ } }, "eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", "dev": true }, "espree": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", - "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", "dev": true, "requires": { "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.0" + "eslint-visitor-keys": "^3.4.1" } }, "esprima": { @@ -22132,9 +20662,9 @@ "dev": true }, "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, "fast-glob": { @@ -22401,13 +20931,14 @@ "dev": true }, "get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" } }, @@ -22609,6 +21140,12 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "graphlib": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", @@ -22668,9 +21205,9 @@ "dev": true }, "hardhat": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.14.0.tgz", - "integrity": "sha512-73jsInY4zZahMSVFurSK+5TNCJTXMv+vemvGia0Ac34Mm19fYp6vEPVGF3sucbumszsYxiTT2TbS8Ii2dsDSoQ==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.14.1.tgz", + "integrity": "sha512-H3Qp/UKyQGmPDDBSfMoSyH18rRnac90rsb0LNer+sKe6at6rxLe4D5j+M+1icqZQF02iLPjNRwc/PA8OPf757A==", "dev": true, "requires": { "@ethersproject/abi": "^5.1.2", @@ -22883,9 +21420,9 @@ } }, "hardhat-exposed": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.3.tgz", - "integrity": "sha512-AFBM3IUlSU9wkt3886kYbCSzZteB4OQt0ciehPUrCgY/Tazn6g2cxsmoIZT8O8va1R2PtXd9PRC4KZ6WiSVXOw==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.6.tgz", + "integrity": "sha512-jb03M+GolhEfjDYJyPkGJzcAs1/eZrVufMADXQDwng2iuUJD1zgzM3k1oq2YQihRxguQDsLE1nuHIn6N8o5GmQ==", "dev": true, "requires": { "micromatch": "^4.0.4", @@ -24653,9 +23190,9 @@ } }, "node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", "dev": true, "requires": { "whatwg-url": "^5.0.0" @@ -25288,9 +23825,9 @@ "dev": true }, "qs": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", - "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "dev": true, "requires": { "side-channel": "^1.0.4" @@ -25845,9 +24382,9 @@ } }, "semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -26653,9 +25190,9 @@ } }, "solidity-ast": { - "version": "0.4.46", - "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.46.tgz", - "integrity": "sha512-MlPZQfPhjWXqh7YxWcBGDXaPZIfMYCOHYoLEhGDWulNwEPIQQZuB7mA9eP17CU0jY/bGR4avCEUVVpvHtT2gbA==", + "version": "0.4.49", + "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.49.tgz", + "integrity": "sha512-Pr5sCAj1SFqzwFZw1HPKSq0PehlQNdM8GwKyAVYh2DOn7/cCK8LUKD1HeHnKtTgBW7hi9h4nnnan7hpAg5RhWQ==", "dev": true }, "solidity-comments": { @@ -28150,24 +26687,24 @@ } }, "web3": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.9.0.tgz", - "integrity": "sha512-E9IvVy/d2ozfQQsCiV+zh/LmlZGv9fQxI0UedDVjm87yOKf4AYbBNEn1iWtHveiGzAk2CEMZMUzAZzaQNSSYog==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.10.0.tgz", + "integrity": "sha512-YfKY9wSkGcM8seO+daR89oVTcbu18NsVfvOngzqMYGUU0pPSQmE57qQDvQzUeoIOHAnXEBNzrhjQJmm8ER0rng==", "dev": true, "requires": { - "web3-bzz": "1.9.0", - "web3-core": "1.9.0", - "web3-eth": "1.9.0", - "web3-eth-personal": "1.9.0", - "web3-net": "1.9.0", - "web3-shh": "1.9.0", - "web3-utils": "1.9.0" + "web3-bzz": "1.10.0", + "web3-core": "1.10.0", + "web3-eth": "1.10.0", + "web3-eth-personal": "1.10.0", + "web3-net": "1.10.0", + "web3-shh": "1.10.0", + "web3-utils": "1.10.0" } }, "web3-bzz": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.9.0.tgz", - "integrity": "sha512-9Zli9dikX8GdHwBb5/WPzpSVuy3EWMKY3P4EokCQra31fD7DLizqAAaTUsFwnK7xYkw5ogpHgelw9uKHHzNajg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.10.0.tgz", + "integrity": "sha512-o9IR59io3pDUsXTsps5pO5hW1D5zBmg46iNc2t4j2DkaYHNdDLwk2IP9ukoM2wg47QILfPEJYzhTfkS/CcX0KA==", "dev": true, "requires": { "@types/node": "^12.12.6", @@ -28176,18 +26713,18 @@ } }, "web3-core": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.9.0.tgz", - "integrity": "sha512-DZ+TPmq/ZLlx4LSVzFgrHCP/QFpKDbGWO4HoquZSdu24cjk5SZ+FEU1SZB2OaK3/bgBh+25mRbmv8y56ysUu1w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.10.0.tgz", + "integrity": "sha512-fWySwqy2hn3TL89w5TM8wXF1Z2Q6frQTKHWmP0ppRQorEK8NcHJRfeMiv/mQlSKoTS1F6n/nv2uyZsixFycjYQ==", "dev": true, "requires": { "@types/bn.js": "^5.1.1", "@types/node": "^12.12.6", "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.9.0", - "web3-core-method": "1.9.0", - "web3-core-requestmanager": "1.9.0", - "web3-utils": "1.9.0" + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-core-requestmanager": "1.10.0", + "web3-utils": "1.10.0" }, "dependencies": { "bignumber.js": { @@ -28199,94 +26736,94 @@ } }, "web3-core-helpers": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.9.0.tgz", - "integrity": "sha512-NeJzylAp9Yj9xAt2uTT+kyug3X0DLnfBdnAcGZuY6HhoNPDIfQRA9CkJjLngVRlGTLZGjNp9x9eR+RyZQgUlXg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.10.0.tgz", + "integrity": "sha512-pIxAzFDS5vnbXvfvLSpaA1tfRykAe9adw43YCKsEYQwH0gCLL0kMLkaCX3q+Q8EVmAh+e1jWL/nl9U0de1+++g==", "dev": true, "requires": { - "web3-eth-iban": "1.9.0", - "web3-utils": "1.9.0" + "web3-eth-iban": "1.10.0", + "web3-utils": "1.10.0" } }, "web3-core-method": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.9.0.tgz", - "integrity": "sha512-sswbNsY2xRBBhGeaLt9c/eDc+0yDDhi6keUBAkgIRa9ueSx/VKzUY9HMqiV6bXDcGT2fJyejq74FfEB4lc/+/w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.10.0.tgz", + "integrity": "sha512-4R700jTLAMKDMhQ+nsVfIXvH6IGJlJzGisIfMKWAIswH31h5AZz7uDUW2YctI+HrYd+5uOAlS4OJeeT9bIpvkA==", "dev": true, "requires": { "@ethersproject/transactions": "^5.6.2", - "web3-core-helpers": "1.9.0", - "web3-core-promievent": "1.9.0", - "web3-core-subscriptions": "1.9.0", - "web3-utils": "1.9.0" + "web3-core-helpers": "1.10.0", + "web3-core-promievent": "1.10.0", + "web3-core-subscriptions": "1.10.0", + "web3-utils": "1.10.0" } }, "web3-core-promievent": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.9.0.tgz", - "integrity": "sha512-PHG1Mn23IGwMZhnPDN8dETKypqsFbHfiyRqP+XsVMPmTHkVfzDQTCBU/c2r6hUktBDoGKut5xZQpGfhFk71KbQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.10.0.tgz", + "integrity": "sha512-68N7k5LWL5R38xRaKFrTFT2pm2jBNFaM4GioS00YjAKXRQ3KjmhijOMG3TICz6Aa5+6GDWYelDNx21YAeZ4YTg==", "dev": true, "requires": { "eventemitter3": "4.0.4" } }, "web3-core-requestmanager": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.9.0.tgz", - "integrity": "sha512-hcJ5PCtTIJpj+8qWxoseqlCovDo94JJjTX7dZOLXgwp8ah7E3WRYozhGyZocerx+KebKyg1mCQIhkDpMwjfo9Q==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.10.0.tgz", + "integrity": "sha512-3z/JKE++Os62APml4dvBM+GAuId4h3L9ckUrj7ebEtS2AR0ixyQPbrBodgL91Sv7j7cQ3Y+hllaluqjguxvSaQ==", "dev": true, "requires": { "util": "^0.12.5", - "web3-core-helpers": "1.9.0", - "web3-providers-http": "1.9.0", - "web3-providers-ipc": "1.9.0", - "web3-providers-ws": "1.9.0" + "web3-core-helpers": "1.10.0", + "web3-providers-http": "1.10.0", + "web3-providers-ipc": "1.10.0", + "web3-providers-ws": "1.10.0" } }, "web3-core-subscriptions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.9.0.tgz", - "integrity": "sha512-MaIo29yz7hTV8X8bioclPDbHFOVuHmnbMv+D3PDH12ceJFJAXGyW8GL5KU1DYyWIj4TD1HM4WknyVA/YWBiiLA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.10.0.tgz", + "integrity": "sha512-HGm1PbDqsxejI075gxBc5OSkwymilRWZufIy9zEpnWKNmfbuv5FfHgW1/chtJP6aP3Uq2vHkvTDl3smQBb8l+g==", "dev": true, "requires": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.9.0" + "web3-core-helpers": "1.10.0" } }, "web3-eth": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.9.0.tgz", - "integrity": "sha512-c5gSWk9bLNr6VPATHmZ1n7LTIefIZQnJMzfnvkoBcIFGKJbGmsuRhv6lEXsKdAO/FlqYnSbaw3fOq1fVFiIOFQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.10.0.tgz", + "integrity": "sha512-Z5vT6slNMLPKuwRyKGbqeGYC87OAy8bOblaqRTgg94CXcn/mmqU7iPIlG4506YdcdK3x6cfEDG7B6w+jRxypKA==", "dev": true, "requires": { - "web3-core": "1.9.0", - "web3-core-helpers": "1.9.0", - "web3-core-method": "1.9.0", - "web3-core-subscriptions": "1.9.0", - "web3-eth-abi": "1.9.0", - "web3-eth-accounts": "1.9.0", - "web3-eth-contract": "1.9.0", - "web3-eth-ens": "1.9.0", - "web3-eth-iban": "1.9.0", - "web3-eth-personal": "1.9.0", - "web3-net": "1.9.0", - "web3-utils": "1.9.0" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-core-subscriptions": "1.10.0", + "web3-eth-abi": "1.10.0", + "web3-eth-accounts": "1.10.0", + "web3-eth-contract": "1.10.0", + "web3-eth-ens": "1.10.0", + "web3-eth-iban": "1.10.0", + "web3-eth-personal": "1.10.0", + "web3-net": "1.10.0", + "web3-utils": "1.10.0" } }, "web3-eth-abi": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.9.0.tgz", - "integrity": "sha512-0BLQ3FKMrzJkA930jOX3fMaybAyubk06HChclLpiR0NWmgWXm1tmBrJdkyRy2ZTZpmfuZc9xTFRfl0yZID1voA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.10.0.tgz", + "integrity": "sha512-cwS+qRBWpJ43aI9L3JS88QYPfFcSJJ3XapxOQ4j40v6mk7ATpA8CVK1vGTzpihNlOfMVRBkR95oAj7oL6aiDOg==", "dev": true, "requires": { "@ethersproject/abi": "^5.6.3", - "web3-utils": "1.9.0" + "web3-utils": "1.10.0" } }, "web3-eth-accounts": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.9.0.tgz", - "integrity": "sha512-VeIZVevmnSll0AC1k5F/y398ZE89d1SRuYk8IewLUhL/tVAsFEsjl2SGgm0+aDcHmgPrkW+qsCJ+C7rWg/N4ZA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.10.0.tgz", + "integrity": "sha512-wiq39Uc3mOI8rw24wE2n15hboLE0E9BsQLdlmsL4Zua9diDS6B5abXG0XhFcoNsXIGMWXVZz4TOq3u4EdpXF/Q==", "dev": true, "requires": { "@ethereumjs/common": "2.5.0", @@ -28295,10 +26832,10 @@ "ethereumjs-util": "^7.1.5", "scrypt-js": "^3.0.1", "uuid": "^9.0.0", - "web3-core": "1.9.0", - "web3-core-helpers": "1.9.0", - "web3-core-method": "1.9.0", - "web3-utils": "1.9.0" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-utils": "1.10.0" }, "dependencies": { "eth-lib": { @@ -28321,45 +26858,45 @@ } }, "web3-eth-contract": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.9.0.tgz", - "integrity": "sha512-+j26hpSaEtAdUed0TN5rnc+YZOcjPxMjFX4ZBKatvFkImdbVv/tzTvcHlltubSpgb2ZLyZ89lSL6phKYwd2zNQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.10.0.tgz", + "integrity": "sha512-MIC5FOzP/+2evDksQQ/dpcXhSqa/2hFNytdl/x61IeWxhh6vlFeSjq0YVTAyIzdjwnL7nEmZpjfI6y6/Ufhy7w==", "dev": true, "requires": { "@types/bn.js": "^5.1.1", - "web3-core": "1.9.0", - "web3-core-helpers": "1.9.0", - "web3-core-method": "1.9.0", - "web3-core-promievent": "1.9.0", - "web3-core-subscriptions": "1.9.0", - "web3-eth-abi": "1.9.0", - "web3-utils": "1.9.0" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-core-promievent": "1.10.0", + "web3-core-subscriptions": "1.10.0", + "web3-eth-abi": "1.10.0", + "web3-utils": "1.10.0" } }, "web3-eth-ens": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.9.0.tgz", - "integrity": "sha512-LOJZeN+AGe9arhuExnrPPFYQr4WSxXEkpvYIlst/joOEUNLDwfndHnJIK6PI5mXaYSROBtTx6erv+HupzGo7vA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.10.0.tgz", + "integrity": "sha512-3hpGgzX3qjgxNAmqdrC2YUQMTfnZbs4GeLEmy8aCWziVwogbuqQZ+Gzdfrym45eOZodk+lmXyLuAdqkNlvkc1g==", "dev": true, "requires": { "content-hash": "^2.5.2", "eth-ens-namehash": "2.0.8", - "web3-core": "1.9.0", - "web3-core-helpers": "1.9.0", - "web3-core-promievent": "1.9.0", - "web3-eth-abi": "1.9.0", - "web3-eth-contract": "1.9.0", - "web3-utils": "1.9.0" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-promievent": "1.10.0", + "web3-eth-abi": "1.10.0", + "web3-eth-contract": "1.10.0", + "web3-utils": "1.10.0" } }, "web3-eth-iban": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.9.0.tgz", - "integrity": "sha512-jPAm77PuEs1kE/UrrBFJdPD2PN42pwfXA0gFuuw35bZezhskYML9W4QCxcqnUtceyEA4FUn7K2qTMuCk+23fog==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.10.0.tgz", + "integrity": "sha512-0l+SP3IGhInw7Q20LY3IVafYEuufo4Dn75jAHT7c2aDJsIolvf2Lc6ugHkBajlwUneGfbRQs/ccYPQ9JeMUbrg==", "dev": true, "requires": { "bn.js": "^5.2.1", - "web3-utils": "1.9.0" + "web3-utils": "1.10.0" }, "dependencies": { "bn.js": { @@ -28371,79 +26908,79 @@ } }, "web3-eth-personal": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.9.0.tgz", - "integrity": "sha512-r9Ldo/luBqJlv1vCUEQnUS+C3a3ZdbYxVHyfDkj6RWMyCqqo8JE41HWE+pfa0RmB1xnGL2g8TbYcHcqItck/qg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.10.0.tgz", + "integrity": "sha512-anseKn98w/d703eWq52uNuZi7GhQeVjTC5/svrBWEKob0WZ5kPdo+EZoFN0sp5a5ubbrk/E0xSl1/M5yORMtpg==", "dev": true, "requires": { "@types/node": "^12.12.6", - "web3-core": "1.9.0", - "web3-core-helpers": "1.9.0", - "web3-core-method": "1.9.0", - "web3-net": "1.9.0", - "web3-utils": "1.9.0" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-net": "1.10.0", + "web3-utils": "1.10.0" } }, "web3-net": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.9.0.tgz", - "integrity": "sha512-L+fDZFgrLM5Y15aonl2q6L+RvfaImAngmC0Jv45hV2FJ5IfRT0/2ob9etxZmvEBWvOpbqSvghfOhJIT3XZ37Pg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.10.0.tgz", + "integrity": "sha512-NLH/N3IshYWASpxk4/18Ge6n60GEvWBVeM8inx2dmZJVmRI6SJIlUxbL8jySgiTn3MMZlhbdvrGo8fpUW7a1GA==", "dev": true, "requires": { - "web3-core": "1.9.0", - "web3-core-method": "1.9.0", - "web3-utils": "1.9.0" + "web3-core": "1.10.0", + "web3-core-method": "1.10.0", + "web3-utils": "1.10.0" } }, "web3-providers-http": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.9.0.tgz", - "integrity": "sha512-5+dMNDAE0rRFz6SJpfnBqlVi2J5bB/Ivr2SanMt2YUrkxW5t8betZbzVwRkTbwtUvkqgj3xeUQzqpOttiv+IqQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.10.0.tgz", + "integrity": "sha512-eNr965YB8a9mLiNrkjAWNAPXgmQWfpBfkkn7tpEFlghfww0u3I0tktMZiaToJVcL2+Xq+81cxbkpeWJ5XQDwOA==", "dev": true, "requires": { "abortcontroller-polyfill": "^1.7.3", "cross-fetch": "^3.1.4", "es6-promise": "^4.2.8", - "web3-core-helpers": "1.9.0" + "web3-core-helpers": "1.10.0" } }, "web3-providers-ipc": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.9.0.tgz", - "integrity": "sha512-cPXU93Du40HCylvjaa5x62DbnGqH+86HpK/+kMcFIzF6sDUBhKpag2tSbYhGbj7GMpfkmDTUiiMLdWnFV6+uBA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.10.0.tgz", + "integrity": "sha512-OfXG1aWN8L1OUqppshzq8YISkWrYHaATW9H8eh0p89TlWMc1KZOL9vttBuaBEi96D/n0eYDn2trzt22bqHWfXA==", "dev": true, "requires": { "oboe": "2.1.5", - "web3-core-helpers": "1.9.0" + "web3-core-helpers": "1.10.0" } }, "web3-providers-ws": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.9.0.tgz", - "integrity": "sha512-JRVsnQZ7j2k1a2yzBNHe39xqk1ijOv01dfIBFw52VeEkSRzvrOcsPIM/ttSyBuJqt70ntMxXY0ekCrqfleKH/w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.10.0.tgz", + "integrity": "sha512-sK0fNcglW36yD5xjnjtSGBnEtf59cbw4vZzJ+CmOWIKGIR96mP5l684g0WD0Eo+f4NQc2anWWXG74lRc9OVMCQ==", "dev": true, "requires": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.9.0", + "web3-core-helpers": "1.10.0", "websocket": "^1.0.32" } }, "web3-shh": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.9.0.tgz", - "integrity": "sha512-bIBZlralgz4ICCrwkefB2nPPJWfx28NuHIpjB7d9ADKynElubQuqudYhKtSEkKXACuME/BJm0pIFJcJs/gDnMg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.10.0.tgz", + "integrity": "sha512-uNUUuNsO2AjX41GJARV9zJibs11eq6HtOe6Wr0FtRUcj8SN6nHeYIzwstAvJ4fXA53gRqFMTxdntHEt9aXVjpg==", "dev": true, "requires": { - "web3-core": "1.9.0", - "web3-core-method": "1.9.0", - "web3-core-subscriptions": "1.9.0", - "web3-net": "1.9.0" + "web3-core": "1.10.0", + "web3-core-method": "1.10.0", + "web3-core-subscriptions": "1.10.0", + "web3-net": "1.10.0" } }, "web3-utils": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.9.0.tgz", - "integrity": "sha512-p++69rCNNfu2jM9n5+VD/g26l+qkEOQ1m6cfRQCbH8ZRrtquTmrirJMgTmyOoax5a5XRYOuws14aypCOs51pdQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.0.tgz", + "integrity": "sha512-kSaCM0uMcZTNUSmn5vMEhlo02RObGNRRCkdX0V9UTAU0+lrvn0HSaudyCo6CQzuXUsnuY2ERJGCGPfeWmv19Rg==", "dev": true, "requires": { "bn.js": "^5.2.1", From 5cef83d2c7730fbd523045b551d0efd020ceb17b Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Fri, 2 Jun 2023 11:37:59 -0300 Subject: [PATCH 150/197] Optimize array allocation in ERC1155 (#4196) Co-authored-by: Francisco --- .changeset/serious-books-lie.md | 5 +++++ contracts/token/ERC1155/ERC1155.sol | 28 ++++++++++++++++++---------- 2 files changed, 23 insertions(+), 10 deletions(-) create mode 100644 .changeset/serious-books-lie.md diff --git a/.changeset/serious-books-lie.md b/.changeset/serious-books-lie.md new file mode 100644 index 000000000..6f0a0a732 --- /dev/null +++ b/.changeset/serious-books-lie.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ERC1155`: Optimize array allocation. diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol index a2d7404bf..6761edd24 100644 --- a/contracts/token/ERC1155/ERC1155.sol +++ b/contracts/token/ERC1155/ERC1155.sol @@ -206,8 +206,7 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { 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"); - uint256[] memory ids = _asSingletonArray(id); - uint256[] memory amounts = _asSingletonArray(amount); + (uint256[] memory ids, uint256[] memory amounts) = _asSingletonArrays(id, amount); _update(from, to, ids, amounts, data); } @@ -269,8 +268,7 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { */ function _mint(address to, uint256 id, uint256 amount, bytes memory data) internal { require(to != address(0), "ERC1155: mint to the zero address"); - uint256[] memory ids = _asSingletonArray(id); - uint256[] memory amounts = _asSingletonArray(amount); + (uint256[] memory ids, uint256[] memory amounts) = _asSingletonArrays(id, amount); _update(address(0), to, ids, amounts, data); } @@ -302,8 +300,7 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { */ function _burn(address from, uint256 id, uint256 amount) internal { require(from != address(0), "ERC1155: burn from the zero address"); - uint256[] memory ids = _asSingletonArray(id); - uint256[] memory amounts = _asSingletonArray(amount); + (uint256[] memory ids, uint256[] memory amounts) = _asSingletonArrays(id, amount); _update(from, address(0), ids, amounts, ""); } @@ -376,10 +373,21 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { } } - function _asSingletonArray(uint256 element) private pure returns (uint256[] memory) { - uint256[] memory array = new uint256[](1); - array[0] = element; + function _asSingletonArrays( + uint256 element1, + uint256 element2 + ) private pure returns (uint256[] memory array1, uint256[] memory array2) { + /// @solidity memory-safe-assembly + assembly { + array1 := mload(0x40) + mstore(array1, 1) + mstore(add(array1, 0x20), element1) - return array; + array2 := add(array1, 0x40) + mstore(array2, 1) + mstore(add(array2, 0x20), element2) + + mstore(0x40, add(array2, 0x40)) + } } } From 3902a410f15a579c91d17a8d7e1e5b24351b6c65 Mon Sep 17 00:00:00 2001 From: Francisco Date: Fri, 2 Jun 2023 16:02:57 +0100 Subject: [PATCH 151/197] Remove DOMAIN_SEPARATOR from Votes and update docs examples (#4297) Co-authored-by: Qiwei Yang Co-authored-by: Hadrien Croubois --- .changeset/silly-bees-beam.md | 4 +- .github/workflows/checks.yml | 8 +- contracts/governance/utils/Votes.sol | 8 - .../mocks/docs/governance/MyGovernor.sol | 88 ++++++++ contracts/mocks/docs/governance/MyToken.sol | 20 ++ .../docs/governance/MyTokenTimestampBased.sol | 31 +++ .../mocks/docs/governance/MyTokenWrapped.sol | 27 +++ .../token/ERC20/extensions/ERC20Permit.sol | 2 +- docs/modules/ROOT/pages/governance.adoc | 199 +----------------- scripts/prepare-docs.sh | 13 +- test/governance/utils/Votes.behavior.js | 6 +- .../token/ERC20/extensions/ERC20Votes.test.js | 4 - 12 files changed, 186 insertions(+), 224 deletions(-) create mode 100644 contracts/mocks/docs/governance/MyGovernor.sol create mode 100644 contracts/mocks/docs/governance/MyToken.sol create mode 100644 contracts/mocks/docs/governance/MyTokenTimestampBased.sol create mode 100644 contracts/mocks/docs/governance/MyTokenWrapped.sol diff --git a/.changeset/silly-bees-beam.md b/.changeset/silly-bees-beam.md index 6be23768c..0f4f40507 100644 --- a/.changeset/silly-bees-beam.md +++ b/.changeset/silly-bees-beam.md @@ -2,4 +2,6 @@ 'openzeppelin-solidity': major --- -`ERC20Votes`: Changed internal vote accounting to reusable `Votes` module previously used by `ERC721Votes`. Removed implicit `ERC20Permit` inheritance. [#3816](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3816) +`ERC20Votes`: Changed internal vote accounting to reusable `Votes` module previously used by `ERC721Votes`. Removed implicit `ERC20Permit` inheritance. Note that the `DOMAIN_SEPARATOR` getter was previously guaranteed to be available for `ERC20Votes` contracts, but is no longer available unless `ERC20Permit` is explicitly used; ERC-5267 support is included in `ERC20Votes` with `EIP712` and is recommended as an alternative. + +pr: #3816 diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 7cf7e6917..122d39564 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -13,6 +13,9 @@ concurrency: group: checks-${{ github.ref }} cancel-in-progress: true +env: + NODE_OPTIONS: --max_old_space_size=5120 + jobs: lint: runs-on: ubuntu-latest @@ -26,7 +29,6 @@ jobs: runs-on: ubuntu-latest env: FORCE_COLOR: 1 - NODE_OPTIONS: --max_old_space_size=4096 GAS: true steps: - uses: actions/checkout@v3 @@ -57,8 +59,6 @@ jobs: run: bash scripts/upgradeable/transpile.sh - name: Run tests run: npm run test - env: - NODE_OPTIONS: --max_old_space_size=4096 - name: Check linearisation of the inheritance graph run: npm run test:inheritance - name: Check storage layout @@ -86,8 +86,6 @@ jobs: - name: Set up environment uses: ./.github/actions/setup - run: npm run coverage - env: - NODE_OPTIONS: --max_old_space_size=4096 - uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index 5c855b5e4..94f6d4fb9 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -225,14 +225,6 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { return a - b; } - /** - * @dev Returns the contract's {EIP712} domain separator. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32) { - return _domainSeparatorV4(); - } - /** * @dev Must return the voting units held by an account. */ diff --git a/contracts/mocks/docs/governance/MyGovernor.sol b/contracts/mocks/docs/governance/MyGovernor.sol new file mode 100644 index 000000000..095523b3d --- /dev/null +++ b/contracts/mocks/docs/governance/MyGovernor.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../../../governance/Governor.sol"; +import "../../../governance/compatibility/GovernorCompatibilityBravo.sol"; +import "../../../governance/extensions/GovernorVotes.sol"; +import "../../../governance/extensions/GovernorVotesQuorumFraction.sol"; +import "../../../governance/extensions/GovernorTimelockControl.sol"; + +contract MyGovernor is + Governor, + GovernorCompatibilityBravo, + GovernorVotes, + GovernorVotesQuorumFraction, + GovernorTimelockControl +{ + constructor( + IVotes _token, + TimelockController _timelock + ) Governor("MyGovernor") GovernorVotes(_token) GovernorVotesQuorumFraction(4) GovernorTimelockControl(_timelock) {} + + function votingDelay() public pure override returns (uint256) { + return 7200; // 1 day + } + + function votingPeriod() public pure override returns (uint256) { + return 50400; // 1 week + } + + function proposalThreshold() public pure override returns (uint256) { + return 0; + } + + // The functions below are overrides required by Solidity. + + function state( + uint256 proposalId + ) public view override(Governor, IGovernor, GovernorTimelockControl) returns (ProposalState) { + return super.state(proposalId); + } + + function propose( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description + ) public override(Governor, GovernorCompatibilityBravo, IGovernor) returns (uint256) { + return super.propose(targets, values, calldatas, description); + } + + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public override(Governor, GovernorCompatibilityBravo, IGovernor) returns (uint256) { + return super.cancel(targets, values, calldatas, descriptionHash); + } + + function _execute( + uint256 proposalId, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal override(Governor, GovernorTimelockControl) { + super._execute(proposalId, targets, values, calldatas, descriptionHash); + } + + function _cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal override(Governor, GovernorTimelockControl) returns (uint256) { + return super._cancel(targets, values, calldatas, descriptionHash); + } + + function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) { + return super._executor(); + } + + function supportsInterface( + bytes4 interfaceId + ) public view override(Governor, IERC165, GovernorTimelockControl) returns (bool) { + return super.supportsInterface(interfaceId); + } +} diff --git a/contracts/mocks/docs/governance/MyToken.sol b/contracts/mocks/docs/governance/MyToken.sol new file mode 100644 index 000000000..f7707ff9d --- /dev/null +++ b/contracts/mocks/docs/governance/MyToken.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../../../token/ERC20/ERC20.sol"; +import "../../../token/ERC20/extensions/ERC20Permit.sol"; +import "../../../token/ERC20/extensions/ERC20Votes.sol"; + +contract MyToken is ERC20, ERC20Permit, ERC20Votes { + constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {} + + // The functions below are overrides required by Solidity. + + function _update(address from, address to, uint256 amount) internal override(ERC20, ERC20Votes) { + super._update(from, to, amount); + } + + function nonces(address owner) public view virtual override(ERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } +} diff --git a/contracts/mocks/docs/governance/MyTokenTimestampBased.sol b/contracts/mocks/docs/governance/MyTokenTimestampBased.sol new file mode 100644 index 000000000..1c688c250 --- /dev/null +++ b/contracts/mocks/docs/governance/MyTokenTimestampBased.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../../../token/ERC20/ERC20.sol"; +import "../../../token/ERC20/extensions/ERC20Permit.sol"; +import "../../../token/ERC20/extensions/ERC20Votes.sol"; + +contract MyTokenTimestampBased is ERC20, ERC20Permit, ERC20Votes { + constructor() ERC20("MyTokenTimestampBased", "MTK") ERC20Permit("MyTokenTimestampBased") {} + + // Overrides IERC6372 functions to make the token & governor timestamp-based + + function clock() public view override returns (uint48) { + return uint48(block.timestamp); + } + + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public pure override returns (string memory) { + return "mode=timestamp"; + } + + // The functions below are overrides required by Solidity. + + function _update(address from, address to, uint256 amount) internal override(ERC20, ERC20Votes) { + super._update(from, to, amount); + } + + function nonces(address owner) public view virtual override(ERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } +} diff --git a/contracts/mocks/docs/governance/MyTokenWrapped.sol b/contracts/mocks/docs/governance/MyTokenWrapped.sol new file mode 100644 index 000000000..6637dccc3 --- /dev/null +++ b/contracts/mocks/docs/governance/MyTokenWrapped.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../../../token/ERC20/ERC20.sol"; +import "../../../token/ERC20/extensions/ERC20Permit.sol"; +import "../../../token/ERC20/extensions/ERC20Votes.sol"; +import "../../../token/ERC20/extensions/ERC20Wrapper.sol"; + +contract MyTokenWrapped is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper { + constructor( + IERC20 wrappedToken + ) ERC20("MyTokenWrapped", "MTK") ERC20Permit("MyTokenWrapped") ERC20Wrapper(wrappedToken) {} + + // The functions below are overrides required by Solidity. + + function decimals() public view override(ERC20, ERC20Wrapper) returns (uint8) { + return super.decimals(); + } + + function _update(address from, address to, uint256 amount) internal override(ERC20, ERC20Votes) { + super._update(from, to, amount); + } + + function nonces(address owner) public view virtual override(ERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } +} diff --git a/contracts/token/ERC20/extensions/ERC20Permit.sol b/contracts/token/ERC20/extensions/ERC20Permit.sol index dbdc37053..e7d43c2dd 100644 --- a/contracts/token/ERC20/extensions/ERC20Permit.sol +++ b/contracts/token/ERC20/extensions/ERC20Permit.sol @@ -66,7 +66,7 @@ abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { * @dev See {IERC20Permit-DOMAIN_SEPARATOR}. */ // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view override returns (bytes32) { + function DOMAIN_SEPARATOR() external view virtual override returns (bytes32) { return _domainSeparatorV4(); } } diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index a40275b07..3d2bc05f3 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -43,82 +43,13 @@ In the rest of this guide, we will focus on a fresh deploy of the vanilla OpenZe The voting power of each account in our governance setup will be determined by an ERC20 token. The token has to implement the ERC20Votes extension. This extension will keep track of historical balances so that voting power is retrieved from past snapshots rather than current balance, which is an important protection that prevents double voting. ```solidity -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; - -contract MyToken is ERC20, ERC20Permit, ERC20Votes { - constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC20, ERC20Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC20, ERC20Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC20, ERC20Votes) - { - super._burn(account, amount); - } -} +include::api:example$governance/MyToken.sol[] ``` If your project already has a live token that does not include ERC20Votes and is not upgradeable, you can wrap it in a governance token by using ERC20Wrapper. This will allow token holders to participate in governance by wrapping their tokens 1-to-1. ```solidity -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; - -contract MyToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper { - constructor(IERC20 wrappedToken) - ERC20("MyToken", "MTK") - ERC20Permit("MyToken") - ERC20Wrapper(wrappedToken) - {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC20, ERC20Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC20, ERC20Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC20, ERC20Votes) - { - super._burn(account, amount); - } -} +include::api:example$governance/MyTokenWrapped.sol[] ``` NOTE: The only other source of voting power available in OpenZeppelin Contracts currently is xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`]. ERC721 tokens that don't provide this functionality can be wrapped into a voting tokens using a combination of xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`] and xref:api:token/ERC721Wrapper.adoc#ERC721Wrapper[`ERC721Wrapper`]. @@ -146,87 +77,7 @@ These parameters are specified in the unit defined in the token's clock. Assumin We can optionally set a proposal threshold as well. This restricts proposal creation to accounts who have enough voting power. ```solidity -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import "@openzeppelin/contracts/governance/Governor.sol"; -import "@openzeppelin/contracts/governance/compatibility/GovernorCompatibilityBravo.sol"; -import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol"; -import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol"; -import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol"; - -contract MyGovernor is Governor, GovernorCompatibilityBravo, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl { - constructor(IVotes _token, TimelockController _timelock) - Governor("MyGovernor") - GovernorVotes(_token) - GovernorVotesQuorumFraction(4) - GovernorTimelockControl(_timelock) - {} - - function votingDelay() public pure override returns (uint256) { - return 7200; // 1 day - } - - function votingPeriod() public pure override returns (uint256) { - return 50400; // 1 week - } - - function proposalThreshold() public pure override returns (uint256) { - return 0; - } - - // The functions below are overrides required by Solidity. - - function state(uint256 proposalId) - public - view - override(Governor, IGovernor, GovernorTimelockControl) - returns (ProposalState) - { - return super.state(proposalId); - } - - function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) - public - override(Governor, GovernorCompatibilityBravo, IGovernor) - returns (uint256) - { - return super.propose(targets, values, calldatas, description); - } - - function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) - internal - override(Governor, GovernorTimelockControl) - { - super._execute(proposalId, targets, values, calldatas, descriptionHash); - } - - function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) - internal - override(Governor, GovernorTimelockControl) - returns (uint256) - { - return super._cancel(targets, values, calldatas, descriptionHash); - } - - function _executor() - internal - view - override(Governor, GovernorTimelockControl) - returns (address) - { - return super._executor(); - } - - function supportsInterface(bytes4 interfaceId) - public - view - override(Governor, IERC165, GovernorTimelockControl) - returns (bool) - { - return super.supportsInterface(interfaceId); - } -} +include::api:example$governance/MyGovernor.sol[] ``` === Timelock @@ -338,49 +189,7 @@ Therefore, designing a timestamp based voting system starts with the token. Since v4.9, all voting contracts (including xref:api:token/ERC20.adoc#ERC20Votes[`ERC20Votes`] and xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`]) rely on xref:api:interfaces.adoc#IERC6372[IERC6372] for clock management. In order to change from operating with block numbers to operating with timestamps, all that is required is to override the `clock()` and `CLOCK_MODE()` functions. ```solidity -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import "github.com/openzeppelin/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import "github.com/openzeppelin/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol"; -import "github.com/openzeppelin/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Votes.sol"; - -contract MyToken is ERC20, ERC20Permit, ERC20Votes { - constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {} - - // Overrides IERC6372 functions to make the token & governor timestamp-based - - function clock() public view override returns (uint48) { - return uint48(block.timestamp); - } - - function CLOCK_MODE() public pure override returns (string memory) { - return "mode=timestamp"; - } - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC20, ERC20Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC20, ERC20Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC20, ERC20Votes) - { - super._burn(account, amount); - } -} +include::api:example$governance/MyTokenTimestampBased.sol[] ``` === Governor diff --git a/scripts/prepare-docs.sh b/scripts/prepare-docs.sh index bb9c5d0ad..d1317b092 100755 --- a/scripts/prepare-docs.sh +++ b/scripts/prepare-docs.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -euo pipefail +shopt -s globstar OUTDIR="$(node -p 'require("./docs/config.js").outputDir')" @@ -13,11 +14,13 @@ rm -rf "$OUTDIR" hardhat docgen # copy examples and adjust imports -examples_dir="docs/modules/api/examples" -mkdir -p "$examples_dir" -for f in contracts/mocks/docs/*.sol; do - name="$(basename "$f")" - sed -e '/^import/s|\.\./\.\./|@openzeppelin/contracts/|' "$f" > "docs/modules/api/examples/$name" +examples_source_dir="contracts/mocks/docs" +examples_target_dir="docs/modules/api/examples" + +for f in "$examples_source_dir"/**/*.sol; do + name="${f/#"$examples_source_dir/"/}" + mkdir -p "$examples_target_dir/$(dirname "$name")" + sed -Ee '/^import/s|"(\.\./)+|"@openzeppelin/contracts/|' "$f" > "$examples_target_dir/$name" done node scripts/gen-nav.js "$OUTDIR" > "$OUTDIR/../nav.adoc" diff --git a/test/governance/utils/Votes.behavior.js b/test/governance/utils/Votes.behavior.js index 02f2c2544..37062e19c 100644 --- a/test/governance/utils/Votes.behavior.js +++ b/test/governance/utils/Votes.behavior.js @@ -7,7 +7,7 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; const { shouldBehaveLikeEIP6372 } = require('./EIP6372.behavior'); -const { getDomain, domainType, domainSeparator } = require('../../helpers/eip712'); +const { getDomain, domainType } = require('../../helpers/eip712'); const { clockFromReceipt } = require('../../helpers/time'); const Delegation = [ @@ -36,10 +36,6 @@ function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungibl expect(await this.votes.nonces(accounts[0])).to.be.bignumber.equal('0'); }); - it('domain separator', async function () { - expect(await this.votes.DOMAIN_SEPARATOR()).to.equal(domainSeparator(await getDomain(this.votes))); - }); - describe('delegation with signature', function () { const token = tokens[0]; diff --git a/test/token/ERC20/extensions/ERC20Votes.test.js b/test/token/ERC20/extensions/ERC20Votes.test.js index 621f58b7d..e4ff58cd9 100644 --- a/test/token/ERC20/extensions/ERC20Votes.test.js +++ b/test/token/ERC20/extensions/ERC20Votes.test.js @@ -46,10 +46,6 @@ contract('ERC20Votes', function (accounts) { expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); }); - it('domain separator', async function () { - expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator)); - }); - it('minting restriction', async function () { const amount = new BN('2').pow(new BN('224')); await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes'); From 2d1da295e6e28b3cd86eccab47c40a2bcbf0f4d1 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 2 Jun 2023 17:14:41 +0200 Subject: [PATCH 152/197] Move some changeset to the "Removals" section of CHANGELOG (#4290) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García Co-authored-by: Francisco --- .changeset/beige-ducks-flow.md | 5 ----- .changeset/dirty-mangos-sort.md | 5 ----- .changeset/fluffy-gifts-build.md | 5 ----- .changeset/friendly-suits-camp.md | 5 ----- .changeset/hungry-impalas-perform.md | 5 ----- .changeset/selfish-queens-rest.md | 5 ----- .changeset/spicy-ducks-cough.md | 5 ----- .changeset/swift-berries-sort.md | 5 ----- .changeset/tame-geckos-search.md | 5 ----- .changeset/three-weeks-double.md | 5 ----- .changeset/unlucky-snakes-drive.md | 5 ----- CHANGELOG.md | 26 ++++++++++++++++++++++++-- 12 files changed, 24 insertions(+), 57 deletions(-) delete mode 100644 .changeset/beige-ducks-flow.md delete mode 100644 .changeset/dirty-mangos-sort.md delete mode 100644 .changeset/fluffy-gifts-build.md delete mode 100644 .changeset/friendly-suits-camp.md delete mode 100644 .changeset/hungry-impalas-perform.md delete mode 100644 .changeset/selfish-queens-rest.md delete mode 100644 .changeset/spicy-ducks-cough.md delete mode 100644 .changeset/swift-berries-sort.md delete mode 100644 .changeset/tame-geckos-search.md delete mode 100644 .changeset/three-weeks-double.md delete mode 100644 .changeset/unlucky-snakes-drive.md diff --git a/.changeset/beige-ducks-flow.md b/.changeset/beige-ducks-flow.md deleted file mode 100644 index 4edc870b5..000000000 --- a/.changeset/beige-ducks-flow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -Remove deprecated GovernorProposalThreshold module. diff --git a/.changeset/dirty-mangos-sort.md b/.changeset/dirty-mangos-sort.md deleted file mode 100644 index 6981399c0..000000000 --- a/.changeset/dirty-mangos-sort.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -Removed presets in favor of [OpenZeppelin Contracts Wizard](https://wizard.openzeppelin.com/). diff --git a/.changeset/fluffy-gifts-build.md b/.changeset/fluffy-gifts-build.md deleted file mode 100644 index a1ffe8be1..000000000 --- a/.changeset/fluffy-gifts-build.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -Remove ERC1820Implementer. diff --git a/.changeset/friendly-suits-camp.md b/.changeset/friendly-suits-camp.md deleted file mode 100644 index bcf1d7ee4..000000000 --- a/.changeset/friendly-suits-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -Remove Checkpoints.History. diff --git a/.changeset/hungry-impalas-perform.md b/.changeset/hungry-impalas-perform.md deleted file mode 100644 index 24901ec86..000000000 --- a/.changeset/hungry-impalas-perform.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -`ERC165Storage`: Removed this contract in favor of inheritance based approach. ([#3880](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3880)) diff --git a/.changeset/selfish-queens-rest.md b/.changeset/selfish-queens-rest.md deleted file mode 100644 index 739fa1356..000000000 --- a/.changeset/selfish-queens-rest.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -Remove PullPayment and Escrow contracts (Escrow, ConditionalEscrow, RefundEscrow). diff --git a/.changeset/spicy-ducks-cough.md b/.changeset/spicy-ducks-cough.md deleted file mode 100644 index bf7296e4e..000000000 --- a/.changeset/spicy-ducks-cough.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -Remove the Timers library. diff --git a/.changeset/swift-berries-sort.md b/.changeset/swift-berries-sort.md deleted file mode 100644 index 9af6cba64..000000000 --- a/.changeset/swift-berries-sort.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -Remove ERC777 implementation. diff --git a/.changeset/tame-geckos-search.md b/.changeset/tame-geckos-search.md deleted file mode 100644 index 6b6ae86d0..000000000 --- a/.changeset/tame-geckos-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -Remove SafeMath and SignedSafeMath libraries. diff --git a/.changeset/three-weeks-double.md b/.changeset/three-weeks-double.md deleted file mode 100644 index d267e72c4..000000000 --- a/.changeset/three-weeks-double.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -Remove CrossChain contracts, including AccessControlCrossChain and all the vendored bridge interfaces. diff --git a/.changeset/unlucky-snakes-drive.md b/.changeset/unlucky-snakes-drive.md deleted file mode 100644 index d4c78dd50..000000000 --- a/.changeset/unlucky-snakes-drive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`Address`: Removed `isContract` because of its ambiguous nature and potential for misuse. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fc4cc5e3..c9fac4d1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,36 @@ ### Removals -The following contracts and libraries were removed: +The following contracts, libraries and functions were removed: +- `Address.isContract` (because of its ambiguous nature and potential for misuse) +- `Checkpoints.History` - `Counters` - `ERC20Snapshot` - `ERC20VotesComp` +- `ERC165Storage` (in favor of inheritance based approach) +- `ERC777` +- `ERC1820Implementer` - `GovernorVotesComp` +- `GovernorProposalThreshold` (deprecated since 4.4) - `PaymentSplitter` -- `TokenTimelock` (removed in favor of `VestingWallet`) +- `PullPayment` +- `SafeMath` +- `SignedSafeMath` +- `Timers` +- `TokenTimelock` (in favor of `VestingWallet`) +- All escrow contracts (`Escrow`, `ConditionalEscrow` and `RefundEscrow`) +- All cross-chain contracts, including `AccessControlCrossChain` and all the vendored bridge interfaces +- All presets in favor of [OpenZeppelin Contracts Wizard](https://wizard.openzeppelin.com/) + +These removals were implemented in the following PRs: + +- [3637](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3637) +- [3880](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3880) +- [3945](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3945) +- [4258](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4258) +- [4276](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4276) +- [4289](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4289) ### How to upgrade from 4.x From eecd5e15c7157ad8dd8a65e9d1462853876f1d00 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 2 Jun 2023 17:42:02 +0200 Subject: [PATCH 153/197] Make CHANGELOG more compact for improved readability (#4306) --- CHANGELOG.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9fac4d1f..9d4e13046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,14 +24,7 @@ The following contracts, libraries and functions were removed: - All cross-chain contracts, including `AccessControlCrossChain` and all the vendored bridge interfaces - All presets in favor of [OpenZeppelin Contracts Wizard](https://wizard.openzeppelin.com/) -These removals were implemented in the following PRs: - -- [3637](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3637) -- [3880](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3880) -- [3945](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3945) -- [4258](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4258) -- [4276](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4276) -- [4289](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4289) +These removals were implemented in the following PRs: [#3637](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3637), [#3880](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3880), [#3945](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3945), [#4258](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4258), [#4276](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4276), [#4289](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4289) ### How to upgrade from 4.x From ffceb3cd988874369806139ae9e59d2e2a93daec Mon Sep 17 00:00:00 2001 From: Francisco Date: Fri, 2 Jun 2023 18:20:58 +0100 Subject: [PATCH 154/197] Remove hardcoded function resolution (#4299) --- .changeset/red-dots-fold.md | 5 +++ .../ERC1155/extensions/ERC1155Supply.sol | 2 +- .../token/ERC20/extensions/ERC20FlashMint.sol | 2 +- contracts/token/ERC721/ERC721.sol | 32 +++++++++---------- .../ERC721/extensions/ERC721Enumerable.sol | 4 +-- 5 files changed, 25 insertions(+), 20 deletions(-) create mode 100644 .changeset/red-dots-fold.md diff --git a/.changeset/red-dots-fold.md b/.changeset/red-dots-fold.md new file mode 100644 index 000000000..6df4c8f0c --- /dev/null +++ b/.changeset/red-dots-fold.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Overrides are now used internally for a number of functions that were previously hardcoded to their default implementation in certain locations: `ERC1155Supply.totalSupply`, `ERC721.ownerOf`, `ERC721.balanceOf` in `ERC721Enumerable`, and `ERC20.totalSupply` in `ERC20FlashMint`. diff --git a/contracts/token/ERC1155/extensions/ERC1155Supply.sol b/contracts/token/ERC1155/extensions/ERC1155Supply.sol index 599df00f3..4ad83ea02 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Supply.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Supply.sol @@ -38,7 +38,7 @@ abstract contract ERC1155Supply is ERC1155 { * @dev Indicates whether any token exist with a given id, or not. */ function exists(uint256 id) public view virtual returns (bool) { - return ERC1155Supply.totalSupply(id) > 0; + return totalSupply(id) > 0; } /** diff --git a/contracts/token/ERC20/extensions/ERC20FlashMint.sol b/contracts/token/ERC20/extensions/ERC20FlashMint.sol index ce68793b5..18b0a81c6 100644 --- a/contracts/token/ERC20/extensions/ERC20FlashMint.sol +++ b/contracts/token/ERC20/extensions/ERC20FlashMint.sol @@ -25,7 +25,7 @@ abstract contract ERC20FlashMint is ERC20, IERC3156FlashLender { * @return The amount of token that can be loaned. */ function maxFlashLoan(address token) public view virtual override returns (uint256) { - return token == address(this) ? type(uint256).max - ERC20.totalSupply() : 0; + return token == address(this) ? type(uint256).max - totalSupply() : 0; } /** diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 58a626cda..af00ecaf4 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -108,7 +108,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * @dev See {IERC721-approve}. */ function approve(address to, uint256 tokenId) public virtual override { - address owner = ERC721.ownerOf(tokenId); + address owner = ownerOf(tokenId); require(to != owner, "ERC721: approval to current owner"); require( @@ -217,7 +217,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * - `tokenId` must exist. */ function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) { - address owner = ERC721.ownerOf(tokenId); + address owner = ownerOf(tokenId); return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender); } @@ -295,21 +295,20 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * Emits a {Transfer} event. */ function _burn(uint256 tokenId) internal virtual { - address owner = ERC721.ownerOf(tokenId); + address owner = ownerOf(tokenId); _beforeTokenTransfer(owner, address(0), tokenId, 1); // Update ownership in case tokenId was transferred by `_beforeTokenTransfer` hook - owner = ERC721.ownerOf(tokenId); + owner = ownerOf(tokenId); // Clear approvals delete _tokenApprovals[tokenId]; - unchecked { - // Cannot overflow, as that would require more tokens to be burned/transferred - // out than the owner initially received through minting and transferring in. - _balances[owner] -= 1; - } + // Decrease balance with checked arithmetic, because an `ownerOf` override may + // invalidate the assumption that `_balances[from] >= 1`. + _balances[owner] -= 1; + delete _owners[tokenId]; emit Transfer(owner, address(0), tokenId); @@ -329,26 +328,27 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * Emits a {Transfer} event. */ function _transfer(address from, address to, uint256 tokenId) internal virtual { - require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); + require(ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); require(to != address(0), "ERC721: transfer to the zero address"); _beforeTokenTransfer(from, to, tokenId, 1); // Check that tokenId was not transferred by `_beforeTokenTransfer` hook - require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); + require(ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); // Clear approvals from the previous owner delete _tokenApprovals[tokenId]; + // Decrease balance with checked arithmetic, because an `ownerOf` override may + // invalidate the assumption that `_balances[from] >= 1`. + _balances[from] -= 1; + unchecked { - // `_balances[from]` cannot overflow for the same reason as described in `_burn`: - // `from`'s balance is the number of token held, which is at least one before the current - // transfer. // `_balances[to]` could overflow in the conditions described in `_mint`. That would require // all 2**256 token ids to be minted, which in practice is impossible. - _balances[from] -= 1; _balances[to] += 1; } + _owners[tokenId] = to; emit Transfer(from, to, tokenId); @@ -363,7 +363,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { */ function _approve(address to, uint256 tokenId) internal virtual { _tokenApprovals[tokenId] = to; - emit Approval(ERC721.ownerOf(tokenId), to, tokenId); + emit Approval(ownerOf(tokenId), to, tokenId); } /** diff --git a/contracts/token/ERC721/extensions/ERC721Enumerable.sol b/contracts/token/ERC721/extensions/ERC721Enumerable.sol index 6b702d53b..a9513b21d 100644 --- a/contracts/token/ERC721/extensions/ERC721Enumerable.sol +++ b/contracts/token/ERC721/extensions/ERC721Enumerable.sol @@ -35,7 +35,7 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { * @dev See {IERC721Enumerable-tokenOfOwnerByIndex}. */ function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual override returns (uint256) { - require(index < ERC721.balanceOf(owner), "ERC721Enumerable: owner index out of bounds"); + require(index < balanceOf(owner), "ERC721Enumerable: owner index out of bounds"); return _ownedTokens[owner][index]; } @@ -90,7 +90,7 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { * @param tokenId uint256 ID of the token to be added to the tokens list of the given address */ function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private { - uint256 length = ERC721.balanceOf(to); + uint256 length = balanceOf(to); _ownedTokens[to][length] = tokenId; _ownedTokensIndex[tokenId] = length; } From 253bfa68c27785536d58afcc95ebeb4af6f9aa7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=BBuk?= Date: Tue, 6 Jun 2023 02:37:12 +0200 Subject: [PATCH 155/197] Optimize Strings.equal (#4262) Co-authored-by: Hadrien Croubois --- .changeset/eighty-crabs-listen.md | 5 +++++ contracts/utils/Strings.sol | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/eighty-crabs-listen.md diff --git a/.changeset/eighty-crabs-listen.md b/.changeset/eighty-crabs-listen.md new file mode 100644 index 000000000..7de904db8 --- /dev/null +++ b/.changeset/eighty-crabs-listen.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +Optimize `Strings.equal` diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 33d7bbf59..050f6f9ca 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -80,6 +80,6 @@ library Strings { * @dev Returns true if the two strings are equal. */ function equal(string memory a, string memory b) internal pure returns (bool) { - return keccak256(bytes(a)) == keccak256(bytes(b)); + return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b)); } } From 6c14de4f0c4e5b86762360f96348b095551d936e Mon Sep 17 00:00:00 2001 From: Pascal Marco Caversaccio Date: Tue, 6 Jun 2023 19:00:01 +0300 Subject: [PATCH 156/197] `ECDSA`: Use hexadecimal literals (#4317) --- contracts/utils/cryptography/ECDSA.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/utils/cryptography/ECDSA.sol b/contracts/utils/cryptography/ECDSA.sol index f12815d9d..2ddbd9ba0 100644 --- a/contracts/utils/cryptography/ECDSA.sol +++ b/contracts/utils/cryptography/ECDSA.sol @@ -197,7 +197,7 @@ library ECDSA { /// @solidity memory-safe-assembly assembly { let ptr := mload(0x40) - mstore(ptr, "\x19\x01") + mstore(ptr, hex"19_01") mstore(add(ptr, 0x02), domainSeparator) mstore(add(ptr, 0x22), structHash) data := keccak256(ptr, 0x42) @@ -211,6 +211,6 @@ library ECDSA { * See {recover}. */ function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) { - return keccak256(abi.encodePacked("\x19\x00", validator, data)); + return keccak256(abi.encodePacked(hex"19_00", validator, data)); } } From 85696d80ad50246e9f7ec983fa325a3c40190033 Mon Sep 17 00:00:00 2001 From: Francisco Date: Tue, 6 Jun 2023 18:42:50 +0100 Subject: [PATCH 157/197] Remove further hardcoded function resolution (#4309) --- .changeset/red-dots-fold.md | 2 +- contracts/mocks/proxy/UUPSUpgradeableMock.sol | 4 ++-- contracts/proxy/ERC1967/ERC1967Proxy.sol | 2 +- .../token/ERC721/extensions/ERC721Enumerable.sol | 12 +++++++----- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.changeset/red-dots-fold.md b/.changeset/red-dots-fold.md index 6df4c8f0c..08cc77843 100644 --- a/.changeset/red-dots-fold.md +++ b/.changeset/red-dots-fold.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': major --- -Overrides are now used internally for a number of functions that were previously hardcoded to their default implementation in certain locations: `ERC1155Supply.totalSupply`, `ERC721.ownerOf`, `ERC721.balanceOf` in `ERC721Enumerable`, and `ERC20.totalSupply` in `ERC20FlashMint`. +Overrides are now used internally for a number of functions that were previously hardcoded to their default implementation in certain locations: `ERC1155Supply.totalSupply`, `ERC721.ownerOf`, `ERC721.balanceOf` and `ERC721.totalSupply` in `ERC721Enumerable`, `ERC20.totalSupply` in `ERC20FlashMint`, and `ERC1967._getImplementation` in `ERC1967Proxy`. diff --git a/contracts/mocks/proxy/UUPSUpgradeableMock.sol b/contracts/mocks/proxy/UUPSUpgradeableMock.sol index 567f3b536..60eed4c93 100644 --- a/contracts/mocks/proxy/UUPSUpgradeableMock.sol +++ b/contracts/mocks/proxy/UUPSUpgradeableMock.sol @@ -23,10 +23,10 @@ contract UUPSUpgradeableMock is NonUpgradeableMock, UUPSUpgradeable { contract UUPSUpgradeableUnsafeMock is UUPSUpgradeableMock { function upgradeTo(address newImplementation) public override { - ERC1967Upgrade._upgradeToAndCall(newImplementation, bytes(""), false); + _upgradeToAndCall(newImplementation, bytes(""), false); } function upgradeToAndCall(address newImplementation, bytes memory data) public payable override { - ERC1967Upgrade._upgradeToAndCall(newImplementation, data, false); + _upgradeToAndCall(newImplementation, data, false); } } diff --git a/contracts/proxy/ERC1967/ERC1967Proxy.sol b/contracts/proxy/ERC1967/ERC1967Proxy.sol index 320e026fa..ea5c204f8 100644 --- a/contracts/proxy/ERC1967/ERC1967Proxy.sol +++ b/contracts/proxy/ERC1967/ERC1967Proxy.sol @@ -31,6 +31,6 @@ contract ERC1967Proxy is Proxy, ERC1967Upgrade { * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` */ function _implementation() internal view virtual override returns (address impl) { - return ERC1967Upgrade._getImplementation(); + return _getImplementation(); } } diff --git a/contracts/token/ERC721/extensions/ERC721Enumerable.sol b/contracts/token/ERC721/extensions/ERC721Enumerable.sol index a9513b21d..2168e100b 100644 --- a/contracts/token/ERC721/extensions/ERC721Enumerable.sol +++ b/contracts/token/ERC721/extensions/ERC721Enumerable.sol @@ -7,9 +7,11 @@ import "../ERC721.sol"; import "./IERC721Enumerable.sol"; /** - * @dev This implements an optional extension of {ERC721} defined in the EIP that adds - * enumerability of all the token ids in the contract as well as all token ids owned by each - * account. + * @dev This implements an optional extension of {ERC721} defined in the EIP that adds enumerability + * of all the token ids in the contract as well as all token ids owned by each account. + * + * CAUTION: `ERC721` extensions that implement custom `balanceOf` logic, such as `ERC721Consecutive`, + * interfere with enumerability and should not be used together with `ERC721Enumerable`. */ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { // Mapping from owner to list of owned token IDs @@ -50,7 +52,7 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { * @dev See {IERC721Enumerable-tokenByIndex}. */ function tokenByIndex(uint256 index) public view virtual override returns (uint256) { - require(index < ERC721Enumerable.totalSupply(), "ERC721Enumerable: global index out of bounds"); + require(index < totalSupply(), "ERC721Enumerable: global index out of bounds"); return _allTokens[index]; } @@ -116,7 +118,7 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { // To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and // then delete the last slot (swap and pop). - uint256 lastTokenIndex = ERC721.balanceOf(from) - 1; + uint256 lastTokenIndex = balanceOf(from) - 1; uint256 tokenIndex = _ownedTokensIndex[tokenId]; // When the token to delete is the last token, the swap operation is unnecessary From 4fd2f8be339e850c32206342c3f9a1a7bedbb204 Mon Sep 17 00:00:00 2001 From: Balaji Shetty Pachai <32358081+balajipachai@users.noreply.github.com> Date: Wed, 7 Jun 2023 02:02:55 +0530 Subject: [PATCH 158/197] Replace abi.encodeWithSelector & abi.encodeWithSignature with abi.encodeCall (#4293) Co-authored-by: Hadrien Croubois --- .changeset/big-plums-cover.md | 4 ++++ contracts/mocks/MulticallTest.sol | 2 +- contracts/mocks/ReentrancyMock.sol | 2 +- contracts/mocks/proxy/UUPSLegacy.sol | 5 +---- contracts/token/ERC20/extensions/ERC4626.sol | 2 +- contracts/token/ERC20/utils/SafeERC20.sol | 8 ++++---- contracts/utils/cryptography/SignatureChecker.sol | 2 +- contracts/utils/introspection/ERC165Checker.sol | 2 +- 8 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 .changeset/big-plums-cover.md diff --git a/.changeset/big-plums-cover.md b/.changeset/big-plums-cover.md new file mode 100644 index 000000000..411156253 --- /dev/null +++ b/.changeset/big-plums-cover.md @@ -0,0 +1,4 @@ +--- +'openzeppelin-solidity': major +--- +Use `abi.encodeCall` in place of `abi.encodeWithSelector` and `abi.encodeWithSignature` for improved type-checking of parameters diff --git a/contracts/mocks/MulticallTest.sol b/contracts/mocks/MulticallTest.sol index 090f73cb4..cf89c58df 100644 --- a/contracts/mocks/MulticallTest.sol +++ b/contracts/mocks/MulticallTest.sol @@ -12,7 +12,7 @@ contract MulticallTest { ) external { bytes[] memory calls = new bytes[](recipients.length); for (uint256 i = 0; i < recipients.length; i++) { - calls[i] = abi.encodeWithSignature("transfer(address,uint256)", recipients[i], amounts[i]); + calls[i] = abi.encodeCall(multicallToken.transfer, (recipients[i], amounts[i])); } bytes[] memory results = multicallToken.multicall(calls); diff --git a/contracts/mocks/ReentrancyMock.sol b/contracts/mocks/ReentrancyMock.sol index 104d4f42a..b4819dd59 100644 --- a/contracts/mocks/ReentrancyMock.sol +++ b/contracts/mocks/ReentrancyMock.sol @@ -26,7 +26,7 @@ contract ReentrancyMock is ReentrancyGuard { function countThisRecursive(uint256 n) public nonReentrant { if (n > 0) { _count(); - (bool success, ) = address(this).call(abi.encodeWithSignature("countThisRecursive(uint256)", n - 1)); + (bool success, ) = address(this).call(abi.encodeCall(this.countThisRecursive, (n - 1))); require(success, "ReentrancyMock: failed call"); } } diff --git a/contracts/mocks/proxy/UUPSLegacy.sol b/contracts/mocks/proxy/UUPSLegacy.sol index ed243519e..f8ea7214b 100644 --- a/contracts/mocks/proxy/UUPSLegacy.sol +++ b/contracts/mocks/proxy/UUPSLegacy.sol @@ -31,10 +31,7 @@ contract UUPSUpgradeableLegacyMock is UUPSUpgradeableMock { if (!rollbackTesting.value) { // Trigger rollback using upgradeTo from the new implementation rollbackTesting.value = true; - Address.functionDelegateCall( - newImplementation, - abi.encodeWithSignature("upgradeTo(address)", oldImplementation) - ); + Address.functionDelegateCall(newImplementation, abi.encodeCall(this.upgradeTo, (oldImplementation))); rollbackTesting.value = false; // Check rollback was effective require(oldImplementation == _getImplementation(), "ERC1967Upgrade: upgrade breaks further upgrades"); diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index a706b5457..54f5ae24b 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -67,7 +67,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { */ function _tryGetAssetDecimals(IERC20 asset_) private view returns (bool, uint8) { (bool success, bytes memory encodedDecimals) = address(asset_).staticcall( - abi.encodeWithSelector(IERC20Metadata.decimals.selector) + abi.encodeCall(IERC20Metadata.decimals, ()) ); if (success && encodedDecimals.length >= 32) { uint256 returnedDecimals = abi.decode(encodedDecimals, (uint256)); diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index 751b82735..b1532d1cb 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -24,7 +24,7 @@ library SafeERC20 { * non-reverting calls are assumed to be successful. */ function safeTransfer(IERC20 token, address to, uint256 value) internal { - _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); + _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value))); } /** @@ -32,7 +32,7 @@ library SafeERC20 { * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful. */ function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { - _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); + _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value))); } /** @@ -62,10 +62,10 @@ library SafeERC20 { * 0 before setting it to a non-zero value. */ function forceApprove(IERC20 token, address spender, uint256 value) internal { - bytes memory approvalCall = abi.encodeWithSelector(token.approve.selector, spender, value); + bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value)); if (!_callOptionalReturnBool(token, approvalCall)) { - _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, 0)); + _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0))); _callOptionalReturn(token, approvalCall); } } diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index ba8f7cf36..941f7538f 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -41,7 +41,7 @@ library SignatureChecker { bytes memory signature ) internal view returns (bool) { (bool success, bytes memory result) = signer.staticcall( - abi.encodeWithSelector(IERC1271.isValidSignature.selector, hash, signature) + abi.encodeCall(IERC1271.isValidSignature, (hash, signature)) ); return (success && result.length >= 32 && diff --git a/contracts/utils/introspection/ERC165Checker.sol b/contracts/utils/introspection/ERC165Checker.sol index 4dc84c163..c27b2f17d 100644 --- a/contracts/utils/introspection/ERC165Checker.sol +++ b/contracts/utils/introspection/ERC165Checker.sol @@ -109,7 +109,7 @@ library ERC165Checker { */ function supportsERC165InterfaceUnchecked(address account, bytes4 interfaceId) internal view returns (bool) { // prepare call - bytes memory encodedParams = abi.encodeWithSelector(IERC165.supportsInterface.selector, interfaceId); + bytes memory encodedParams = abi.encodeCall(IERC165.supportsInterface, (interfaceId)); // perform static call bool success; From df2778f38ecf196d6ecab6df9b958fb5169d3463 Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Tue, 6 Jun 2023 21:13:08 -0300 Subject: [PATCH 159/197] Remove override interface implementations (#4315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- .changeset/violet-dancers-cough.md | 5 +++ contracts/access/AccessControl.sol | 10 +++--- contracts/access/AccessControlEnumerable.sol | 4 +-- contracts/governance/Governor.sol | 12 ++----- contracts/governance/IGovernor.sol | 4 +-- contracts/governance/TimelockController.sol | 12 ++----- contracts/governance/utils/Votes.sol | 16 +++++----- contracts/mocks/ERC1271WalletMock.sol | 4 +-- contracts/mocks/ERC3156FlashBorrowerMock.sol | 2 +- contracts/mocks/token/ERC1155ReceiverMock.sol | 4 +-- .../mocks/token/ERC20VotesLegacyMock.sol | 12 +++---- contracts/mocks/token/ERC721ReceiverMock.sol | 2 +- contracts/proxy/beacon/UpgradeableBeacon.sol | 2 +- contracts/proxy/utils/UUPSUpgradeable.sol | 4 +-- contracts/token/ERC1155/ERC1155.sol | 20 ++++-------- contracts/token/ERC20/ERC20.sol | 18 +++++------ .../token/ERC20/extensions/ERC20FlashMint.sol | 6 ++-- .../token/ERC20/extensions/ERC20Permit.sol | 4 +-- contracts/token/ERC20/extensions/ERC4626.sol | 32 +++++++++---------- contracts/token/ERC721/ERC721.sol | 24 +++++++------- .../ERC721/extensions/ERC721Enumerable.sol | 6 ++-- .../token/ERC721/extensions/ERC721Wrapper.sol | 7 +--- contracts/token/ERC721/utils/ERC721Holder.sol | 2 +- contracts/token/common/ERC2981.sol | 2 +- contracts/utils/cryptography/EIP712.sol | 1 - contracts/utils/introspection/ERC165.sol | 2 +- 26 files changed, 99 insertions(+), 118 deletions(-) create mode 100644 .changeset/violet-dancers-cough.md diff --git a/.changeset/violet-dancers-cough.md b/.changeset/violet-dancers-cough.md new file mode 100644 index 000000000..c6160d287 --- /dev/null +++ b/.changeset/violet-dancers-cough.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +Remove the `override` specifier from functions that only override a single interface function. diff --git a/contracts/access/AccessControl.sol b/contracts/access/AccessControl.sol index 82a43933d..df16dbdab 100644 --- a/contracts/access/AccessControl.sol +++ b/contracts/access/AccessControl.sol @@ -82,7 +82,7 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { /** * @dev Returns `true` if `account` has been granted `role`. */ - function hasRole(bytes32 role, address account) public view virtual override returns (bool) { + function hasRole(bytes32 role, address account) public view virtual returns (bool) { return _roles[role].members[account]; } @@ -126,7 +126,7 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { * * To change a role's admin, use {_setRoleAdmin}. */ - function getRoleAdmin(bytes32 role) public view virtual override returns (bytes32) { + function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) { return _roles[role].adminRole; } @@ -142,7 +142,7 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { * * May emit a {RoleGranted} event. */ - function grantRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) { + function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) { _grantRole(role, account); } @@ -157,7 +157,7 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { * * May emit a {RoleRevoked} event. */ - function revokeRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) { + function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) { _revokeRole(role, account); } @@ -177,7 +177,7 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { * * May emit a {RoleRevoked} event. */ - function renounceRole(bytes32 role, address account) public virtual override { + function renounceRole(bytes32 role, address account) public virtual { require(account == _msgSender(), "AccessControl: can only renounce roles for self"); _revokeRole(role, account); diff --git a/contracts/access/AccessControlEnumerable.sol b/contracts/access/AccessControlEnumerable.sol index 1c37a30a9..297d34536 100644 --- a/contracts/access/AccessControlEnumerable.sol +++ b/contracts/access/AccessControlEnumerable.sol @@ -34,7 +34,7 @@ abstract contract AccessControlEnumerable is IAccessControlEnumerable, AccessCon * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] * for more information. */ - function getRoleMember(bytes32 role, uint256 index) public view virtual override returns (address) { + function getRoleMember(bytes32 role, uint256 index) public view virtual returns (address) { return _roleMembers[role].at(index); } @@ -42,7 +42,7 @@ abstract contract AccessControlEnumerable is IAccessControlEnumerable, AccessCon * @dev Returns the number of accounts that have `role`. Can be used * together with {getRoleMember} to enumerate all bearers of a role. */ - function getRoleMemberCount(bytes32 role) public view virtual override returns (uint256) { + function getRoleMemberCount(bytes32 role) public view virtual returns (uint256) { return _roleMembers[role].length(); } diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index 4ff8c3492..74d60c742 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -605,20 +605,14 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive /** * @dev See {IERC721Receiver-onERC721Received}. */ - function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { + function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) { return this.onERC721Received.selector; } /** * @dev See {IERC1155Receiver-onERC1155Received}. */ - function onERC1155Received( - address, - address, - uint256, - uint256, - bytes memory - ) public virtual override returns (bytes4) { + function onERC1155Received(address, address, uint256, uint256, bytes memory) public virtual returns (bytes4) { return this.onERC1155Received.selector; } @@ -631,7 +625,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive uint256[] memory, uint256[] memory, bytes memory - ) public virtual override returns (bytes4) { + ) public virtual returns (bytes4) { return this.onERC1155BatchReceived.selector; } } diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 245c5d97a..992b5ca10 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -86,14 +86,14 @@ abstract contract IGovernor is IERC165, IERC6372 { * @notice module:core * @dev See {IERC6372} */ - function clock() public view virtual override returns (uint48); + function clock() public view virtual returns (uint48); /** * @notice module:core * @dev See EIP-6372. */ // solhint-disable-next-line func-name-mixedcase - function CLOCK_MODE() public view virtual override returns (string memory); + function CLOCK_MODE() public view virtual returns (string memory); /** * @notice module:voting diff --git a/contracts/governance/TimelockController.sol b/contracts/governance/TimelockController.sol index 5f09161df..9930d6a49 100644 --- a/contracts/governance/TimelockController.sol +++ b/contracts/governance/TimelockController.sol @@ -384,20 +384,14 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver /** * @dev See {IERC721Receiver-onERC721Received}. */ - function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { + function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) { return this.onERC721Received.selector; } /** * @dev See {IERC1155Receiver-onERC1155Received}. */ - function onERC1155Received( - address, - address, - uint256, - uint256, - bytes memory - ) public virtual override returns (bytes4) { + function onERC1155Received(address, address, uint256, uint256, bytes memory) public virtual returns (bytes4) { return this.onERC1155Received.selector; } @@ -410,7 +404,7 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver uint256[] memory, uint256[] memory, bytes memory - ) public virtual override returns (bytes4) { + ) public virtual returns (bytes4) { return this.onERC1155BatchReceived.selector; } } diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index 94f6d4fb9..5fc15da29 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -46,7 +46,7 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { * @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. */ - function clock() public view virtual override returns (uint48) { + function clock() public view virtual returns (uint48) { return SafeCast.toUint48(block.number); } @@ -54,7 +54,7 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { * @dev Machine-readable description of the clock as specified in EIP-6372. */ // solhint-disable-next-line func-name-mixedcase - function CLOCK_MODE() public view virtual override returns (string memory) { + function CLOCK_MODE() public view virtual returns (string memory) { // Check that the clock was not modified require(clock() == block.number, "Votes: broken clock mode"); return "mode=blocknumber&from=default"; @@ -63,7 +63,7 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { /** * @dev Returns the current amount of votes that `account` has. */ - function getVotes(address account) public view virtual override returns (uint256) { + function getVotes(address account) public view virtual returns (uint256) { return _delegateCheckpoints[account].latest(); } @@ -75,7 +75,7 @@ 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 override returns (uint256) { + function getPastVotes(address account, uint256 timepoint) public view virtual returns (uint256) { require(timepoint < clock(), "Votes: future lookup"); return _delegateCheckpoints[account].upperLookupRecent(SafeCast.toUint32(timepoint)); } @@ -92,7 +92,7 @@ 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 override returns (uint256) { + function getPastTotalSupply(uint256 timepoint) public view virtual returns (uint256) { require(timepoint < clock(), "Votes: future lookup"); return _totalCheckpoints.upperLookupRecent(SafeCast.toUint32(timepoint)); } @@ -107,14 +107,14 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { /** * @dev Returns the delegate that `account` has chosen. */ - function delegates(address account) public view virtual override returns (address) { + function delegates(address account) public view virtual returns (address) { return _delegation[account]; } /** * @dev Delegates votes from the sender to `delegatee`. */ - function delegate(address delegatee) public virtual override { + function delegate(address delegatee) public virtual { address account = _msgSender(); _delegate(account, delegatee); } @@ -129,7 +129,7 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { uint8 v, bytes32 r, bytes32 s - ) public virtual override { + ) public virtual { require(block.timestamp <= expiry, "Votes: signature expired"); address signer = ECDSA.recover( _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), diff --git a/contracts/mocks/ERC1271WalletMock.sol b/contracts/mocks/ERC1271WalletMock.sol index a28fd0fb9..2bc390636 100644 --- a/contracts/mocks/ERC1271WalletMock.sol +++ b/contracts/mocks/ERC1271WalletMock.sol @@ -9,13 +9,13 @@ import "../utils/cryptography/ECDSA.sol"; contract ERC1271WalletMock is Ownable, IERC1271 { constructor(address originalOwner) Ownable(originalOwner) {} - function isValidSignature(bytes32 hash, bytes memory signature) public view override returns (bytes4 magicValue) { + function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bytes4 magicValue) { return ECDSA.recover(hash, signature) == owner() ? this.isValidSignature.selector : bytes4(0); } } contract ERC1271MaliciousMock is IERC1271 { - function isValidSignature(bytes32, bytes memory) public pure override returns (bytes4) { + function isValidSignature(bytes32, bytes memory) public pure returns (bytes4) { assembly { mstore(0, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) return(0, 32) diff --git a/contracts/mocks/ERC3156FlashBorrowerMock.sol b/contracts/mocks/ERC3156FlashBorrowerMock.sol index 8d24af6c1..abf836606 100644 --- a/contracts/mocks/ERC3156FlashBorrowerMock.sol +++ b/contracts/mocks/ERC3156FlashBorrowerMock.sol @@ -33,7 +33,7 @@ contract ERC3156FlashBorrowerMock is IERC3156FlashBorrower { uint256 amount, uint256 fee, bytes calldata data - ) public override returns (bytes32) { + ) public returns (bytes32) { require(msg.sender == token); emit BalanceOf(token, address(this), IERC20(token).balanceOf(address(this))); diff --git a/contracts/mocks/token/ERC1155ReceiverMock.sol b/contracts/mocks/token/ERC1155ReceiverMock.sol index 9f75f002b..2b591c058 100644 --- a/contracts/mocks/token/ERC1155ReceiverMock.sol +++ b/contracts/mocks/token/ERC1155ReceiverMock.sol @@ -27,7 +27,7 @@ contract ERC1155ReceiverMock is ERC165, IERC1155Receiver { uint256 id, uint256 value, bytes calldata data - ) external override returns (bytes4) { + ) external returns (bytes4) { require(!_recReverts, "ERC1155ReceiverMock: reverting on receive"); emit Received(operator, from, id, value, data, gasleft()); return _recRetval; @@ -39,7 +39,7 @@ contract ERC1155ReceiverMock is ERC165, IERC1155Receiver { uint256[] calldata ids, uint256[] calldata values, bytes calldata data - ) external override returns (bytes4) { + ) external returns (bytes4) { require(!_batReverts, "ERC1155ReceiverMock: reverting on batch receive"); emit BatchReceived(operator, from, ids, values, data, gasleft()); return _batRetval; diff --git a/contracts/mocks/token/ERC20VotesLegacyMock.sol b/contracts/mocks/token/ERC20VotesLegacyMock.sol index cf8afa58a..88ba84236 100644 --- a/contracts/mocks/token/ERC20VotesLegacyMock.sol +++ b/contracts/mocks/token/ERC20VotesLegacyMock.sol @@ -41,14 +41,14 @@ abstract contract ERC20VotesLegacyMock is IVotes, ERC20Permit { /** * @dev Get the address `account` is currently delegating to. */ - function delegates(address account) public view virtual override returns (address) { + function delegates(address account) public view virtual returns (address) { return _delegates[account]; } /** * @dev Gets the current votes balance for `account` */ - function getVotes(address account) public view virtual override returns (uint256) { + function getVotes(address account) public view virtual returns (uint256) { uint256 pos = _checkpoints[account].length; unchecked { return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes; @@ -62,7 +62,7 @@ abstract contract ERC20VotesLegacyMock is IVotes, ERC20Permit { * * - `blockNumber` must have been already mined */ - function getPastVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { + function getPastVotes(address account, uint256 blockNumber) public view virtual returns (uint256) { require(blockNumber < block.number, "ERC20Votes: block not yet mined"); return _checkpointsLookup(_checkpoints[account], blockNumber); } @@ -75,7 +75,7 @@ abstract contract ERC20VotesLegacyMock is IVotes, ERC20Permit { * * - `blockNumber` must have been already mined */ - function getPastTotalSupply(uint256 blockNumber) public view virtual override returns (uint256) { + function getPastTotalSupply(uint256 blockNumber) public view virtual returns (uint256) { require(blockNumber < block.number, "ERC20Votes: block not yet mined"); return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber); } @@ -127,7 +127,7 @@ abstract contract ERC20VotesLegacyMock is IVotes, ERC20Permit { /** * @dev Delegate votes from the sender to `delegatee`. */ - function delegate(address delegatee) public virtual override { + function delegate(address delegatee) public virtual { _delegate(_msgSender(), delegatee); } @@ -141,7 +141,7 @@ abstract contract ERC20VotesLegacyMock is IVotes, ERC20Permit { uint8 v, bytes32 r, bytes32 s - ) public virtual override { + ) public virtual { require(block.timestamp <= expiry, "ERC20Votes: signature expired"); address signer = ECDSA.recover( _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), diff --git a/contracts/mocks/token/ERC721ReceiverMock.sol b/contracts/mocks/token/ERC721ReceiverMock.sol index 01526566a..e97ceacf3 100644 --- a/contracts/mocks/token/ERC721ReceiverMock.sol +++ b/contracts/mocks/token/ERC721ReceiverMock.sol @@ -27,7 +27,7 @@ contract ERC721ReceiverMock is IERC721Receiver { address from, uint256 tokenId, bytes memory data - ) public override returns (bytes4) { + ) public returns (bytes4) { if (_error == Error.RevertWithMessage) { revert("ERC721ReceiverMock: reverting"); } else if (_error == Error.RevertWithoutMessage) { diff --git a/contracts/proxy/beacon/UpgradeableBeacon.sol b/contracts/proxy/beacon/UpgradeableBeacon.sol index 24d167f60..37d27f67a 100644 --- a/contracts/proxy/beacon/UpgradeableBeacon.sol +++ b/contracts/proxy/beacon/UpgradeableBeacon.sol @@ -30,7 +30,7 @@ contract UpgradeableBeacon is IBeacon, Ownable { /** * @dev Returns the current implementation address. */ - function implementation() public view virtual override returns (address) { + function implementation() public view virtual returns (address) { return _implementation; } diff --git a/contracts/proxy/utils/UUPSUpgradeable.sol b/contracts/proxy/utils/UUPSUpgradeable.sol index c281c9091..1fd73247c 100644 --- a/contracts/proxy/utils/UUPSUpgradeable.sol +++ b/contracts/proxy/utils/UUPSUpgradeable.sol @@ -52,7 +52,7 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this * function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier. */ - function proxiableUUID() external view virtual override notDelegated returns (bytes32) { + function proxiableUUID() external view virtual notDelegated returns (bytes32) { return _IMPLEMENTATION_SLOT; } @@ -92,7 +92,7 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}. * * ```solidity - * function _authorizeUpgrade(address) internal override onlyOwner {} + * function _authorizeUpgrade(address) internal onlyOwner {} * ``` */ function _authorizeUpgrade(address newImplementation) internal virtual; diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol index 6761edd24..c2f217d7e 100644 --- a/contracts/token/ERC1155/ERC1155.sol +++ b/contracts/token/ERC1155/ERC1155.sol @@ -53,7 +53,7 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * Clients calling this function must replace the `\{id\}` substring with the * actual token type ID. */ - function uri(uint256) public view virtual override returns (string memory) { + function uri(uint256) public view virtual returns (string memory) { return _uri; } @@ -64,7 +64,7 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * * - `account` cannot be the zero address. */ - function balanceOf(address account, uint256 id) public view virtual override returns (uint256) { + function balanceOf(address account, uint256 id) public view virtual returns (uint256) { return _balances[id][account]; } @@ -78,7 +78,7 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { function balanceOfBatch( address[] memory accounts, uint256[] memory ids - ) public view virtual override returns (uint256[] memory) { + ) public view virtual returns (uint256[] memory) { require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch"); uint256[] memory batchBalances = new uint256[](accounts.length); @@ -93,27 +93,21 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { /** * @dev See {IERC1155-setApprovalForAll}. */ - function setApprovalForAll(address operator, bool approved) public virtual override { + function setApprovalForAll(address operator, bool approved) public virtual { _setApprovalForAll(_msgSender(), operator, approved); } /** * @dev See {IERC1155-isApprovedForAll}. */ - function isApprovedForAll(address account, address operator) public view virtual override returns (bool) { + function isApprovedForAll(address account, address operator) public view virtual returns (bool) { return _operatorApprovals[account][operator]; } /** * @dev See {IERC1155-safeTransferFrom}. */ - function safeTransferFrom( - address from, - address to, - uint256 id, - uint256 amount, - bytes memory data - ) public virtual override { + 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" @@ -130,7 +124,7 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { uint256[] memory ids, uint256[] memory amounts, bytes memory data - ) public virtual override { + ) public virtual { require( from == _msgSender() || isApprovedForAll(from, _msgSender()), "ERC1155: caller is not token owner or approved" diff --git a/contracts/token/ERC20/ERC20.sol b/contracts/token/ERC20/ERC20.sol index 2646a0ddc..4db525a7a 100644 --- a/contracts/token/ERC20/ERC20.sol +++ b/contracts/token/ERC20/ERC20.sol @@ -58,7 +58,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata { /** * @dev Returns the name of the token. */ - function name() public view virtual override returns (string memory) { + function name() public view virtual returns (string memory) { return _name; } @@ -66,7 +66,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * @dev Returns the symbol of the token, usually a shorter version of the * name. */ - function symbol() public view virtual override returns (string memory) { + function symbol() public view virtual returns (string memory) { return _symbol; } @@ -83,21 +83,21 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * no way affects any of the arithmetic of the contract, including * {IERC20-balanceOf} and {IERC20-transfer}. */ - function decimals() public view virtual override returns (uint8) { + function decimals() public view virtual returns (uint8) { return 18; } /** * @dev See {IERC20-totalSupply}. */ - function totalSupply() public view virtual override returns (uint256) { + function totalSupply() public view virtual returns (uint256) { return _totalSupply; } /** * @dev See {IERC20-balanceOf}. */ - function balanceOf(address account) public view virtual override returns (uint256) { + function balanceOf(address account) public view virtual returns (uint256) { return _balances[account]; } @@ -109,7 +109,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * - `to` cannot be the zero address. * - the caller must have a balance of at least `amount`. */ - function transfer(address to, uint256 amount) public virtual override returns (bool) { + function transfer(address to, uint256 amount) public virtual returns (bool) { address owner = _msgSender(); _transfer(owner, to, amount); return true; @@ -118,7 +118,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata { /** * @dev See {IERC20-allowance}. */ - function allowance(address owner, address spender) public view virtual override returns (uint256) { + function allowance(address owner, address spender) public view virtual returns (uint256) { return _allowances[owner][spender]; } @@ -132,7 +132,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * * - `spender` cannot be the zero address. */ - function approve(address spender, uint256 amount) public virtual override returns (bool) { + function approve(address spender, uint256 amount) public virtual returns (bool) { address owner = _msgSender(); _approve(owner, spender, amount); return true; @@ -154,7 +154,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * - the caller must have allowance for ``from``'s tokens of at least * `amount`. */ - function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) { + function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) { address spender = _msgSender(); _spendAllowance(from, spender, amount); _transfer(from, to, amount); diff --git a/contracts/token/ERC20/extensions/ERC20FlashMint.sol b/contracts/token/ERC20/extensions/ERC20FlashMint.sol index 18b0a81c6..7a4076678 100644 --- a/contracts/token/ERC20/extensions/ERC20FlashMint.sol +++ b/contracts/token/ERC20/extensions/ERC20FlashMint.sol @@ -24,7 +24,7 @@ abstract contract ERC20FlashMint is ERC20, IERC3156FlashLender { * @param token The address of the token that is requested. * @return The amount of token that can be loaned. */ - function maxFlashLoan(address token) public view virtual override returns (uint256) { + function maxFlashLoan(address token) public view virtual returns (uint256) { return token == address(this) ? type(uint256).max - totalSupply() : 0; } @@ -36,7 +36,7 @@ abstract contract ERC20FlashMint is ERC20, IERC3156FlashLender { * @param amount The amount of tokens to be loaned. * @return The fees applied to the corresponding flash loan. */ - function flashFee(address token, uint256 amount) public view virtual override returns (uint256) { + function flashFee(address token, uint256 amount) public view virtual returns (uint256) { require(token == address(this), "ERC20FlashMint: wrong token"); return _flashFee(token, amount); } @@ -88,7 +88,7 @@ abstract contract ERC20FlashMint is ERC20, IERC3156FlashLender { address token, uint256 amount, bytes calldata data - ) public virtual override returns (bool) { + ) public virtual returns (bool) { require(amount <= maxFlashLoan(token), "ERC20FlashMint: amount exceeds maxFlashLoan"); uint256 fee = flashFee(token, amount); _mint(address(receiver), amount); diff --git a/contracts/token/ERC20/extensions/ERC20Permit.sol b/contracts/token/ERC20/extensions/ERC20Permit.sol index e7d43c2dd..9379e4451 100644 --- a/contracts/token/ERC20/extensions/ERC20Permit.sol +++ b/contracts/token/ERC20/extensions/ERC20Permit.sol @@ -42,7 +42,7 @@ abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { uint8 v, bytes32 r, bytes32 s - ) public virtual override { + ) public virtual { require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); @@ -66,7 +66,7 @@ abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { * @dev See {IERC20Permit-DOMAIN_SEPARATOR}. */ // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view virtual override returns (bytes32) { + function DOMAIN_SEPARATOR() external view virtual returns (bytes32) { return _domainSeparatorV4(); } } diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index 54f5ae24b..665c95a02 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -90,67 +90,67 @@ abstract contract ERC4626 is ERC20, IERC4626 { } /** @dev See {IERC4626-asset}. */ - function asset() public view virtual override returns (address) { + function asset() public view virtual returns (address) { return address(_asset); } /** @dev See {IERC4626-totalAssets}. */ - function totalAssets() public view virtual override returns (uint256) { + function totalAssets() public view virtual returns (uint256) { return _asset.balanceOf(address(this)); } /** @dev See {IERC4626-convertToShares}. */ - function convertToShares(uint256 assets) public view virtual override returns (uint256) { + function convertToShares(uint256 assets) public view virtual returns (uint256) { return _convertToShares(assets, Math.Rounding.Down); } /** @dev See {IERC4626-convertToAssets}. */ - function convertToAssets(uint256 shares) public view virtual override returns (uint256) { + function convertToAssets(uint256 shares) public view virtual returns (uint256) { return _convertToAssets(shares, Math.Rounding.Down); } /** @dev See {IERC4626-maxDeposit}. */ - function maxDeposit(address) public view virtual override returns (uint256) { + function maxDeposit(address) public view virtual returns (uint256) { return type(uint256).max; } /** @dev See {IERC4626-maxMint}. */ - function maxMint(address) public view virtual override returns (uint256) { + function maxMint(address) public view virtual returns (uint256) { return type(uint256).max; } /** @dev See {IERC4626-maxWithdraw}. */ - function maxWithdraw(address owner) public view virtual override returns (uint256) { + function maxWithdraw(address owner) public view virtual returns (uint256) { return _convertToAssets(balanceOf(owner), Math.Rounding.Down); } /** @dev See {IERC4626-maxRedeem}. */ - function maxRedeem(address owner) public view virtual override returns (uint256) { + function maxRedeem(address owner) public view virtual returns (uint256) { return balanceOf(owner); } /** @dev See {IERC4626-previewDeposit}. */ - function previewDeposit(uint256 assets) public view virtual override returns (uint256) { + function previewDeposit(uint256 assets) public view virtual returns (uint256) { return _convertToShares(assets, Math.Rounding.Down); } /** @dev See {IERC4626-previewMint}. */ - function previewMint(uint256 shares) public view virtual override returns (uint256) { + function previewMint(uint256 shares) public view virtual returns (uint256) { return _convertToAssets(shares, Math.Rounding.Up); } /** @dev See {IERC4626-previewWithdraw}. */ - function previewWithdraw(uint256 assets) public view virtual override returns (uint256) { + function previewWithdraw(uint256 assets) public view virtual returns (uint256) { return _convertToShares(assets, Math.Rounding.Up); } /** @dev See {IERC4626-previewRedeem}. */ - function previewRedeem(uint256 shares) public view virtual override returns (uint256) { + function previewRedeem(uint256 shares) public view virtual returns (uint256) { return _convertToAssets(shares, Math.Rounding.Down); } /** @dev See {IERC4626-deposit}. */ - function deposit(uint256 assets, address receiver) public virtual override returns (uint256) { + function deposit(uint256 assets, address receiver) public virtual returns (uint256) { require(assets <= maxDeposit(receiver), "ERC4626: deposit more than max"); uint256 shares = previewDeposit(assets); @@ -164,7 +164,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { * As opposed to {deposit}, minting is allowed even if the vault is in a state where the price of a share is zero. * In this case, the shares will be minted without requiring any assets to be deposited. */ - function mint(uint256 shares, address receiver) public virtual override returns (uint256) { + function mint(uint256 shares, address receiver) public virtual returns (uint256) { require(shares <= maxMint(receiver), "ERC4626: mint more than max"); uint256 assets = previewMint(shares); @@ -174,7 +174,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { } /** @dev See {IERC4626-withdraw}. */ - function withdraw(uint256 assets, address receiver, address owner) public virtual override returns (uint256) { + function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256) { require(assets <= maxWithdraw(owner), "ERC4626: withdraw more than max"); uint256 shares = previewWithdraw(assets); @@ -184,7 +184,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { } /** @dev See {IERC4626-redeem}. */ - function redeem(uint256 shares, address receiver, address owner) public virtual override returns (uint256) { + function redeem(uint256 shares, address receiver, address owner) public virtual returns (uint256) { require(shares <= maxRedeem(owner), "ERC4626: redeem more than max"); uint256 assets = previewRedeem(shares); diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index af00ecaf4..3eb3f74cf 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -57,7 +57,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { /** * @dev See {IERC721-balanceOf}. */ - function balanceOf(address owner) public view virtual override returns (uint256) { + function balanceOf(address owner) public view virtual returns (uint256) { require(owner != address(0), "ERC721: address zero is not a valid owner"); return _balances[owner]; } @@ -65,7 +65,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { /** * @dev See {IERC721-ownerOf}. */ - function ownerOf(uint256 tokenId) public view virtual override returns (address) { + function ownerOf(uint256 tokenId) public view virtual returns (address) { address owner = _ownerOf(tokenId); require(owner != address(0), "ERC721: invalid token ID"); return owner; @@ -74,21 +74,21 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { /** * @dev See {IERC721Metadata-name}. */ - function name() public view virtual override returns (string memory) { + function name() public view virtual returns (string memory) { return _name; } /** * @dev See {IERC721Metadata-symbol}. */ - function symbol() public view virtual override returns (string memory) { + function symbol() public view virtual returns (string memory) { return _symbol; } /** * @dev See {IERC721Metadata-tokenURI}. */ - function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + function tokenURI(uint256 tokenId) public view virtual returns (string memory) { _requireMinted(tokenId); string memory baseURI = _baseURI(); @@ -107,7 +107,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { /** * @dev See {IERC721-approve}. */ - function approve(address to, uint256 tokenId) public virtual override { + function approve(address to, uint256 tokenId) public virtual { address owner = ownerOf(tokenId); require(to != owner, "ERC721: approval to current owner"); @@ -122,7 +122,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { /** * @dev See {IERC721-getApproved}. */ - function getApproved(uint256 tokenId) public view virtual override returns (address) { + function getApproved(uint256 tokenId) public view virtual returns (address) { _requireMinted(tokenId); return _tokenApprovals[tokenId]; @@ -131,21 +131,21 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { /** * @dev See {IERC721-setApprovalForAll}. */ - function setApprovalForAll(address operator, bool approved) public virtual override { + function setApprovalForAll(address operator, bool approved) public virtual { _setApprovalForAll(_msgSender(), operator, approved); } /** * @dev See {IERC721-isApprovedForAll}. */ - function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + function isApprovedForAll(address owner, address operator) public view virtual returns (bool) { return _operatorApprovals[owner][operator]; } /** * @dev See {IERC721-transferFrom}. */ - function transferFrom(address from, address to, uint256 tokenId) public virtual override { + 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"); @@ -155,14 +155,14 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { /** * @dev See {IERC721-safeTransferFrom}. */ - function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override { + function safeTransferFrom(address from, address to, uint256 tokenId) public virtual { safeTransferFrom(from, to, tokenId, ""); } /** * @dev See {IERC721-safeTransferFrom}. */ - function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override { + 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"); _safeTransfer(from, to, tokenId, data); } diff --git a/contracts/token/ERC721/extensions/ERC721Enumerable.sol b/contracts/token/ERC721/extensions/ERC721Enumerable.sol index 2168e100b..8cea9e19a 100644 --- a/contracts/token/ERC721/extensions/ERC721Enumerable.sol +++ b/contracts/token/ERC721/extensions/ERC721Enumerable.sol @@ -36,7 +36,7 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { /** * @dev See {IERC721Enumerable-tokenOfOwnerByIndex}. */ - function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual override returns (uint256) { + function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual returns (uint256) { require(index < balanceOf(owner), "ERC721Enumerable: owner index out of bounds"); return _ownedTokens[owner][index]; } @@ -44,14 +44,14 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { /** * @dev See {IERC721Enumerable-totalSupply}. */ - function totalSupply() public view virtual override returns (uint256) { + function totalSupply() public view virtual returns (uint256) { return _allTokens.length; } /** * @dev See {IERC721Enumerable-tokenByIndex}. */ - function tokenByIndex(uint256 index) public view virtual override returns (uint256) { + function tokenByIndex(uint256 index) public view virtual returns (uint256) { require(index < totalSupply(), "ERC721Enumerable: global index out of bounds"); return _allTokens[index]; } diff --git a/contracts/token/ERC721/extensions/ERC721Wrapper.sol b/contracts/token/ERC721/extensions/ERC721Wrapper.sol index 58346182b..9226349f4 100644 --- a/contracts/token/ERC721/extensions/ERC721Wrapper.sol +++ b/contracts/token/ERC721/extensions/ERC721Wrapper.sol @@ -67,12 +67,7 @@ abstract contract ERC721Wrapper is ERC721, IERC721Receiver { * WARNING: Doesn't work with unsafe transfers (eg. {IERC721-transferFrom}). Use {ERC721Wrapper-_recover} * for recovering in that scenario. */ - function onERC721Received( - address, - address from, - uint256 tokenId, - bytes memory - ) public virtual override returns (bytes4) { + function onERC721Received(address, address from, uint256 tokenId, bytes memory) public virtual returns (bytes4) { require(address(underlying()) == _msgSender(), "ERC721Wrapper: caller is not underlying"); _safeMint(from, tokenId); return IERC721Receiver.onERC721Received.selector; diff --git a/contracts/token/ERC721/utils/ERC721Holder.sol b/contracts/token/ERC721/utils/ERC721Holder.sol index 41bee820c..9f5b2e9f9 100644 --- a/contracts/token/ERC721/utils/ERC721Holder.sol +++ b/contracts/token/ERC721/utils/ERC721Holder.sol @@ -17,7 +17,7 @@ contract ERC721Holder is IERC721Receiver { * * Always returns `IERC721Receiver.onERC721Received.selector`. */ - function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { + function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) { return this.onERC721Received.selector; } } diff --git a/contracts/token/common/ERC2981.sol b/contracts/token/common/ERC2981.sol index 5d40388aa..85e902733 100644 --- a/contracts/token/common/ERC2981.sol +++ b/contracts/token/common/ERC2981.sol @@ -40,7 +40,7 @@ abstract contract ERC2981 is IERC2981, ERC165 { /** * @inheritdoc IERC2981 */ - function royaltyInfo(uint256 tokenId, uint256 salePrice) public view virtual override returns (address, uint256) { + function royaltyInfo(uint256 tokenId, uint256 salePrice) public view virtual returns (address, uint256) { RoyaltyInfo memory royalty = _tokenRoyaltyInfo[tokenId]; if (royalty.receiver == address(0)) { diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol index 526b13580..b9c9c9d19 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -118,7 +118,6 @@ abstract contract EIP712 is IERC5267 { public view virtual - override returns ( bytes1 fields, string memory name, diff --git a/contracts/utils/introspection/ERC165.sol b/contracts/utils/introspection/ERC165.sol index 6849637e8..111913007 100644 --- a/contracts/utils/introspection/ERC165.sol +++ b/contracts/utils/introspection/ERC165.sol @@ -21,7 +21,7 @@ abstract contract ERC165 is IERC165 { /** * @dev See {IERC165-supportsInterface}. */ - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { return interfaceId == type(IERC165).interfaceId; } } From d9474327a492f9f310f31bc53f38dbea56ed9a57 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 7 Jun 2023 02:32:14 +0200 Subject: [PATCH 160/197] Merge pull request from GHSA-5h3x-9wvq-w4m2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Francisco Co-authored-by: Ernesto García --- .changeset/swift-bags-divide.md | 5 ++ contracts/governance/Governor.sol | 90 ++++++++++++++++++++++++++++++- test/governance/Governor.t.sol | 55 +++++++++++++++++++ test/governance/Governor.test.js | 83 ++++++++++++++++++++++++++++ 4 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 .changeset/swift-bags-divide.md create mode 100644 test/governance/Governor.t.sol diff --git a/.changeset/swift-bags-divide.md b/.changeset/swift-bags-divide.md new file mode 100644 index 000000000..9af63e98e --- /dev/null +++ b/.changeset/swift-bags-divide.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`Governor`: Add a mechanism to restrict the address of the proposer using a suffix in the description. diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index 74d60c742..2e2289c6a 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -263,7 +263,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive } /** - * @dev See {IGovernor-propose}. + * @dev See {IGovernor-propose}. This function has opt-in frontrunning protection, described in {_isValidDescriptionForProposer}. */ function propose( address[] memory targets, @@ -272,8 +272,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive string memory description ) public virtual override returns (uint256) { address proposer = _msgSender(); - uint256 currentTimepoint = clock(); + require(_isValidDescriptionForProposer(proposer, description), "Governor: proposer restricted"); + uint256 currentTimepoint = clock(); require( getVotes(proposer, currentTimepoint - 1) >= proposalThreshold(), "Governor: proposer votes below proposal threshold" @@ -628,4 +629,89 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive ) public virtual returns (bytes4) { return this.onERC1155BatchReceived.selector; } + + /** + * @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 + * (case insensitive), then the submission of this proposal will only be authorized to said address. + * + * This is used for frontrunning protection. By adding this pattern at the end of their proposal, one can ensure + * that no other address can submit the same proposal. An attacker would have to either remove or change that part, + * which would result in a different proposal id. + * + * If the description does not match this pattern, it is unrestricted and anyone can submit it. This includes: + * - If the `0x???` part is not a valid hex string. + * - If the `0x???` part is a valid hex string, but does not contain exactly 40 hex digits. + * - If it ends with the expected suffix followed by newlines or other whitespace. + * - If it ends with some other similar suffix, e.g. `#other=abc`. + * - If it does not end with any such suffix. + */ + function _isValidDescriptionForProposer( + address proposer, + string memory description + ) internal view virtual returns (bool) { + uint256 len = bytes(description).length; + + // Length is too short to contain a valid proposer suffix + if (len < 52) { + return true; + } + + // Extract what would be the `#proposer=0x` marker beginning the suffix + bytes12 marker; + assembly { + // - Start of the string contents in memory = description + 32 + // - First character of the marker = len - 52 + // - Length of "#proposer=0x0000000000000000000000000000000000000000" = 52 + // - We read the memory word starting at the first character of the marker: + // - (description + 32) + (len - 52) = description + (len - 20) + // - Note: Solidity will ignore anything past the first 12 bytes + marker := mload(add(description, sub(len, 20))) + } + + // If the marker is not found, there is no proposer suffix to check + if (marker != bytes12("#proposer=0x")) { + return true; + } + + // Parse the 40 characters following the marker as uint160 + uint160 recovered = 0; + for (uint256 i = len - 40; i < len; ++i) { + (bool isHex, uint8 value) = _tryHexToUint(bytes(description)[i]); + // If any of the characters is not a hex digit, ignore the suffix entirely + if (!isHex) { + return true; + } + recovered = (recovered << 4) | value; + } + + return recovered == uint160(proposer); + } + + /** + * @dev Try to parse a character from a string as a hex value. Returns `(true, value)` if the char is in + * `[0-9a-fA-F]` and `(false, 0)` otherwise. Value is guaranteed to be in the range `0 <= value < 16` + */ + function _tryHexToUint(bytes1 char) private pure returns (bool, uint8) { + uint8 c = uint8(char); + unchecked { + // Case 0-9 + if (47 < c && c < 58) { + return (true, c - 48); + } + // Case A-F + else if (64 < c && c < 71) { + return (true, c - 55); + } + // Case a-f + else if (96 < c && c < 103) { + return (true, c - 87); + } + // Else: not a hex char + else { + return (false, 0); + } + } + } } diff --git a/test/governance/Governor.t.sol b/test/governance/Governor.t.sol new file mode 100644 index 000000000..43c4c5ddd --- /dev/null +++ b/test/governance/Governor.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../../contracts/utils/Strings.sol"; +import "../../contracts/governance/Governor.sol"; + +contract GovernorInternalTest is Test, Governor { + constructor() Governor("") {} + + function testValidDescriptionForProposer(string memory description, address proposer, bool includeProposer) public { + if (includeProposer) { + description = string.concat(description, "#proposer=", Strings.toHexString(proposer)); + } + assertTrue(_isValidDescriptionForProposer(proposer, description)); + } + + function testInvalidDescriptionForProposer( + string memory description, + address commitProposer, + address actualProposer + ) public { + vm.assume(commitProposer != actualProposer); + description = string.concat(description, "#proposer=", Strings.toHexString(commitProposer)); + assertFalse(_isValidDescriptionForProposer(actualProposer, description)); + } + + // We don't need to truly implement implement the missing functions because we are just testing + // internal helpers. + + function clock() public pure override returns (uint48) {} + + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public pure override returns (string memory) {} + + // solhint-disable-next-line func-name-mixedcase + function COUNTING_MODE() public pure virtual override returns (string memory) {} + + function votingDelay() public pure virtual override returns (uint256) {} + + function votingPeriod() public pure virtual override returns (uint256) {} + + function quorum(uint256) public pure virtual override returns (uint256) {} + + function hasVoted(uint256, address) public pure virtual override returns (bool) {} + + function _quorumReached(uint256) internal pure virtual override returns (bool) {} + + function _voteSucceeded(uint256) internal pure virtual override returns (bool) {} + + function _getVotes(address, uint256, bytes memory) internal pure virtual override returns (uint256) {} + + function _countVote(uint256, address, uint8, uint256, bytes memory) internal virtual override {} +} diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js index d34482212..96feaf3a7 100644 --- a/test/governance/Governor.test.js +++ b/test/governance/Governor.test.js @@ -551,6 +551,89 @@ contract('Governor', function (accounts) { }); }); + describe('frontrun protection using description suffix', function () { + describe('without protection', function () { + describe('without suffix', function () { + it('proposer can propose', async function () { + expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated'); + }); + + it('someone else can propose', async function () { + expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated'); + }); + }); + + describe('with different suffix', function () { + beforeEach(async function () { + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + value, + }, + ], + `#wrong-suffix=${proposer}`, + ); + }); + + it('proposer can propose', async function () { + expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated'); + }); + + it('someone else can propose', async function () { + expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated'); + }); + }); + + describe('with proposer suffix but bad address part', function () { + beforeEach(async function () { + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + value, + }, + ], + `#proposer=0x3C44CdDdB6a900fa2b585dd299e03d12FA429XYZ`, // XYZ are not a valid hex char + ); + }); + + it('propose can propose', async function () { + expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated'); + }); + + it('someone else can propose', async function () { + expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated'); + }); + }); + }); + + describe('with protection via proposer suffix', function () { + beforeEach(async function () { + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + value, + }, + ], + `#proposer=${proposer}`, + ); + }); + + it('proposer can propose', async function () { + expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated'); + }); + + it('someone else cannot propose', async function () { + await expectRevert(this.helper.propose({ from: voter1 }), 'Governor: proposer restricted'); + }); + }); + }); + describe('onlyGovernance updates', function () { it('setVotingDelay is protected', async function () { await expectRevert(this.mock.setVotingDelay('0'), 'Governor: onlyGovernance'); From 1d5bcd04e77041fa7b971bc7386cc161a5ae3bf8 Mon Sep 17 00:00:00 2001 From: Pascal Marco Caversaccio Date: Thu, 8 Jun 2023 04:10:43 +0300 Subject: [PATCH 161/197] `ECDSA`: Use unchecked arithmetic for the `tryRecover` function (#4301) Signed-off-by: Pascal Marco Caversaccio --- .changeset/four-adults-knock.md | 5 +++++ contracts/utils/cryptography/ECDSA.sol | 9 ++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 .changeset/four-adults-knock.md diff --git a/.changeset/four-adults-knock.md b/.changeset/four-adults-knock.md new file mode 100644 index 000000000..f6f566d7a --- /dev/null +++ b/.changeset/four-adults-knock.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ECDSA`: Use unchecked arithmetic for the `tryRecover` function that receives the `r` and `vs` short-signature fields separately. diff --git a/contracts/utils/cryptography/ECDSA.sol b/contracts/utils/cryptography/ECDSA.sol index 2ddbd9ba0..efff6063c 100644 --- a/contracts/utils/cryptography/ECDSA.sol +++ b/contracts/utils/cryptography/ECDSA.sol @@ -98,9 +98,12 @@ library ECDSA { * _Available since v4.3._ */ function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError) { - bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); - uint8 v = uint8((uint256(vs) >> 255) + 27); - return tryRecover(hash, v, r, s); + unchecked { + bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + // We do not check for an overflow here since the shift operation results in 0 or 1. + uint8 v = uint8((uint256(vs) >> 255) + 27); + return tryRecover(hash, v, r, s); + } } /** From cc0426317052b1850b686f2fbbcf481520399735 Mon Sep 17 00:00:00 2001 From: Robb Walters Date: Fri, 9 Jun 2023 09:00:16 -0700 Subject: [PATCH 162/197] Highlight Reentrancy Risk in IERC1155 SafeTransferFrom Function (#4283) Co-authored-by: Francisco --- contracts/token/ERC1155/IERC1155.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/contracts/token/ERC1155/IERC1155.sol b/contracts/token/ERC1155/IERC1155.sol index d7e25a5b1..3d2585d7e 100644 --- a/contracts/token/ERC1155/IERC1155.sol +++ b/contracts/token/ERC1155/IERC1155.sol @@ -86,6 +86,11 @@ interface IERC1155 is IERC165 { /** * @dev Transfers `amount` tokens of token type `id` from `from` to `to`. * + * WARNING: This function can potentially allow a reentrancy attack when transferring tokens + * to an untrusted contract, when invoking {onERC1155Received} on the receiver. + * Ensure to follow the checks-effects-interactions pattern and consider employing + * reentrancy guards when interacting with untrusted contracts. + * * Emits a {TransferSingle} event. * * Requirements: @@ -101,6 +106,12 @@ interface IERC1155 is IERC165 { /** * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}. * + * + * WARNING: This function can potentially allow a reentrancy attack when transferring tokens + * to an untrusted contract, when invoking {onERC1155Received} on the receiver. + * Ensure to follow the checks-effects-interactions pattern and consider employing + * reentrancy guards when interacting with untrusted contracts. + * * Emits a {TransferBatch} event. * * Requirements: From e73f90fa9d42489b5e7df327738dad9cdad31b0b Mon Sep 17 00:00:00 2001 From: Pascal Marco Caversaccio Date: Sun, 11 Jun 2023 21:38:31 +0200 Subject: [PATCH 163/197] Fix `IERC1155.safeBatchTransferFrom` documentation (#4340) --- contracts/token/ERC1155/IERC1155.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/token/ERC1155/IERC1155.sol b/contracts/token/ERC1155/IERC1155.sol index 3d2585d7e..16b2b71a3 100644 --- a/contracts/token/ERC1155/IERC1155.sol +++ b/contracts/token/ERC1155/IERC1155.sol @@ -108,7 +108,7 @@ interface IERC1155 is IERC165 { * * * WARNING: This function can potentially allow a reentrancy attack when transferring tokens - * to an untrusted contract, when invoking {onERC1155Received} on the receiver. + * to an untrusted contract, when invoking {onERC1155BatchReceived} on the receiver. * Ensure to follow the checks-effects-interactions pattern and consider employing * reentrancy guards when interacting with untrusted contracts. * From 08fd777f6d66369f7a6cbd2d64effda937ff9ce9 Mon Sep 17 00:00:00 2001 From: Pascal Marco Caversaccio Date: Mon, 12 Jun 2023 11:00:35 +0200 Subject: [PATCH 164/197] Rename "`ecrecover` opcode" to "`ecrecover` precompile" in `ECDSA` (#4339) --- contracts/utils/cryptography/ECDSA.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/utils/cryptography/ECDSA.sol b/contracts/utils/cryptography/ECDSA.sol index efff6063c..b907abfc0 100644 --- a/contracts/utils/cryptography/ECDSA.sol +++ b/contracts/utils/cryptography/ECDSA.sol @@ -35,7 +35,7 @@ library ECDSA { * @dev Returns the address that signed a hashed message (`hash`) with * `signature` or error string. This address can then be used for verification purposes. * - * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: * this function rejects them by requiring the `s` value to be in the lower * half order, and the `v` value to be either 27 or 28. * @@ -74,7 +74,7 @@ library ECDSA { * @dev Returns the address that signed a hashed message (`hash`) with * `signature`. This address can then be used for verification purposes. * - * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: * this function rejects them by requiring the `s` value to be in the lower * half order, and the `v` value to be either 27 or 28. * From b425a722409fdf4f3e0ec8f42d686f3c0522af19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Mon, 12 Jun 2023 17:41:52 -0600 Subject: [PATCH 165/197] Replace revert strings with custom errors (#4261) Co-authored-by: Hadrien Croubois Co-authored-by: Francisco --- .changeset/lovely-geckos-hide.md | 5 + GUIDELINES.md | 14 + contracts/access/AccessControl.sol | 21 +- .../access/AccessControlDefaultAdminRules.sol | 36 ++- contracts/access/IAccessControl.sol | 16 +- .../IAccessControlDefaultAdminRules.sol | 22 ++ contracts/access/Ownable.sol | 18 +- contracts/access/Ownable2Step.sol | 4 +- contracts/finance/VestingWallet.sol | 9 +- contracts/governance/Governor.sol | 96 ++++-- contracts/governance/IGovernor.sol | 57 ++++ contracts/governance/TimelockController.sol | 76 ++++- .../GovernorCompatibilityBravo.sol | 19 +- .../IGovernorCompatibilityBravo.sol | 5 + .../extensions/GovernorCountingSimple.sol | 6 +- .../extensions/GovernorSettings.sol | 4 +- .../extensions/GovernorTimelockCompound.sol | 20 +- .../extensions/GovernorTimelockControl.sol | 9 +- .../GovernorVotesQuorumFraction.sol | 13 +- .../extensions/IGovernorTimelock.sol | 10 + contracts/governance/utils/IVotes.sol | 5 + contracts/governance/utils/Votes.sol | 30 +- contracts/interfaces/draft-IERC6093.sol | 162 ++++++++++ contracts/metatx/MinimalForwarder.sol | 42 ++- contracts/mocks/AddressFnPointersMock.sol | 50 ++++ .../mocks/token/ERC721ConsecutiveMock.sol | 1 - contracts/proxy/Clones.sol | 13 +- contracts/proxy/ERC1967/ERC1967Upgrade.sol | 49 +++- contracts/proxy/beacon/UpgradeableBeacon.sol | 9 +- .../TransparentUpgradeableProxy.sol | 16 +- contracts/proxy/utils/Initializable.sol | 29 +- contracts/proxy/utils/UUPSUpgradeable.sol | 20 +- contracts/security/Pausable.sol | 20 +- contracts/security/ReentrancyGuard.sol | 9 +- contracts/token/ERC1155/ERC1155.sol | 77 +++-- .../ERC1155/extensions/ERC1155Burnable.sol | 14 +- .../ERC1155/extensions/ERC1155Pausable.sol | 3 +- .../ERC1155/extensions/ERC1155Supply.sol | 5 +- contracts/token/ERC20/ERC20.sol | 51 +++- .../token/ERC20/extensions/ERC20Capped.sol | 26 +- .../token/ERC20/extensions/ERC20FlashMint.sol | 31 +- .../token/ERC20/extensions/ERC20Pausable.sol | 3 +- .../token/ERC20/extensions/ERC20Permit.sol | 18 +- .../token/ERC20/extensions/ERC20Votes.sol | 11 +- .../token/ERC20/extensions/ERC20Wrapper.sol | 13 +- contracts/token/ERC20/extensions/ERC4626.sol | 40 ++- contracts/token/ERC20/utils/SafeERC20.sol | 32 +- contracts/token/ERC721/ERC721.sol | 78 +++-- .../ERC721/extensions/ERC721Burnable.sol | 5 +- .../ERC721/extensions/ERC721Consecutive.sol | 44 ++- .../ERC721/extensions/ERC721Enumerable.sol | 22 +- .../ERC721/extensions/ERC721Pausable.sol | 2 +- .../ERC721/extensions/ERC721URIStorage.sol | 4 +- .../token/ERC721/extensions/ERC721Wrapper.sol | 18 +- contracts/token/common/ERC2981.sol | 40 ++- contracts/utils/Address.sol | 117 ++++++-- contracts/utils/Create2.sol | 27 +- contracts/utils/Nonces.sol | 16 + contracts/utils/StorageSlot.sol | 2 +- contracts/utils/Strings.sol | 14 +- contracts/utils/cryptography/ECDSA.sol | 54 ++-- contracts/utils/cryptography/MerkleProof.sol | 13 +- .../utils/cryptography/SignatureChecker.sol | 2 +- contracts/utils/math/Math.sol | 9 +- contracts/utils/math/SafeCast.sol | 276 ++++++++++++++---- contracts/utils/structs/Checkpoints.sol | 13 +- contracts/utils/structs/DoubleEndedQueue.sol | 24 +- contracts/utils/structs/EnumerableMap.sol | 9 +- scripts/generate/templates/Checkpoints.js | 12 +- scripts/generate/templates/EnumerableMap.js | 9 +- scripts/generate/templates/SafeCast.js | 39 ++- scripts/generate/templates/StorageSlot.js | 2 +- test/access/AccessControl.behavior.js | 126 ++++---- test/access/AccessControl.test.js | 2 +- .../AccessControlDefaultAdminRules.test.js | 7 +- test/access/AccessControlEnumerable.test.js | 4 +- test/access/Ownable.test.js | 19 +- test/access/Ownable2Step.test.js | 21 +- test/finance/VestingWallet.test.js | 8 +- test/governance/Governor.test.js | 160 +++++++--- test/governance/TimelockController.test.js | 157 ++++++---- .../GovernorCompatibilityBravo.test.js | 61 ++-- .../GovernorPreventLateQuorum.test.js | 9 +- .../GovernorTimelockCompound.test.js | 59 ++-- .../GovernorTimelockControl.test.js | 89 ++++-- .../GovernorVotesQuorumFraction.test.js | 30 +- test/governance/utils/Votes.behavior.js | 39 ++- test/governance/utils/Votes.test.js | 9 +- test/helpers/customError.js | 43 ++- test/helpers/enums.js | 1 + test/helpers/governance.js | 39 +++ test/metatx/MinimalForwarder.test.js | 132 ++++----- test/proxy/Clones.test.js | 6 +- test/proxy/beacon/BeaconProxy.test.js | 9 +- test/proxy/beacon/UpgradeableBeacon.test.js | 20 +- test/proxy/transparent/ProxyAdmin.test.js | 19 +- .../TransparentUpgradeableProxy.behaviour.js | 18 +- test/proxy/utils/Initializable.test.js | 25 +- test/proxy/utils/UUPSUpgradeable.test.js | 15 +- test/security/Pausable.test.js | 15 +- test/security/ReentrancyGuard.test.js | 9 +- test/token/ERC1155/ERC1155.behavior.js | 109 +++---- test/token/ERC1155/ERC1155.test.js | 58 ++-- .../extensions/ERC1155Burnable.test.js | 14 +- .../extensions/ERC1155Pausable.test.js | 43 +-- test/token/ERC20/ERC20.behavior.js | 70 +++-- test/token/ERC20/ERC20.test.js | 47 +-- .../extensions/ERC20Burnable.behavior.js | 19 +- .../ERC20/extensions/ERC20Capped.behavior.js | 7 +- .../ERC20/extensions/ERC20Capped.test.js | 5 +- .../ERC20/extensions/ERC20FlashMint.test.js | 20 +- .../ERC20/extensions/ERC20Pausable.test.js | 17 +- ...RC20Permit.test.js => ERC20Permit.test.js} | 35 ++- .../token/ERC20/extensions/ERC20Votes.test.js | 42 ++- .../ERC20/extensions/ERC20Wrapper.test.js | 22 +- test/token/ERC20/extensions/ERC4626.test.js | 12 +- test/token/ERC20/utils/SafeERC20.test.js | 117 +++++--- test/token/ERC721/ERC721.behavior.js | 129 ++++---- test/token/ERC721/ERC721.test.js | 4 +- test/token/ERC721/ERC721Enumerable.test.js | 6 +- .../ERC721/extensions/ERC721Burnable.test.js | 11 +- .../extensions/ERC721Consecutive.test.js | 37 +-- .../ERC721/extensions/ERC721Pausable.test.js | 22 +- .../extensions/ERC721URIStorage.test.js | 18 +- .../ERC721/extensions/ERC721Wrapper.test.js | 30 +- test/token/common/ERC2981.behavior.js | 32 +- test/utils/Address.test.js | 115 +++++--- test/utils/Create2.test.js | 14 +- test/utils/Multicall.test.js | 7 +- test/utils/Nonces.test.js | 58 +++- test/utils/ShortStrings.test.js | 4 +- test/utils/Strings.test.js | 11 +- test/utils/cryptography/ECDSA.test.js | 46 +-- test/utils/cryptography/MerkleProof.test.js | 14 +- test/utils/math/SafeCast.test.js | 43 +-- test/utils/structs/Checkpoints.test.js | 10 +- test/utils/structs/DoubleEndedQueue.test.js | 10 +- test/utils/structs/EnumerableMap.behavior.js | 8 +- 138 files changed, 3220 insertions(+), 1287 deletions(-) create mode 100644 .changeset/lovely-geckos-hide.md create mode 100644 contracts/interfaces/draft-IERC6093.sol create mode 100644 contracts/mocks/AddressFnPointersMock.sol rename test/token/ERC20/extensions/{draft-ERC20Permit.test.js => ERC20Permit.test.js} (73%) diff --git a/.changeset/lovely-geckos-hide.md b/.changeset/lovely-geckos-hide.md new file mode 100644 index 000000000..1fbcb2077 --- /dev/null +++ b/.changeset/lovely-geckos-hide.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Replace revert strings and require statements with custom errors. diff --git a/GUIDELINES.md b/GUIDELINES.md index 0f4c9829e..1dd606ddd 100644 --- a/GUIDELINES.md +++ b/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` if the error is a violation of an ERC specification. + 2. Use the name of the underlying component where it belongs (eg. `Governor`, `ECDSA`, or `Timelock`). + + * The location of custom errors should be decided in the following order: + 1. Take the errors from their underlying ERCs if they're already defined. + 2. Declare the errors in the underlying interface/library if the error makes sense in its context. + 3. Declare the error in the implementation if the underlying interface/library is not suitable to do so (eg. interface/library already specified in an ERC). + 4. Declare the error in an extension if the error only happens in such extension or child contracts. + + * Custom error names should not be declared twice along the library to avoid duplicated identifier declarations when inheriting from multiple contracts. diff --git a/contracts/access/AccessControl.sol b/contracts/access/AccessControl.sol index df16dbdab..12dc770b3 100644 --- a/contracts/access/AccessControl.sol +++ b/contracts/access/AccessControl.sol @@ -107,16 +107,7 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { */ function _checkRole(bytes32 role, address account) internal view virtual { if (!hasRole(role, account)) { - revert( - string( - abi.encodePacked( - "AccessControl: account ", - Strings.toHexString(account), - " is missing role ", - Strings.toHexString(uint256(role), 32) - ) - ) - ); + revert AccessControlUnauthorizedAccount(account, role); } } @@ -173,14 +164,16 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { * * Requirements: * - * - the caller must be `account`. + * - the caller must be `callerConfirmation`. * * May emit a {RoleRevoked} event. */ - function renounceRole(bytes32 role, address account) public virtual { - require(account == _msgSender(), "AccessControl: can only renounce roles for self"); + function renounceRole(bytes32 role, address callerConfirmation) public virtual { + if (callerConfirmation != _msgSender()) { + revert AccessControlBadConfirmation(); + } - _revokeRole(role, account); + _revokeRole(role, callerConfirmation); } /** diff --git a/contracts/access/AccessControlDefaultAdminRules.sol b/contracts/access/AccessControlDefaultAdminRules.sol index 47df078c1..e27eaf3db 100644 --- a/contracts/access/AccessControlDefaultAdminRules.sol +++ b/contracts/access/AccessControlDefaultAdminRules.sol @@ -53,7 +53,9 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu * @dev Sets the initial values for {defaultAdminDelay} and {defaultAdmin} address. */ constructor(uint48 initialDelay, address initialDefaultAdmin) { - require(initialDefaultAdmin != address(0), "AccessControl: 0 default admin"); + if (initialDefaultAdmin == address(0)) { + revert AccessControlInvalidDefaultAdmin(address(0)); + } _currentDelay = initialDelay; _grantRole(DEFAULT_ADMIN_ROLE, initialDefaultAdmin); } @@ -80,7 +82,9 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu * @dev See {AccessControl-grantRole}. Reverts for `DEFAULT_ADMIN_ROLE`. */ function grantRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { - require(role != DEFAULT_ADMIN_ROLE, "AccessControl: can't directly grant default admin role"); + if (role == DEFAULT_ADMIN_ROLE) { + revert AccessControlEnforcedDefaultAdminRules(); + } super.grantRole(role, account); } @@ -88,7 +92,9 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu * @dev See {AccessControl-revokeRole}. Reverts for `DEFAULT_ADMIN_ROLE`. */ function revokeRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { - require(role != DEFAULT_ADMIN_ROLE, "AccessControl: can't directly revoke default admin role"); + if (role == DEFAULT_ADMIN_ROLE) { + revert AccessControlEnforcedDefaultAdminRules(); + } super.revokeRole(role, account); } @@ -108,10 +114,9 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu function renounceRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { if (role == DEFAULT_ADMIN_ROLE && account == defaultAdmin()) { (address newDefaultAdmin, uint48 schedule) = pendingDefaultAdmin(); - require( - newDefaultAdmin == address(0) && _isScheduleSet(schedule) && _hasSchedulePassed(schedule), - "AccessControl: only can renounce in two delayed steps" - ); + if (newDefaultAdmin != address(0) || !_isScheduleSet(schedule) || !_hasSchedulePassed(schedule)) { + revert AccessControlEnforcedDefaultAdminDelay(schedule); + } delete _pendingDefaultAdminSchedule; } super.renounceRole(role, account); @@ -128,7 +133,9 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu */ function _grantRole(bytes32 role, address account) internal virtual override { if (role == DEFAULT_ADMIN_ROLE) { - require(defaultAdmin() == address(0), "AccessControl: default admin already granted"); + if (defaultAdmin() != address(0)) { + revert AccessControlEnforcedDefaultAdminRules(); + } _currentDefaultAdmin = account; } super._grantRole(role, account); @@ -148,7 +155,9 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu * @dev See {AccessControl-_setRoleAdmin}. Reverts for `DEFAULT_ADMIN_ROLE`. */ function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual override { - require(role != DEFAULT_ADMIN_ROLE, "AccessControl: can't violate default admin rules"); + if (role == DEFAULT_ADMIN_ROLE) { + revert AccessControlEnforcedDefaultAdminRules(); + } super._setRoleAdmin(role, adminRole); } @@ -236,7 +245,10 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu */ function acceptDefaultAdminTransfer() public virtual { (address newDefaultAdmin, ) = pendingDefaultAdmin(); - require(_msgSender() == newDefaultAdmin, "AccessControl: pending admin must accept"); + if (_msgSender() != newDefaultAdmin) { + // Enforce newDefaultAdmin explicit acceptance. + revert AccessControlInvalidDefaultAdmin(_msgSender()); + } _acceptDefaultAdminTransfer(); } @@ -247,7 +259,9 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu */ function _acceptDefaultAdminTransfer() internal virtual { (address newAdmin, uint48 schedule) = pendingDefaultAdmin(); - require(_isScheduleSet(schedule) && _hasSchedulePassed(schedule), "AccessControl: transfer delay not passed"); + if (!_isScheduleSet(schedule) || !_hasSchedulePassed(schedule)) { + revert AccessControlEnforcedDefaultAdminDelay(schedule); + } _revokeRole(DEFAULT_ADMIN_ROLE, defaultAdmin()); _grantRole(DEFAULT_ADMIN_ROLE, newAdmin); delete _pendingDefaultAdmin; diff --git a/contracts/access/IAccessControl.sol b/contracts/access/IAccessControl.sol index 34708b78d..9abc2b735 100644 --- a/contracts/access/IAccessControl.sol +++ b/contracts/access/IAccessControl.sol @@ -7,6 +7,18 @@ pragma solidity ^0.8.19; * @dev External interface of AccessControl declared to support ERC165 detection. */ interface IAccessControl { + /** + * @dev The `account` is missing a role. + */ + error AccessControlUnauthorizedAccount(address account, bytes32 neededRole); + + /** + * @dev The caller of a function is not the expected one. + * + * NOTE: Don't confuse with {AccessControlUnauthorizedAccount}. + */ + error AccessControlBadConfirmation(); + /** * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` * @@ -82,7 +94,7 @@ interface IAccessControl { * * Requirements: * - * - the caller must be `account`. + * - the caller must be `callerConfirmation`. */ - function renounceRole(bytes32 role, address account) external; + function renounceRole(bytes32 role, address callerConfirmation) external; } diff --git a/contracts/access/IAccessControlDefaultAdminRules.sol b/contracts/access/IAccessControlDefaultAdminRules.sol index 94cbe871d..fbecfe120 100644 --- a/contracts/access/IAccessControlDefaultAdminRules.sol +++ b/contracts/access/IAccessControlDefaultAdminRules.sol @@ -11,6 +11,28 @@ import "./IAccessControl.sol"; * _Available since v4.9._ */ interface IAccessControlDefaultAdminRules is IAccessControl { + /** + * @dev The new default admin is not a valid default admin. + */ + error AccessControlInvalidDefaultAdmin(address defaultAdmin); + + /** + * @dev At least one of the following rules was violated: + * + * - The `DEFAULT_ADMIN_ROLE` must only be managed by itself. + * - The `DEFAULT_ADMIN_ROLE` must only be held by one account at the time. + * - Any `DEFAULT_ADMIN_ROLE` transfer must be in two delayed steps. + */ + error AccessControlEnforcedDefaultAdminRules(); + + /** + * @dev The delay for transferring the default admin delay is enforced and + * the operation must wait until `schedule`. + * + * NOTE: `schedule` can be 0 indicating there's no transfer scheduled. + */ + error AccessControlEnforcedDefaultAdminDelay(uint48 schedule); + /** * @dev Emitted when a {defaultAdmin} transfer is started, setting `newAdmin` as the next * address to become the {defaultAdmin} by calling {acceptDefaultAdminTransfer} only after `acceptSchedule` diff --git a/contracts/access/Ownable.sol b/contracts/access/Ownable.sol index 6c901b7a1..f4394378b 100644 --- a/contracts/access/Ownable.sol +++ b/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); } diff --git a/contracts/access/Ownable2Step.sol b/contracts/access/Ownable2Step.sol index 59ffa3e0e..61005b7e3 100644 --- a/contracts/access/Ownable2Step.sol +++ b/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); } } diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol index 5b7e1b150..ebdf0a330 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -23,6 +23,11 @@ contract VestingWallet is Context { event EtherReleased(uint256 amount); event ERC20Released(address indexed token, uint256 amount); + /** + * @dev The `beneficiary` is not a valid account. + */ + error VestingWalletInvalidBeneficiary(address beneficiary); + uint256 private _released; mapping(address => uint256) private _erc20Released; address private immutable _beneficiary; @@ -33,7 +38,9 @@ contract VestingWallet is Context { * @dev Set the beneficiary, start timestamp and vesting duration of the vesting wallet. */ constructor(address beneficiaryAddress, uint64 startTimestamp, uint64 durationSeconds) payable { - require(beneficiaryAddress != address(0), "VestingWallet: beneficiary is zero address"); + if (beneficiaryAddress == address(0)) { + revert VestingWalletInvalidBeneficiary(address(0)); + } _beneficiary = beneficiaryAddress; _start = startTimestamp; _duration = durationSeconds; diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index 2e2289c6a..b42fe1034 100644 --- a/contracts/governance/Governor.sol +++ b/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 diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 992b5ca10..5b51f088a 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -23,6 +23,63 @@ abstract contract IGovernor is IERC165, IERC6372 { Executed } + /** + * @dev Empty proposal or a mismatch between the parameters length for a proposal call. + */ + error GovernorInvalidProposalLength(uint256 targets, uint256 calldatas, uint256 values); + + /** + * @dev The vote was already cast. + */ + error GovernorAlreadyCastVote(address voter); + + /** + * @dev Token deposits are disabled in this contract. + */ + error GovernorDisabledDeposit(); + + /** + * @dev The `account` is not a proposer. + */ + error GovernorOnlyProposer(address account); + + /** + * @dev The `account` is not the governance executor. + */ + error GovernorOnlyExecutor(address account); + + /** + * @dev The `proposalId` doesn't exist. + */ + error GovernorNonexistentProposal(uint256 proposalId); + + /** + * @dev The current state of a proposal is not the required for performing an operation. + * The `expectedStates` is a bitmap with the bits enabled for each ProposalState enum position + * counting from right to left. + * + * NOTE: If `expectedState` is `bytes32(0)`, the proposal is expected to not be in any state (i.e. not exist). + * This is the case when a proposal that is expected to be unset is already initiated (the proposal is duplicated). + * + * See {Governor-_encodeStateBitmap}. + */ + error GovernorUnexpectedProposalState(uint256 proposalId, ProposalState current, bytes32 expectedStates); + + /** + * @dev The voting period set is not a valid period. + */ + error GovernorInvalidVotingPeriod(uint256 votingPeriod); + + /** + * @dev The `proposer` does not have the required votes to operate on a proposal. + */ + error GovernorInsufficientProposerVotes(address proposer, uint256 votes, uint256 threshold); + + /** + * @dev The vote type used is not valid for the corresponding counting module. + */ + error GovernorInvalidVoteType(); + /** * @dev Emitted when a proposal is created. */ diff --git a/contracts/governance/TimelockController.sol b/contracts/governance/TimelockController.sol index 9930d6a49..4772a252f 100644 --- a/contracts/governance/TimelockController.sol +++ b/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; } diff --git a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol index 425ecad09..670be9591 100644 --- a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol @@ -69,7 +69,9 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp bytes[] memory calldatas, string memory description ) public virtual override returns (uint256) { - require(signatures.length == calldatas.length, "GovernorBravo: invalid signatures length"); + if (signatures.length != calldatas.length) { + revert GovernorInvalidSignaturesLength(signatures.length, calldatas.length); + } // Stores the full proposal and fallback to the public (possibly overridden) propose. The fallback is done // after the full proposal is stored, so the store operation included in the fallback will be skipped. Here we // call `propose` and not `super.propose` to make sure if a child contract override `propose`, whatever code @@ -133,10 +135,11 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); address proposer = proposalProposer(proposalId); - require( - _msgSender() == proposer || getVotes(proposer, clock() - 1) < proposalThreshold(), - "GovernorBravo: proposer above threshold" - ); + uint256 proposerVotes = getVotes(proposer, clock() - 1); + uint256 votesThreshold = proposalThreshold(); + if (_msgSender() != proposer && proposerVotes >= votesThreshold) { + revert GovernorInsufficientProposerVotes(proposer, proposerVotes, votesThreshold); + } return _cancel(targets, values, calldatas, descriptionHash); } @@ -312,7 +315,9 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp ProposalDetails storage details = _proposalDetails[proposalId]; Receipt storage receipt = details.receipts[account]; - require(!receipt.hasVoted, "GovernorCompatibilityBravo: vote already cast"); + if (receipt.hasVoted) { + revert GovernorAlreadyCastVote(account); + } receipt.hasVoted = true; receipt.support = support; receipt.votes = SafeCast.toUint96(weight); @@ -324,7 +329,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp } else if (support == uint8(VoteType.Abstain)) { details.abstainVotes += weight; } else { - revert("GovernorCompatibilityBravo: invalid vote type"); + revert GovernorInvalidVoteType(); } } } diff --git a/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol b/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol index d69bbf619..197936ab9 100644 --- a/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol @@ -11,6 +11,11 @@ import "../IGovernor.sol"; * _Available since v4.3._ */ abstract contract IGovernorCompatibilityBravo is IGovernor { + /** + * @dev Mismatch between the parameters length for a proposal call. + */ + error GovernorInvalidSignaturesLength(uint256 signatures, uint256 calldatas); + /** * @dev Proposal structure from Compound Governor Bravo. Not actually used by the compatibility layer, as * {{proposal}} returns a very different structure. diff --git a/contracts/governance/extensions/GovernorCountingSimple.sol b/contracts/governance/extensions/GovernorCountingSimple.sol index d5c99e593..315f4ad45 100644 --- a/contracts/governance/extensions/GovernorCountingSimple.sol +++ b/contracts/governance/extensions/GovernorCountingSimple.sol @@ -84,7 +84,9 @@ abstract contract GovernorCountingSimple is Governor { ) internal virtual override { ProposalVote storage proposalVote = _proposalVotes[proposalId]; - require(!proposalVote.hasVoted[account], "GovernorVotingSimple: vote already cast"); + if (proposalVote.hasVoted[account]) { + revert GovernorAlreadyCastVote(account); + } proposalVote.hasVoted[account] = true; if (support == uint8(VoteType.Against)) { @@ -94,7 +96,7 @@ abstract contract GovernorCountingSimple is Governor { } else if (support == uint8(VoteType.Abstain)) { proposalVote.abstainVotes += weight; } else { - revert("GovernorVotingSimple: invalid value for enum VoteType"); + revert GovernorInvalidVoteType(); } } } diff --git a/contracts/governance/extensions/GovernorSettings.sol b/contracts/governance/extensions/GovernorSettings.sol index 570c88c54..64e081cc5 100644 --- a/contracts/governance/extensions/GovernorSettings.sol +++ b/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; } diff --git a/contracts/governance/extensions/GovernorTimelockCompound.sol b/contracts/governance/extensions/GovernorTimelockCompound.sol index 1efd3efff..21439b4db 100644 --- a/contracts/governance/extensions/GovernorTimelockCompound.sol +++ b/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); diff --git a/contracts/governance/extensions/GovernorTimelockControl.sol b/contracts/governance/extensions/GovernorTimelockControl.sol index 3fbce763a..888406ba7 100644 --- a/contracts/governance/extensions/GovernorTimelockControl.sol +++ b/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); diff --git a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol index 6c10240ce..0094fecd6 100644 --- a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol +++ b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol @@ -21,6 +21,11 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes { event QuorumNumeratorUpdated(uint256 oldQuorumNumerator, uint256 newQuorumNumerator); + /** + * @dev The quorum set is not a valid fraction. + */ + error GovernorInvalidQuorumFraction(uint256 quorumNumerator, uint256 quorumDenominator); + /** * @dev Initialize quorum as a fraction of the token's total supply. * @@ -94,10 +99,10 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes { * - New numerator must be smaller or equal to the denominator. */ function _updateQuorumNumerator(uint256 newQuorumNumerator) internal virtual { - require( - newQuorumNumerator <= quorumDenominator(), - "GovernorVotesQuorumFraction: quorumNumerator over quorumDenominator" - ); + uint256 denominator = quorumDenominator(); + if (newQuorumNumerator > denominator) { + revert GovernorInvalidQuorumFraction(newQuorumNumerator, denominator); + } uint256 oldQuorumNumerator = quorumNumerator(); _quorumNumeratorHistory.push(SafeCast.toUint32(clock()), SafeCast.toUint224(newQuorumNumerator)); diff --git a/contracts/governance/extensions/IGovernorTimelock.sol b/contracts/governance/extensions/IGovernorTimelock.sol index 570092bc5..c51429481 100644 --- a/contracts/governance/extensions/IGovernorTimelock.sol +++ b/contracts/governance/extensions/IGovernorTimelock.sol @@ -11,6 +11,16 @@ import "../IGovernor.sol"; * _Available since v4.3._ */ abstract contract IGovernorTimelock is IGovernor { + /** + * @dev The proposal hasn't been queued yet. + */ + error GovernorNotQueuedProposal(uint256 proposalId); + + /** + * @dev The proposal has already been queued. + */ + error GovernorAlreadyQueuedProposal(uint256 proposalId); + event ProposalQueued(uint256 proposalId, uint256 eta); function timelock() public view virtual returns (address); diff --git a/contracts/governance/utils/IVotes.sol b/contracts/governance/utils/IVotes.sol index a1e4fe63a..a8a20856f 100644 --- a/contracts/governance/utils/IVotes.sol +++ b/contracts/governance/utils/IVotes.sol @@ -8,6 +8,11 @@ pragma solidity ^0.8.19; * _Available since v4.5._ */ interface IVotes { + /** + * @dev The signature used has expired. + */ + error VotesExpiredSignature(uint256 expiry); + /** * @dev Emitted when an account changes their delegate. */ diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index 5fc15da29..09eb4e22c 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -42,6 +42,16 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { /// @custom:oz-retyped-from Checkpoints.History Checkpoints.Trace224 private _totalCheckpoints; + /** + * @dev The clock was incorrectly modified. + */ + error ERC6372InconsistentClock(); + + /** + * @dev Lookup to future votes is not available. + */ + error ERC5805FutureLookup(uint256 timepoint, uint48 clock); + /** * @dev Clock used for flagging checkpoints. Can be overridden to implement timestamp based * checkpoints (and voting), in which case {CLOCK_MODE} should be overridden as well to match. @@ -56,7 +66,9 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { // solhint-disable-next-line func-name-mixedcase function CLOCK_MODE() public view virtual returns (string memory) { // Check that the clock was not modified - require(clock() == block.number, "Votes: broken clock mode"); + if (clock() != block.number) { + revert ERC6372InconsistentClock(); + } return "mode=blocknumber&from=default"; } @@ -76,7 +88,10 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. */ function getPastVotes(address account, uint256 timepoint) public view virtual returns (uint256) { - require(timepoint < clock(), "Votes: future lookup"); + uint48 currentTimepoint = clock(); + if (timepoint >= currentTimepoint) { + revert ERC5805FutureLookup(timepoint, currentTimepoint); + } return _delegateCheckpoints[account].upperLookupRecent(SafeCast.toUint32(timepoint)); } @@ -93,7 +108,10 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. */ function getPastTotalSupply(uint256 timepoint) public view virtual returns (uint256) { - require(timepoint < clock(), "Votes: future lookup"); + uint48 currentTimepoint = clock(); + if (timepoint >= currentTimepoint) { + revert ERC5805FutureLookup(timepoint, currentTimepoint); + } return _totalCheckpoints.upperLookupRecent(SafeCast.toUint32(timepoint)); } @@ -130,14 +148,16 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { bytes32 r, bytes32 s ) public virtual { - require(block.timestamp <= expiry, "Votes: signature expired"); + if (block.timestamp > expiry) { + revert VotesExpiredSignature(expiry); + } address signer = ECDSA.recover( _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), v, r, s ); - require(nonce == _useNonce(signer), "Votes: invalid nonce"); + _useCheckedNonce(signer, nonce); _delegate(signer, delegatee); } diff --git a/contracts/interfaces/draft-IERC6093.sol b/contracts/interfaces/draft-IERC6093.sol new file mode 100644 index 000000000..cebda56a3 --- /dev/null +++ b/contracts/interfaces/draft-IERC6093.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/** + * @dev Standard ERC20 Errors + * Interface of the ERC6093 custom errors for ERC20 tokens + * as defined in https://eips.ethereum.org/EIPS/eip-6093 + */ +interface IERC20Errors { + /** + * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param balance Current balance for the interacting account. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC20InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC20InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers. + * @param spender Address that may be allowed to operate on tokens without being their owner. + * @param allowance Amount of tokens a `spender` is allowed to operate with. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC20InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `spender` to be approved. Used in approvals. + * @param spender Address that may be allowed to operate on tokens without being their owner. + */ + error ERC20InvalidSpender(address spender); +} + +/** + * @dev Standard ERC721 Errors + * Interface of the ERC6093 custom errors for ERC721 tokens + * as defined in https://eips.ethereum.org/EIPS/eip-6093 + */ +interface IERC721Errors { + /** + * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in EIP-20. + * Used in balance queries. + * @param owner Address of the current owner of a token. + */ + error ERC721InvalidOwner(address owner); + + /** + * @dev Indicates a `tokenId` whose `owner` is the zero address. + * @param tokenId Identifier number of a token. + */ + error ERC721NonexistentToken(uint256 tokenId); + + /** + * @dev Indicates an error related to the ownership over a particular token. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param tokenId Identifier number of a token. + * @param owner Address of the current owner of a token. + */ + error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC721InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC721InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `operator`’s approval. Used in transfers. + * @param operator Address that may be allowed to operate on tokens without being their owner. + * @param tokenId Identifier number of a token. + */ + error ERC721InsufficientApproval(address operator, uint256 tokenId); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC721InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `operator` to be approved. Used in approvals. + * @param operator Address that may be allowed to operate on tokens without being their owner. + */ + error ERC721InvalidOperator(address operator); +} + +/** + * @dev Standard ERC1155 Errors + * Interface of the ERC6093 custom errors for ERC1155 tokens + * as defined in https://eips.ethereum.org/EIPS/eip-6093 + */ +interface IERC1155Errors { + /** + * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param balance Current balance for the interacting account. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC1155InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC1155InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `operator`’s approval. Used in transfers. + * @param operator Address that may be allowed to operate on tokens without being their owner. + * @param owner Address of the current owner of a token. + */ + error 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); +} diff --git a/contracts/metatx/MinimalForwarder.sol b/contracts/metatx/MinimalForwarder.sol index 8ea7a76e8..b5267aa10 100644 --- a/contracts/metatx/MinimalForwarder.sol +++ b/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); + } } diff --git a/contracts/mocks/AddressFnPointersMock.sol b/contracts/mocks/AddressFnPointersMock.sol new file mode 100644 index 000000000..c696b3ec1 --- /dev/null +++ b/contracts/mocks/AddressFnPointersMock.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/Address.sol"; + +/** + * @dev A mock to expose `Address`'s functions with function pointers. + */ +contract AddressFnPointerMock { + error CustomRevert(); + + function functionCall(address target, bytes memory data) external returns (bytes memory) { + return Address.functionCall(target, data, _customRevert); + } + + function functionCallWithValue(address target, bytes memory data, uint256 value) external returns (bytes memory) { + return Address.functionCallWithValue(target, data, value, _customRevert); + } + + function functionStaticCall(address target, bytes memory data) external view returns (bytes memory) { + return Address.functionStaticCall(target, data, _customRevert); + } + + function functionDelegateCall(address target, bytes memory data) external returns (bytes memory) { + return Address.functionDelegateCall(target, data, _customRevert); + } + + function verifyCallResultFromTarget( + address target, + bool success, + bytes memory returndata + ) external view returns (bytes memory) { + return Address.verifyCallResultFromTarget(target, success, returndata, _customRevert); + } + + function verifyCallResult(bool success, bytes memory returndata) external view returns (bytes memory) { + return Address.verifyCallResult(success, returndata, _customRevert); + } + + function verifyCallResultVoid(bool success, bytes memory returndata) external view returns (bytes memory) { + return Address.verifyCallResult(success, returndata, _customRevertVoid); + } + + function _customRevert() internal pure { + revert CustomRevert(); + } + + function _customRevertVoid() internal pure {} +} diff --git a/contracts/mocks/token/ERC721ConsecutiveMock.sol b/contracts/mocks/token/ERC721ConsecutiveMock.sol index 851e45ceb..4aae388e0 100644 --- a/contracts/mocks/token/ERC721ConsecutiveMock.sol +++ b/contracts/mocks/token/ERC721ConsecutiveMock.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.19; import "../../token/ERC721/extensions/ERC721Consecutive.sol"; -import "../../token/ERC721/extensions/ERC721Enumerable.sol"; import "../../token/ERC721/extensions/ERC721Pausable.sol"; import "../../token/ERC721/extensions/ERC721Votes.sol"; diff --git a/contracts/proxy/Clones.sol b/contracts/proxy/Clones.sol index 7cdab55f6..d859d5645 100644 --- a/contracts/proxy/Clones.sol +++ b/contracts/proxy/Clones.sol @@ -17,6 +17,11 @@ pragma solidity ^0.8.19; * _Available since v3.4._ */ library Clones { + /** + * @dev A clone instance deployment failed. + */ + error ERC1167FailedCreateClone(); + /** * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`. * @@ -32,7 +37,9 @@ library Clones { mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3)) instance := create(0, 0x09, 0x37) } - require(instance != address(0), "ERC1167: create failed"); + if (instance == address(0)) { + revert ERC1167FailedCreateClone(); + } } /** @@ -52,7 +59,9 @@ library Clones { mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3)) instance := create2(0, 0x09, 0x37, salt) } - require(instance != address(0), "ERC1167: create2 failed"); + if (instance == address(0)) { + revert ERC1167FailedCreateClone(); + } } /** diff --git a/contracts/proxy/ERC1967/ERC1967Upgrade.sol b/contracts/proxy/ERC1967/ERC1967Upgrade.sol index e42a06eb1..ca6b92580 100644 --- a/contracts/proxy/ERC1967/ERC1967Upgrade.sol +++ b/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; } diff --git a/contracts/proxy/beacon/UpgradeableBeacon.sol b/contracts/proxy/beacon/UpgradeableBeacon.sol index 37d27f67a..c5e64ea59 100644 --- a/contracts/proxy/beacon/UpgradeableBeacon.sol +++ b/contracts/proxy/beacon/UpgradeableBeacon.sol @@ -15,6 +15,11 @@ import "../../access/Ownable.sol"; contract UpgradeableBeacon is IBeacon, Ownable { address private _implementation; + /** + * @dev The `implementation` of the beacon is invalid. + */ + error BeaconInvalidImplementation(address implementation); + /** * @dev Emitted when the implementation returned by the beacon is changed. */ @@ -57,7 +62,9 @@ contract UpgradeableBeacon is IBeacon, Ownable { * - `newImplementation` must be a contract. */ function _setImplementation(address newImplementation) private { - require(newImplementation.code.length > 0, "UpgradeableBeacon: implementation is not a contract"); + if (newImplementation.code.length == 0) { + revert BeaconInvalidImplementation(newImplementation); + } _implementation = newImplementation; } } diff --git a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol index 01f55e99b..4536e28c8 100644 --- a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +++ b/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(); + } } } diff --git a/contracts/proxy/utils/Initializable.sol b/contracts/proxy/utils/Initializable.sol index a42887918..3ae5e4a65 100644 --- a/contracts/proxy/utils/Initializable.sol +++ b/contracts/proxy/utils/Initializable.sol @@ -67,6 +67,16 @@ abstract contract Initializable { */ bool private _initializing; + /** + * @dev The contract is already initialized. + */ + error AlreadyInitialized(); + + /** + * @dev The contract is not initializing. + */ + error NotInitializing(); + /** * @dev Triggered when the contract has been initialized or reinitialized. */ @@ -83,10 +93,9 @@ abstract contract Initializable { */ modifier initializer() { bool isTopLevelCall = !_initializing; - require( - (isTopLevelCall && _initialized < 1) || (address(this).code.length == 0 && _initialized == 1), - "Initializable: contract is already initialized" - ); + if (!(isTopLevelCall && _initialized < 1) && !(address(this).code.length == 0 && _initialized == 1)) { + revert AlreadyInitialized(); + } _initialized = 1; if (isTopLevelCall) { _initializing = true; @@ -117,7 +126,9 @@ abstract contract Initializable { * Emits an {Initialized} event. */ modifier reinitializer(uint8 version) { - require(!_initializing && _initialized < version, "Initializable: contract is already initialized"); + if (_initializing || _initialized >= version) { + revert AlreadyInitialized(); + } _initialized = version; _initializing = true; _; @@ -130,7 +141,9 @@ abstract contract Initializable { * {initializer} and {reinitializer} modifiers, directly or indirectly. */ modifier onlyInitializing() { - require(_initializing, "Initializable: contract is not initializing"); + if (!_initializing) { + revert NotInitializing(); + } _; } @@ -143,7 +156,9 @@ abstract contract Initializable { * Emits an {Initialized} event the first time it is successfully executed. */ function _disableInitializers() internal virtual { - require(!_initializing, "Initializable: contract is initializing"); + if (_initializing) { + revert AlreadyInitialized(); + } if (_initialized != type(uint8).max) { _initialized = type(uint8).max; emit Initialized(type(uint8).max); diff --git a/contracts/proxy/utils/UUPSUpgradeable.sol b/contracts/proxy/utils/UUPSUpgradeable.sol index 1fd73247c..41a72ef9c 100644 --- a/contracts/proxy/utils/UUPSUpgradeable.sol +++ b/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(); + } _; } diff --git a/contracts/security/Pausable.sol b/contracts/security/Pausable.sol index cdf3ee2cd..dc0afa663 100644 --- a/contracts/security/Pausable.sol +++ b/contracts/security/Pausable.sol @@ -15,6 +15,8 @@ import "../utils/Context.sol"; * simply including this module, only once the modifiers are put in place. */ abstract contract Pausable is Context { + bool private _paused; + /** * @dev Emitted when the pause is triggered by `account`. */ @@ -25,7 +27,15 @@ abstract contract Pausable is Context { */ event Unpaused(address account); - bool private _paused; + /** + * @dev The operation failed because the contract is paused. + */ + error EnforcedPause(); + + /** + * @dev The operation failed because the contract is not paused. + */ + error ExpectedPause(); /** * @dev Initializes the contract in unpaused state. @@ -69,14 +79,18 @@ abstract contract Pausable is Context { * @dev Throws if the contract is paused. */ function _requireNotPaused() internal view virtual { - require(!paused(), "Pausable: paused"); + if (paused()) { + revert EnforcedPause(); + } } /** * @dev Throws if the contract is not paused. */ function _requirePaused() internal view virtual { - require(paused(), "Pausable: not paused"); + if (!paused()) { + revert ExpectedPause(); + } } /** diff --git a/contracts/security/ReentrancyGuard.sol b/contracts/security/ReentrancyGuard.sol index 88a86ae7e..40ae5b050 100644 --- a/contracts/security/ReentrancyGuard.sol +++ b/contracts/security/ReentrancyGuard.sol @@ -36,6 +36,11 @@ abstract contract ReentrancyGuard { uint256 private _status; + /** + * @dev Unauthorized reentrant call. + */ + error ReentrancyGuardReentrantCall(); + constructor() { _status = _NOT_ENTERED; } @@ -55,7 +60,9 @@ abstract contract ReentrancyGuard { function _nonReentrantBefore() private { // On the first call to nonReentrant, _status will be _NOT_ENTERED - require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); + if (_status == _ENTERED) { + revert ReentrancyGuardReentrantCall(); + } // Any calls to nonReentrant after this point will fail _status = _ENTERED; diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol index c2f217d7e..05bcc3fa1 100644 --- a/contracts/token/ERC1155/ERC1155.sol +++ b/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); } } } diff --git a/contracts/token/ERC1155/extensions/ERC1155Burnable.sol b/contracts/token/ERC1155/extensions/ERC1155Burnable.sol index c079f07e1..ff099cb58 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Burnable.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Burnable.sol @@ -13,19 +13,17 @@ import "../ERC1155.sol"; */ abstract contract ERC1155Burnable is ERC1155 { function burn(address account, uint256 id, uint256 value) public virtual { - require( - account == _msgSender() || isApprovedForAll(account, _msgSender()), - "ERC1155: caller is not token owner or approved" - ); + if (account != _msgSender() && !isApprovedForAll(account, _msgSender())) { + revert 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); } diff --git a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol index 95f006e6f..f8357062c 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol @@ -35,8 +35,7 @@ abstract contract ERC1155Pausable is ERC1155, Pausable { uint256[] memory ids, uint256[] memory amounts, bytes memory data - ) internal virtual override { - require(!paused(), "ERC1155Pausable: token transfer while paused"); + ) internal virtual override whenNotPaused { super._update(from, to, ids, amounts, data); } } diff --git a/contracts/token/ERC1155/extensions/ERC1155Supply.sol b/contracts/token/ERC1155/extensions/ERC1155Supply.sol index 4ad83ea02..f32fbb74b 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Supply.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Supply.sol @@ -66,11 +66,8 @@ abstract contract ERC1155Supply is ERC1155 { for (uint256 i = 0; i < ids.length; ++i) { uint256 id = ids[i]; uint256 amount = amounts[i]; - uint256 supply = _totalSupply[id]; - require(supply >= amount, "ERC1155: burn amount exceeds totalSupply"); + _totalSupply[id] -= amount; unchecked { - // Overflow not possible: amounts[i] <= totalSupply(i) - _totalSupply[id] = supply - amount; // Overflow not possible: sum(amounts[i]) <= sum(totalSupply(i)) <= totalSupplyAll totalBurnAmount += amount; } diff --git a/contracts/token/ERC20/ERC20.sol b/contracts/token/ERC20/ERC20.sol index 4db525a7a..dd46c15dd 100644 --- a/contracts/token/ERC20/ERC20.sol +++ b/contracts/token/ERC20/ERC20.sol @@ -6,6 +6,7 @@ pragma solidity ^0.8.19; import "./IERC20.sol"; import "./extensions/IERC20Metadata.sol"; import "../../utils/Context.sol"; +import "../../interfaces/draft-IERC6093.sol"; /** * @dev Implementation of the {IERC20} interface. @@ -34,7 +35,7 @@ import "../../utils/Context.sol"; * functions have been added to mitigate the well-known issues around setting * allowances. See {IERC20-approve}. */ -contract ERC20 is Context, IERC20, IERC20Metadata { +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); } diff --git a/contracts/token/ERC20/extensions/ERC20Capped.sol b/contracts/token/ERC20/extensions/ERC20Capped.sol index cda072651..41e9ce5cf 100644 --- a/contracts/token/ERC20/extensions/ERC20Capped.sol +++ b/contracts/token/ERC20/extensions/ERC20Capped.sol @@ -11,12 +11,24 @@ import "../ERC20.sol"; abstract contract ERC20Capped is ERC20 { uint256 private immutable _cap; + /** + * @dev Total supply cap has been exceeded. + */ + error ERC20ExceededCap(uint256 increasedSupply, uint256 cap); + + /** + * @dev The supplied cap is not a valid cap. + */ + error ERC20InvalidCap(uint256 cap); + /** * @dev Sets the value of the `cap`. This value is immutable, it can only be * set once during construction. */ constructor(uint256 cap_) { - require(cap_ > 0, "ERC20Capped: cap is 0"); + if (cap_ == 0) { + revert ERC20InvalidCap(0); + } _cap = cap_; } @@ -31,10 +43,14 @@ abstract contract ERC20Capped is ERC20 { * @dev See {ERC20-_update}. */ function _update(address from, address to, uint256 amount) internal virtual override { - if (from == address(0)) { - require(totalSupply() + amount <= cap(), "ERC20Capped: cap exceeded"); - } - super._update(from, to, amount); + + if (from == address(0)) { + uint256 maxSupply = cap(); + uint256 supply = totalSupply(); + if (supply > maxSupply) { + revert ERC20ExceededCap(supply, maxSupply); + } + } } } diff --git a/contracts/token/ERC20/extensions/ERC20FlashMint.sol b/contracts/token/ERC20/extensions/ERC20FlashMint.sol index 7a4076678..09c20bacc 100644 --- a/contracts/token/ERC20/extensions/ERC20FlashMint.sol +++ b/contracts/token/ERC20/extensions/ERC20FlashMint.sol @@ -19,6 +19,21 @@ import "../ERC20.sol"; abstract contract ERC20FlashMint is ERC20, IERC3156FlashLender { bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan"); + /** + * @dev The loan token is not valid. + */ + error ERC3156UnsupportedToken(address token); + + /** + * @dev The requested loan exceeds the max loan amount for `token`. + */ + error ERC3156ExceededMaxLoan(uint256 maxLoan); + + /** + * @dev The receiver of a flashloan is not a valid {onFlashLoan} implementer. + */ + error ERC3156InvalidReceiver(address receiver); + /** * @dev Returns the maximum amount of tokens available for loan. * @param token The address of the token that is requested. @@ -37,7 +52,9 @@ abstract contract ERC20FlashMint is ERC20, IERC3156FlashLender { * @return The fees applied to the corresponding flash loan. */ function flashFee(address token, uint256 amount) public view virtual returns (uint256) { - require(token == address(this), "ERC20FlashMint: wrong token"); + if (token != address(this)) { + revert ERC3156UnsupportedToken(token); + } return _flashFee(token, amount); } @@ -89,13 +106,15 @@ abstract contract ERC20FlashMint is ERC20, IERC3156FlashLender { uint256 amount, bytes calldata data ) public virtual returns (bool) { - require(amount <= maxFlashLoan(token), "ERC20FlashMint: amount exceeds maxFlashLoan"); + uint256 maxLoan = maxFlashLoan(token); + if (amount > maxLoan) { + revert ERC3156ExceededMaxLoan(maxLoan); + } uint256 fee = flashFee(token, amount); _mint(address(receiver), amount); - require( - receiver.onFlashLoan(msg.sender, token, amount, fee, data) == _RETURN_VALUE, - "ERC20FlashMint: invalid return value" - ); + if (receiver.onFlashLoan(msg.sender, token, amount, fee, data) != _RETURN_VALUE) { + revert ERC3156InvalidReceiver(address(receiver)); + } address flashFeeReceiver = _flashFeeReceiver(); _spendAllowance(address(receiver), address(this), amount + fee); if (fee == 0 || flashFeeReceiver == address(0)) { diff --git a/contracts/token/ERC20/extensions/ERC20Pausable.sol b/contracts/token/ERC20/extensions/ERC20Pausable.sol index b31cac7de..5ef50f9c6 100644 --- a/contracts/token/ERC20/extensions/ERC20Pausable.sol +++ b/contracts/token/ERC20/extensions/ERC20Pausable.sol @@ -27,8 +27,7 @@ abstract contract ERC20Pausable is ERC20, Pausable { * * - the contract must not be paused. */ - function _update(address from, address to, uint256 amount) internal virtual override { - require(!paused(), "ERC20Pausable: token transfer while paused"); + function _update(address from, address to, uint256 amount) internal virtual override whenNotPaused { super._update(from, to, amount); } } diff --git a/contracts/token/ERC20/extensions/ERC20Permit.sol b/contracts/token/ERC20/extensions/ERC20Permit.sol index 9379e4451..4378eb7c1 100644 --- a/contracts/token/ERC20/extensions/ERC20Permit.sol +++ b/contracts/token/ERC20/extensions/ERC20Permit.sol @@ -24,6 +24,16 @@ abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { bytes32 private constant _PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + /** + * @dev Permit deadline has expired. + */ + error ERC2612ExpiredSignature(uint256 deadline); + + /** + * @dev Mismatched signature. + */ + error ERC2612InvalidSigner(address signer, address owner); + /** * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * @@ -43,14 +53,18 @@ abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { bytes32 r, bytes32 s ) public virtual { - require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); + if (block.timestamp > deadline) { + revert ERC2612ExpiredSignature(deadline); + } bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); bytes32 hash = _hashTypedDataV4(structHash); address signer = ECDSA.recover(hash, v, r, s); - require(signer == owner, "ERC20Permit: invalid signature"); + if (signer != owner) { + revert ERC2612InvalidSigner(signer, owner); + } _approve(owner, spender, value); } diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index c078878ec..98f798efe 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -23,6 +23,11 @@ import "../../../utils/math/SafeCast.sol"; * _Available since v4.2._ */ abstract contract ERC20Votes is ERC20, Votes { + /** + * @dev Total supply cap has been exceeded, introducing a risk of votes overflowing. + */ + error ERC20ExceededSafeSupply(uint256 increasedSupply, uint256 cap); + /** * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). */ @@ -38,7 +43,11 @@ abstract contract ERC20Votes is ERC20, Votes { function _update(address from, address to, uint256 amount) internal virtual override { super._update(from, to, amount); if (from == address(0)) { - require(totalSupply() <= _maxSupply(), "ERC20Votes: total supply risks overflowing votes"); + uint256 supply = totalSupply(); + uint256 cap = _maxSupply(); + if (supply > cap) { + revert ERC20ExceededSafeSupply(supply, cap); + } } _transferVotingUnits(from, to, amount); } diff --git a/contracts/token/ERC20/extensions/ERC20Wrapper.sol b/contracts/token/ERC20/extensions/ERC20Wrapper.sol index bf2b225cf..389965e9c 100644 --- a/contracts/token/ERC20/extensions/ERC20Wrapper.sol +++ b/contracts/token/ERC20/extensions/ERC20Wrapper.sol @@ -18,8 +18,15 @@ import "../utils/SafeERC20.sol"; abstract contract ERC20Wrapper is ERC20 { IERC20 private immutable _underlying; + /** + * @dev The underlying token couldn't be wrapped. + */ + error ERC20InvalidUnderlying(address token); + constructor(IERC20 underlyingToken) { - require(underlyingToken != this, "ERC20Wrapper: cannot self wrap"); + if (underlyingToken == this) { + revert ERC20InvalidUnderlying(address(this)); + } _underlying = underlyingToken; } @@ -46,7 +53,9 @@ abstract contract ERC20Wrapper is ERC20 { */ function depositFor(address account, uint256 amount) public virtual returns (bool) { address sender = _msgSender(); - require(sender != address(this), "ERC20Wrapper: wrapper can't deposit"); + if (sender == address(this)) { + revert ERC20InvalidSender(address(this)); + } SafeERC20.safeTransferFrom(_underlying, sender, address(this), amount); _mint(account, amount); return true; diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index 665c95a02..9ea6789f7 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -53,6 +53,26 @@ abstract contract ERC4626 is ERC20, IERC4626 { IERC20 private immutable _asset; uint8 private immutable _underlyingDecimals; + /** + * @dev Attempted to deposit more assets than the max amount for `receiver`. + */ + error ERC4626ExceededMaxDeposit(address receiver, uint256 assets, uint256 max); + + /** + * @dev Attempted to mint more shares than the max amount for `receiver`. + */ + error ERC4626ExceededMaxMint(address receiver, uint256 shares, uint256 max); + + /** + * @dev Attempted to withdraw more assets than the max amount for `receiver`. + */ + error ERC4626ExceededMaxWithdraw(address owner, uint256 assets, uint256 max); + + /** + * @dev Attempted to redeem more shares than the max amount for `receiver`. + */ + error ERC4626ExceededMaxRedeem(address owner, uint256 shares, uint256 max); + /** * @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC20 or ERC777). */ @@ -151,7 +171,10 @@ abstract contract ERC4626 is ERC20, IERC4626 { /** @dev See {IERC4626-deposit}. */ function deposit(uint256 assets, address receiver) public virtual returns (uint256) { - require(assets <= maxDeposit(receiver), "ERC4626: deposit more than max"); + uint256 maxAssets = maxDeposit(receiver); + if (assets > maxAssets) { + revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets); + } uint256 shares = previewDeposit(assets); _deposit(_msgSender(), receiver, assets, shares); @@ -165,7 +188,10 @@ abstract contract ERC4626 is ERC20, IERC4626 { * In this case, the shares will be minted without requiring any assets to be deposited. */ function mint(uint256 shares, address receiver) public virtual returns (uint256) { - require(shares <= maxMint(receiver), "ERC4626: mint more than max"); + uint256 maxShares = maxMint(receiver); + if (shares > maxShares) { + revert ERC4626ExceededMaxMint(receiver, shares, maxShares); + } uint256 assets = previewMint(shares); _deposit(_msgSender(), receiver, assets, shares); @@ -175,7 +201,10 @@ abstract contract ERC4626 is ERC20, IERC4626 { /** @dev See {IERC4626-withdraw}. */ function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256) { - require(assets <= maxWithdraw(owner), "ERC4626: withdraw more than max"); + uint256 maxAssets = maxWithdraw(owner); + if (assets > maxAssets) { + revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets); + } uint256 shares = previewWithdraw(assets); _withdraw(_msgSender(), receiver, owner, assets, shares); @@ -185,7 +214,10 @@ abstract contract ERC4626 is ERC20, IERC4626 { /** @dev See {IERC4626-redeem}. */ function redeem(uint256 shares, address receiver, address owner) public virtual returns (uint256) { - require(shares <= maxRedeem(owner), "ERC4626: redeem more than max"); + uint256 maxShares = maxRedeem(owner); + if (shares > maxShares) { + revert ERC4626ExceededMaxRedeem(owner, shares, maxShares); + } uint256 assets = previewRedeem(shares); _withdraw(_msgSender(), receiver, owner, assets, shares); diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index b1532d1cb..599307e7f 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -19,6 +19,16 @@ import "../../../utils/Address.sol"; library SafeERC20 { using Address for address; + /** + * @dev An operation with an ERC20 token failed. + */ + error SafeERC20FailedOperation(address token); + + /** + * @dev Indicates a failed `decreaseAllowance` request. + */ + error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease); + /** * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value, * non-reverting calls are assumed to be successful. @@ -45,14 +55,16 @@ library SafeERC20 { } /** - * @dev Decrease the calling contract's allowance toward `spender` by `value`. If `token` returns no value, + * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no value, * non-reverting calls are assumed to be successful. */ - function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal { + function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal { unchecked { - uint256 oldAllowance = token.allowance(address(this), spender); - require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); - forceApprove(token, spender, oldAllowance - value); + uint256 currentAllowance = token.allowance(address(this), spender); + if (currentAllowance < requestedDecrease) { + revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease); + } + forceApprove(token, spender, currentAllowance - requestedDecrease); } } @@ -87,7 +99,9 @@ library SafeERC20 { uint256 nonceBefore = token.nonces(owner); token.permit(owner, spender, value, deadline, v, r, s); uint256 nonceAfter = token.nonces(owner); - require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed"); + if (nonceAfter != nonceBefore + 1) { + revert SafeERC20FailedOperation(address(token)); + } } /** @@ -101,8 +115,10 @@ library SafeERC20 { // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that // the target address contains contract code and also asserts for success in the low-level call. - bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed"); - require(returndata.length == 0 || abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); + bytes memory returndata = address(token).functionCall(data); + if (returndata.length != 0 && !abi.decode(returndata, (bool))) { + revert SafeERC20FailedOperation(address(token)); + } } /** diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 3eb3f74cf..08f660a29 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -9,13 +9,14 @@ import "./extensions/IERC721Metadata.sol"; import "../../utils/Context.sol"; import "../../utils/Strings.sol"; import "../../utils/introspection/ERC165.sol"; +import "../../interfaces/draft-IERC6093.sol"; /** * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including * the Metadata extension, but not including the Enumerable extension, which is available separately as * {ERC721Enumerable}. */ -contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { +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 { diff --git a/contracts/token/ERC721/extensions/ERC721Burnable.sol b/contracts/token/ERC721/extensions/ERC721Burnable.sol index 5489169e8..217f039ca 100644 --- a/contracts/token/ERC721/extensions/ERC721Burnable.sol +++ b/contracts/token/ERC721/extensions/ERC721Burnable.sol @@ -19,8 +19,9 @@ abstract contract ERC721Burnable is Context, ERC721 { * - The caller must own `tokenId` or be an approved operator. */ function burn(uint256 tokenId) public virtual { - //solhint-disable-next-line max-line-length - require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved"); + if (!_isApprovedOrOwner(_msgSender(), tokenId)) { + revert ERC721InsufficientApproval(_msgSender(), tokenId); + } _burn(tokenId); } } diff --git a/contracts/token/ERC721/extensions/ERC721Consecutive.sol b/contracts/token/ERC721/extensions/ERC721Consecutive.sol index b7295e476..f1308cdab 100644 --- a/contracts/token/ERC721/extensions/ERC721Consecutive.sol +++ b/contracts/token/ERC721/extensions/ERC721Consecutive.sol @@ -36,6 +36,28 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { Checkpoints.Trace160 private _sequentialOwnership; BitMaps.BitMap private _sequentialBurn; + /** + * @dev Batch mint is restricted to the constructor. + * Any batch mint not emitting the {IERC721-Transfer} event outside of the constructor + * is non-ERC721 compliant. + */ + error ERC721ForbiddenBatchMint(); + + /** + * @dev Exceeds the max amount of mints per batch. + */ + error ERC721ExceededMaxBatchMint(uint256 batchSize, uint256 maxBatch); + + /** + * @dev Individual minting is not allowed. + */ + error ERC721ForbiddenMint(); + + /** + * @dev Batch burn is not supported. + */ + error ERC721ForbiddenBatchBurn(); + /** * @dev Maximum size of a batch of consecutive tokens. This is designed to limit stress on off-chain indexing * services that have to record one entry per token, and have protections against "unreasonably large" batches of @@ -86,9 +108,17 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { // minting a batch of size 0 is a no-op if (batchSize > 0) { - require(address(this).code.length == 0, "ERC721Consecutive: batch minting restricted to constructor"); - require(to != address(0), "ERC721Consecutive: mint to the zero address"); - require(batchSize <= _maxBatchSize(), "ERC721Consecutive: batch too large"); + if (address(this).code.length > 0) { + revert ERC721ForbiddenBatchMint(); + } + if (to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + + uint256 maxBatchSize = _maxBatchSize(); + if (batchSize > maxBatchSize) { + revert ERC721ExceededMaxBatchMint(batchSize, maxBatchSize); + } // hook before _beforeTokenTransfer(address(0), to, next, batchSize); @@ -117,7 +147,9 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { * After construction, {_mintConsecutive} is no longer available and {_mint} becomes available. */ function _mint(address to, uint256 tokenId) internal virtual override { - require(address(this).code.length > 0, "ERC721Consecutive: can't mint during construction"); + if (address(this).code.length == 0) { + revert ERC721ForbiddenMint(); + } super._mint(to, tokenId); } @@ -137,7 +169,9 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { !_sequentialBurn.get(firstTokenId) ) // and the token was never marked as burnt { - require(batchSize == 1, "ERC721Consecutive: batch burn not supported"); + if (batchSize != 1) { + revert ERC721ForbiddenBatchBurn(); + } _sequentialBurn.set(firstTokenId); } super._afterTokenTransfer(from, to, firstTokenId, batchSize); diff --git a/contracts/token/ERC721/extensions/ERC721Enumerable.sol b/contracts/token/ERC721/extensions/ERC721Enumerable.sol index 8cea9e19a..18e2ba5d6 100644 --- a/contracts/token/ERC721/extensions/ERC721Enumerable.sol +++ b/contracts/token/ERC721/extensions/ERC721Enumerable.sol @@ -26,6 +26,18 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { // Mapping from token id to position in the allTokens array mapping(uint256 => uint256) private _allTokensIndex; + /** + * @dev An `owner`'s token query was out of bounds for `index`. + * + * NOTE: The owner being `address(0)` indicates a global out of bounds index. + */ + error ERC721OutOfBoundsIndex(address owner, uint256 index); + + /** + * @dev Batch mint is not allowed. + */ + error ERC721EnumerableForbiddenBatchMint(); + /** * @dev See {IERC165-supportsInterface}. */ @@ -37,7 +49,9 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { * @dev See {IERC721Enumerable-tokenOfOwnerByIndex}. */ function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual returns (uint256) { - require(index < balanceOf(owner), "ERC721Enumerable: owner index out of bounds"); + if (index >= balanceOf(owner)) { + revert ERC721OutOfBoundsIndex(owner, index); + } return _ownedTokens[owner][index]; } @@ -52,7 +66,9 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { * @dev See {IERC721Enumerable-tokenByIndex}. */ function tokenByIndex(uint256 index) public view virtual returns (uint256) { - require(index < totalSupply(), "ERC721Enumerable: global index out of bounds"); + if (index >= totalSupply()) { + revert ERC721OutOfBoundsIndex(address(0), index); + } return _allTokens[index]; } @@ -69,7 +85,7 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { if (batchSize > 1) { // Will only trigger during construction. Batch transferring (minting) is not available afterwards. - revert("ERC721Enumerable: consecutive transfers not supported"); + revert ERC721EnumerableForbiddenBatchMint(); } uint256 tokenId = firstTokenId; diff --git a/contracts/token/ERC721/extensions/ERC721Pausable.sol b/contracts/token/ERC721/extensions/ERC721Pausable.sol index 0cadaa7c7..a9472c5dc 100644 --- a/contracts/token/ERC721/extensions/ERC721Pausable.sol +++ b/contracts/token/ERC721/extensions/ERC721Pausable.sol @@ -35,6 +35,6 @@ abstract contract ERC721Pausable is ERC721, Pausable { ) internal virtual override { super._beforeTokenTransfer(from, to, firstTokenId, batchSize); - require(!paused(), "ERC721Pausable: token transfer while paused"); + _requireNotPaused(); } } diff --git a/contracts/token/ERC721/extensions/ERC721URIStorage.sol b/contracts/token/ERC721/extensions/ERC721URIStorage.sol index 6350a0952..ae058122d 100644 --- a/contracts/token/ERC721/extensions/ERC721URIStorage.sol +++ b/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); diff --git a/contracts/token/ERC721/extensions/ERC721Wrapper.sol b/contracts/token/ERC721/extensions/ERC721Wrapper.sol index 9226349f4..47a42c1f0 100644 --- a/contracts/token/ERC721/extensions/ERC721Wrapper.sol +++ b/contracts/token/ERC721/extensions/ERC721Wrapper.sol @@ -17,6 +17,11 @@ import "../ERC721.sol"; abstract contract ERC721Wrapper is ERC721, IERC721Receiver { IERC721 private immutable _underlying; + /** + * @dev The received ERC721 token couldn't be wrapped. + */ + error ERC721UnsupportedToken(address token); + constructor(IERC721 underlyingToken) { _underlying = underlyingToken; } @@ -46,7 +51,9 @@ abstract contract ERC721Wrapper is ERC721, IERC721Receiver { uint256 length = tokenIds.length; for (uint256 i = 0; i < length; ++i) { uint256 tokenId = tokenIds[i]; - require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721Wrapper: caller is not token owner or approved"); + if (!_isApprovedOrOwner(_msgSender(), tokenId)) { + revert ERC721InsufficientApproval(_msgSender(), tokenId); + } _burn(tokenId); // Checks were already performed at this point, and there's no way to retake ownership or approval from // the wrapped tokenId after this point, so it's safe to remove the reentrancy check for the next line. @@ -68,7 +75,9 @@ abstract contract ERC721Wrapper is ERC721, IERC721Receiver { * for recovering in that scenario. */ function onERC721Received(address, address from, uint256 tokenId, bytes memory) public virtual returns (bytes4) { - require(address(underlying()) == _msgSender(), "ERC721Wrapper: caller is not underlying"); + if (address(underlying()) != _msgSender()) { + revert ERC721UnsupportedToken(_msgSender()); + } _safeMint(from, tokenId); return IERC721Receiver.onERC721Received.selector; } @@ -78,7 +87,10 @@ abstract contract ERC721Wrapper is ERC721, IERC721Receiver { * function that can be exposed with access control if desired. */ function _recover(address account, uint256 tokenId) internal virtual returns (uint256) { - require(underlying().ownerOf(tokenId) == address(this), "ERC721Wrapper: wrapper is not token owner"); + address owner = underlying().ownerOf(tokenId); + if (owner != address(this)) { + revert ERC721IncorrectOwner(address(this), tokenId, owner); + } _safeMint(account, tokenId); return tokenId; } diff --git a/contracts/token/common/ERC2981.sol b/contracts/token/common/ERC2981.sol index 85e902733..21869ee25 100644 --- a/contracts/token/common/ERC2981.sol +++ b/contracts/token/common/ERC2981.sol @@ -30,6 +30,26 @@ abstract contract ERC2981 is IERC2981, ERC165 { RoyaltyInfo private _defaultRoyaltyInfo; mapping(uint256 => RoyaltyInfo) private _tokenRoyaltyInfo; + /** + * @dev The default royalty set is invalid (eg. (numerator / denominator) >= 1). + */ + error ERC2981InvalidDefaultRoyalty(uint256 numerator, uint256 denominator); + + /** + * @dev The default royalty receiver is invalid. + */ + error ERC2981InvalidDefaultRoyaltyReceiver(address receiver); + + /** + * @dev The royalty set for an specific `tokenId` is invalid (eg. (numerator / denominator) >= 1). + */ + error ERC2981InvalidTokenRoyalty(uint256 tokenId, uint256 numerator, uint256 denominator); + + /** + * @dev The royalty receiver for `tokenId` is invalid. + */ + error ERC2981InvalidTokenRoyaltyReceiver(uint256 tokenId, address receiver); + /** * @dev See {IERC165-supportsInterface}. */ @@ -70,8 +90,14 @@ abstract contract ERC2981 is IERC2981, ERC165 { * - `feeNumerator` cannot be greater than the fee denominator. */ function _setDefaultRoyalty(address receiver, uint96 feeNumerator) internal virtual { - require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice"); - require(receiver != address(0), "ERC2981: invalid receiver"); + uint256 denominator = _feeDenominator(); + if (feeNumerator > denominator) { + // Royalty fee will exceed the sale price + revert ERC2981InvalidDefaultRoyalty(feeNumerator, denominator); + } + if (receiver == address(0)) { + revert ERC2981InvalidDefaultRoyaltyReceiver(address(0)); + } _defaultRoyaltyInfo = RoyaltyInfo(receiver, feeNumerator); } @@ -92,8 +118,14 @@ abstract contract ERC2981 is IERC2981, ERC165 { * - `feeNumerator` cannot be greater than the fee denominator. */ function _setTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator) internal virtual { - require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice"); - require(receiver != address(0), "ERC2981: Invalid parameters"); + uint256 denominator = _feeDenominator(); + if (feeNumerator > denominator) { + // Royalty fee will exceed the sale price + revert ERC2981InvalidTokenRoyalty(tokenId, feeNumerator, denominator); + } + if (receiver == address(0)) { + revert ERC2981InvalidTokenRoyaltyReceiver(tokenId, address(0)); + } _tokenRoyaltyInfo[tokenId] = RoyaltyInfo(receiver, feeNumerator); } diff --git a/contracts/utils/Address.sol b/contracts/utils/Address.sol index 02f475620..859332b39 100644 --- a/contracts/utils/Address.sol +++ b/contracts/utils/Address.sol @@ -7,6 +7,21 @@ pragma solidity ^0.8.19; * @dev Collection of functions related to the address type */ library Address { + /** + * @dev The ETH balance of the account is not enough to perform the operation. + */ + error AddressInsufficientBalance(address account); + + /** + * @dev There's no code at `target` (it is not a contract). + */ + error AddressEmptyCode(address target); + + /** + * @dev A call to an address target failed. The target may have reverted. + */ + error FailedInnerCall(); + /** * @dev Replacement for Solidity's `transfer`: sends `amount` wei to * `recipient`, forwarding all available gas and reverting on errors. @@ -24,10 +39,14 @@ library Address { * https://solidity.readthedocs.io/en/v0.8.0/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. */ function sendValue(address payable recipient, uint256 amount) internal { - require(address(this).balance >= amount, "Address: insufficient balance"); + if (address(this).balance < amount) { + revert AddressInsufficientBalance(address(this)); + } (bool success, ) = recipient.call{value: amount}(""); - require(success, "Address: unable to send value, recipient may have reverted"); + if (!success) { + revert FailedInnerCall(); + } } /** @@ -49,21 +68,25 @@ library Address { * _Available since v3.1._ */ function functionCall(address target, bytes memory data) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, "Address: low-level call failed"); + return functionCallWithValue(target, data, 0, defaultRevert); } /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with - * `errorMessage` as a fallback revert reason when `target` reverts. + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with a + * `customRevert` function as a fallback when `target` reverts. * - * _Available since v3.1._ + * Requirements: + * + * - `customRevert` must be a reverting function. + * + * _Available since v5.0._ */ function functionCall( address target, bytes memory data, - string memory errorMessage + function() internal view customRevert ) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, errorMessage); + return functionCallWithValue(target, data, 0, customRevert); } /** @@ -78,24 +101,30 @@ library Address { * _Available since v3.1._ */ function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { - return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + return functionCallWithValue(target, data, value, defaultRevert); } /** * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but - * with `errorMessage` as a fallback revert reason when `target` reverts. + * with a `customRevert` function as a fallback revert reason when `target` reverts. * - * _Available since v3.1._ + * Requirements: + * + * - `customRevert` must be a reverting function. + * + * _Available since v5.0._ */ function functionCallWithValue( address target, bytes memory data, uint256 value, - string memory errorMessage + function() internal view customRevert ) internal returns (bytes memory) { - require(address(this).balance >= value, "Address: insufficient balance for call"); + if (address(this).balance < value) { + revert AddressInsufficientBalance(address(this)); + } (bool success, bytes memory returndata) = target.call{value: value}(data); - return verifyCallResultFromTarget(target, success, returndata, errorMessage); + return verifyCallResultFromTarget(target, success, returndata, customRevert); } /** @@ -105,7 +134,7 @@ library Address { * _Available since v3.3._ */ function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { - return functionStaticCall(target, data, "Address: low-level static call failed"); + return functionStaticCall(target, data, defaultRevert); } /** @@ -117,10 +146,10 @@ library Address { function functionStaticCall( address target, bytes memory data, - string memory errorMessage + function() internal view customRevert ) internal view returns (bytes memory) { (bool success, bytes memory returndata) = target.staticcall(data); - return verifyCallResultFromTarget(target, success, returndata, errorMessage); + return verifyCallResultFromTarget(target, success, returndata, customRevert); } /** @@ -130,7 +159,7 @@ library Address { * _Available since v3.4._ */ function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { - return functionDelegateCall(target, data, "Address: low-level delegate call failed"); + return functionDelegateCall(target, data, defaultRevert); } /** @@ -142,55 +171,78 @@ library Address { function functionDelegateCall( address target, bytes memory data, - string memory errorMessage + function() internal view customRevert ) internal returns (bytes memory) { (bool success, bytes memory returndata) = target.delegatecall(data); - return verifyCallResultFromTarget(target, success, returndata, errorMessage); + return verifyCallResultFromTarget(target, success, returndata, customRevert); } /** * @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling - * the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract. + * the revert reason or using the provided `customRevert`) in case of unsuccessful call or if target was not a contract. * - * _Available since v4.8._ + * _Available since v5.0._ */ function verifyCallResultFromTarget( address target, bool success, bytes memory returndata, - string memory errorMessage + function() internal view customRevert ) internal view returns (bytes memory) { if (success) { if (returndata.length == 0) { // only check if target is a contract if the call was successful and the return data is empty // otherwise we already know that it was a contract - require(target.code.length > 0, "Address: call to non-contract"); + if (target.code.length == 0) { + revert AddressEmptyCode(target); + } } return returndata; } else { - _revert(returndata, errorMessage); + _revert(returndata, customRevert); } } /** * @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the - * revert reason or using the provided one. + * revert reason or with a default revert error. * - * _Available since v4.3._ + * _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`. + * + * Requirements: + * + * - `customRevert` must be a reverting function. + * + * _Available since v5.0._ */ function verifyCallResult( bool success, bytes memory returndata, - string memory errorMessage - ) internal pure returns (bytes memory) { + function() internal view customRevert + ) internal view returns (bytes memory) { if (success) { return returndata; } else { - _revert(returndata, errorMessage); + _revert(returndata, customRevert); } } - function _revert(bytes memory returndata, string memory errorMessage) private pure { + /** + * @dev Default reverting function when no `customRevert` is provided in a function call. + */ + function defaultRevert() internal pure { + revert FailedInnerCall(); + } + + function _revert(bytes memory returndata, function() internal view customRevert) private view { // Look for revert reason and bubble it up if present if (returndata.length > 0) { // The easiest way to bubble the revert reason is using memory via assembly @@ -200,7 +252,8 @@ library Address { revert(add(32, returndata), returndata_size) } } else { - revert(errorMessage); + customRevert(); + revert FailedInnerCall(); } } } diff --git a/contracts/utils/Create2.sol b/contracts/utils/Create2.sol index d5776885f..24d27ea0b 100644 --- a/contracts/utils/Create2.sol +++ b/contracts/utils/Create2.sol @@ -13,6 +13,21 @@ pragma solidity ^0.8.19; * information. */ library Create2 { + /** + * @dev Not enough balance for performing a CREATE2 deploy. + */ + error Create2InsufficientBalance(uint256 balance, uint256 needed); + + /** + * @dev There's no code to deploy. + */ + error Create2EmptyBytecode(); + + /** + * @dev The deployment failed. + */ + error Create2FailedDeployment(); + /** * @dev Deploys a contract using `CREATE2`. The address where the contract * will be deployed can be known in advance via {computeAddress}. @@ -28,13 +43,19 @@ library Create2 { * - if `amount` is non-zero, `bytecode` must have a `payable` constructor. */ function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) internal returns (address addr) { - require(address(this).balance >= amount, "Create2: insufficient balance"); - require(bytecode.length != 0, "Create2: bytecode length is zero"); + if (address(this).balance < amount) { + revert Create2InsufficientBalance(address(this).balance, amount); + } + if (bytecode.length == 0) { + revert Create2EmptyBytecode(); + } /// @solidity memory-safe-assembly assembly { addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt) } - require(addr != address(0), "Create2: Failed on deploy"); + if (addr == address(0)) { + revert Create2FailedDeployment(); + } } /** diff --git a/contracts/utils/Nonces.sol b/contracts/utils/Nonces.sol index 04b884797..f8ea1dfd3 100644 --- a/contracts/utils/Nonces.sol +++ b/contracts/utils/Nonces.sol @@ -5,6 +5,11 @@ pragma solidity ^0.8.19; * @dev Provides tracking nonces for addresses. Nonces will only increment. */ abstract contract Nonces { + /** + * @dev The nonce used for an `account` is not the expected current nonce. + */ + error InvalidAccountNonce(address account, uint256 currentNonce); + mapping(address => uint256) private _nonces; /** @@ -27,4 +32,15 @@ abstract contract Nonces { return _nonces[owner]++; } } + + /** + * @dev Same as {_useNonce} but checking that `nonce` is the next valid for `owner`. + */ + function _useCheckedNonce(address owner, uint256 nonce) internal virtual returns (uint256) { + uint256 current = _useNonce(owner); + if (nonce != current) { + revert InvalidAccountNonce(owner, current); + } + return current; + } } diff --git a/contracts/utils/StorageSlot.sol b/contracts/utils/StorageSlot.sol index f12640e7b..b0e918967 100644 --- a/contracts/utils/StorageSlot.sol +++ b/contracts/utils/StorageSlot.sol @@ -22,7 +22,7 @@ pragma solidity ^0.8.19; * } * * function _setImplementation(address newImplementation) internal { - * require(newImplementation.code.length > 0, "ERC1967: new implementation is not a contract"); + * require(newImplementation.code.length > 0); * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; * } * } diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 050f6f9ca..65c8c8753 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -13,6 +13,11 @@ library Strings { bytes16 private constant _SYMBOLS = "0123456789abcdef"; uint8 private constant _ADDRESS_LENGTH = 20; + /** + * @dev The `value` string doesn't fit in the specified `length`. + */ + error StringsInsufficientHexLength(uint256 value, uint256 length); + /** * @dev Converts a `uint256` to its ASCII `string` decimal representation. */ @@ -58,14 +63,17 @@ library Strings { * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. */ function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + uint256 localValue = value; bytes memory buffer = new bytes(2 * length + 2); buffer[0] = "0"; buffer[1] = "x"; for (uint256 i = 2 * length + 1; i > 1; --i) { - buffer[i] = _SYMBOLS[value & 0xf]; - value >>= 4; + buffer[i] = _SYMBOLS[localValue & 0xf]; + localValue >>= 4; + } + if (localValue != 0) { + revert StringsInsufficientHexLength(value, length); } - require(value == 0, "Strings: hex length insufficient"); return string(buffer); } diff --git a/contracts/utils/cryptography/ECDSA.sol b/contracts/utils/cryptography/ECDSA.sol index b907abfc0..b8f1affee 100644 --- a/contracts/utils/cryptography/ECDSA.sol +++ b/contracts/utils/cryptography/ECDSA.sol @@ -19,15 +19,30 @@ library ECDSA { InvalidSignatureS } - function _throwError(RecoverError error) private pure { + /** + * @dev The signature derives the `address(0)`. + */ + error ECDSAInvalidSignature(); + + /** + * @dev The signature has an invalid length. + */ + error ECDSAInvalidSignatureLength(uint256 length); + + /** + * @dev The signature has an S value that is in the upper half order. + */ + error ECDSAInvalidSignatureS(bytes32 s); + + function _throwError(RecoverError error, bytes32 errorArg) private pure { if (error == RecoverError.NoError) { return; // no error: do nothing } else if (error == RecoverError.InvalidSignature) { - revert("ECDSA: invalid signature"); + revert ECDSAInvalidSignature(); } else if (error == RecoverError.InvalidSignatureLength) { - revert("ECDSA: invalid signature length"); + revert ECDSAInvalidSignatureLength(uint256(errorArg)); } else if (error == RecoverError.InvalidSignatureS) { - revert("ECDSA: invalid signature 's' value"); + revert ECDSAInvalidSignatureS(errorArg); } } @@ -51,7 +66,7 @@ library ECDSA { * * _Available since v4.3._ */ - function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) { + function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError, bytes32) { if (signature.length == 65) { bytes32 r; bytes32 s; @@ -66,7 +81,7 @@ library ECDSA { } return tryRecover(hash, v, r, s); } else { - return (address(0), RecoverError.InvalidSignatureLength); + return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length)); } } @@ -85,8 +100,8 @@ library ECDSA { * be too long), and then calling {toEthSignedMessageHash} on it. */ function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { - (address recovered, RecoverError error) = tryRecover(hash, signature); - _throwError(error); + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature); + _throwError(error, errorArg); return recovered; } @@ -97,7 +112,7 @@ library ECDSA { * * _Available since v4.3._ */ - function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError) { + function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError, bytes32) { unchecked { bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); // We do not check for an overflow here since the shift operation results in 0 or 1. @@ -112,8 +127,8 @@ library ECDSA { * _Available since v4.2._ */ function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { - (address recovered, RecoverError error) = tryRecover(hash, r, vs); - _throwError(error); + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs); + _throwError(error, errorArg); return recovered; } @@ -123,7 +138,12 @@ library ECDSA { * * _Available since v4.3._ */ - function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address, RecoverError) { + function tryRecover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address, RecoverError, bytes32) { // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most @@ -134,16 +154,16 @@ library ECDSA { // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept // these malleable signatures as well. if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { - return (address(0), RecoverError.InvalidSignatureS); + return (address(0), RecoverError.InvalidSignatureS, s); } // If the signature is valid (and not malleable), return the signer address address signer = ecrecover(hash, v, r, s); if (signer == address(0)) { - return (address(0), RecoverError.InvalidSignature); + return (address(0), RecoverError.InvalidSignature, bytes32(0)); } - return (signer, RecoverError.NoError); + return (signer, RecoverError.NoError, bytes32(0)); } /** @@ -151,8 +171,8 @@ library ECDSA { * `r` and `s` signature fields separately. */ function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { - (address recovered, RecoverError error) = tryRecover(hash, v, r, s); - _throwError(error); + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s); + _throwError(error, errorArg); return recovered; } diff --git a/contracts/utils/cryptography/MerkleProof.sol b/contracts/utils/cryptography/MerkleProof.sol index 0bcdda2cd..ed88ea1db 100644 --- a/contracts/utils/cryptography/MerkleProof.sol +++ b/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". diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index 941f7538f..25fdee5b3 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -22,7 +22,7 @@ library SignatureChecker { * change through time. It could return true at block N and false at block N+1 (or the opposite). */ function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { - (address recovered, ECDSA.RecoverError error) = ECDSA.tryRecover(hash, signature); + (address recovered, ECDSA.RecoverError error, ) = ECDSA.tryRecover(hash, signature); return (error == ECDSA.RecoverError.NoError && recovered == signer) || isValidERC1271SignatureNow(signer, hash, signature); diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index ff3dc8d9f..a2fc7ceb7 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -7,6 +7,11 @@ pragma solidity ^0.8.19; * @dev Standard math utilities missing in the Solidity language. */ library Math { + /** + * @dev Muldiv operation overflow. + */ + error MathOverflowedMulDiv(); + enum Rounding { Down, // Toward negative infinity Up, // Toward infinity @@ -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. diff --git a/contracts/utils/math/SafeCast.sol b/contracts/utils/math/SafeCast.sol index d9e21bb17..d3b86b088 100644 --- a/contracts/utils/math/SafeCast.sol +++ b/contracts/utils/math/SafeCast.sol @@ -17,6 +17,26 @@ pragma solidity ^0.8.19; * class of bugs, so it's recommended to use it always. */ library SafeCast { + /** + * @dev Value doesn't fit in an uint of `bits` size. + */ + error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value); + + /** + * @dev An int value doesn't fit in an uint of `bits` size. + */ + error SafeCastOverflowedIntToUint(int256 value); + + /** + * @dev Value doesn't fit in an int of `bits` size. + */ + error SafeCastOverflowedIntDowncast(uint8 bits, int256 value); + + /** + * @dev An uint value doesn't fit in an int of `bits` size. + */ + error SafeCastOverflowedUintToInt(uint256 value); + /** * @dev Returns the downcasted uint248 from uint256, reverting on * overflow (when the input is greater than largest uint248). @@ -30,7 +50,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint248(uint256 value) internal pure returns (uint248) { - require(value <= type(uint248).max, "SafeCast: value doesn't fit in 248 bits"); + if (value > type(uint248).max) { + revert SafeCastOverflowedUintDowncast(248, value); + } return uint248(value); } @@ -47,7 +69,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint240(uint256 value) internal pure returns (uint240) { - require(value <= type(uint240).max, "SafeCast: value doesn't fit in 240 bits"); + if (value > type(uint240).max) { + revert SafeCastOverflowedUintDowncast(240, value); + } return uint240(value); } @@ -64,7 +88,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint232(uint256 value) internal pure returns (uint232) { - require(value <= type(uint232).max, "SafeCast: value doesn't fit in 232 bits"); + if (value > type(uint232).max) { + revert SafeCastOverflowedUintDowncast(232, value); + } return uint232(value); } @@ -81,7 +107,9 @@ library SafeCast { * _Available since v4.2._ */ function toUint224(uint256 value) internal pure returns (uint224) { - require(value <= type(uint224).max, "SafeCast: value doesn't fit in 224 bits"); + if (value > type(uint224).max) { + revert SafeCastOverflowedUintDowncast(224, value); + } return uint224(value); } @@ -98,7 +126,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint216(uint256 value) internal pure returns (uint216) { - require(value <= type(uint216).max, "SafeCast: value doesn't fit in 216 bits"); + if (value > type(uint216).max) { + revert SafeCastOverflowedUintDowncast(216, value); + } return uint216(value); } @@ -115,7 +145,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint208(uint256 value) internal pure returns (uint208) { - require(value <= type(uint208).max, "SafeCast: value doesn't fit in 208 bits"); + if (value > type(uint208).max) { + revert SafeCastOverflowedUintDowncast(208, value); + } return uint208(value); } @@ -132,7 +164,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint200(uint256 value) internal pure returns (uint200) { - require(value <= type(uint200).max, "SafeCast: value doesn't fit in 200 bits"); + if (value > type(uint200).max) { + revert SafeCastOverflowedUintDowncast(200, value); + } return uint200(value); } @@ -149,7 +183,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint192(uint256 value) internal pure returns (uint192) { - require(value <= type(uint192).max, "SafeCast: value doesn't fit in 192 bits"); + if (value > type(uint192).max) { + revert SafeCastOverflowedUintDowncast(192, value); + } return uint192(value); } @@ -166,7 +202,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint184(uint256 value) internal pure returns (uint184) { - require(value <= type(uint184).max, "SafeCast: value doesn't fit in 184 bits"); + if (value > type(uint184).max) { + revert SafeCastOverflowedUintDowncast(184, value); + } return uint184(value); } @@ -183,7 +221,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint176(uint256 value) internal pure returns (uint176) { - require(value <= type(uint176).max, "SafeCast: value doesn't fit in 176 bits"); + if (value > type(uint176).max) { + revert SafeCastOverflowedUintDowncast(176, value); + } return uint176(value); } @@ -200,7 +240,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint168(uint256 value) internal pure returns (uint168) { - require(value <= type(uint168).max, "SafeCast: value doesn't fit in 168 bits"); + if (value > type(uint168).max) { + revert SafeCastOverflowedUintDowncast(168, value); + } return uint168(value); } @@ -217,7 +259,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint160(uint256 value) internal pure returns (uint160) { - require(value <= type(uint160).max, "SafeCast: value doesn't fit in 160 bits"); + if (value > type(uint160).max) { + revert SafeCastOverflowedUintDowncast(160, value); + } return uint160(value); } @@ -234,7 +278,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint152(uint256 value) internal pure returns (uint152) { - require(value <= type(uint152).max, "SafeCast: value doesn't fit in 152 bits"); + if (value > type(uint152).max) { + revert SafeCastOverflowedUintDowncast(152, value); + } return uint152(value); } @@ -251,7 +297,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint144(uint256 value) internal pure returns (uint144) { - require(value <= type(uint144).max, "SafeCast: value doesn't fit in 144 bits"); + if (value > type(uint144).max) { + revert SafeCastOverflowedUintDowncast(144, value); + } return uint144(value); } @@ -268,7 +316,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint136(uint256 value) internal pure returns (uint136) { - require(value <= type(uint136).max, "SafeCast: value doesn't fit in 136 bits"); + if (value > type(uint136).max) { + revert SafeCastOverflowedUintDowncast(136, value); + } return uint136(value); } @@ -285,7 +335,9 @@ library SafeCast { * _Available since v2.5._ */ function toUint128(uint256 value) internal pure returns (uint128) { - require(value <= type(uint128).max, "SafeCast: value doesn't fit in 128 bits"); + if (value > type(uint128).max) { + revert SafeCastOverflowedUintDowncast(128, value); + } return uint128(value); } @@ -302,7 +354,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint120(uint256 value) internal pure returns (uint120) { - require(value <= type(uint120).max, "SafeCast: value doesn't fit in 120 bits"); + if (value > type(uint120).max) { + revert SafeCastOverflowedUintDowncast(120, value); + } return uint120(value); } @@ -319,7 +373,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint112(uint256 value) internal pure returns (uint112) { - require(value <= type(uint112).max, "SafeCast: value doesn't fit in 112 bits"); + if (value > type(uint112).max) { + revert SafeCastOverflowedUintDowncast(112, value); + } return uint112(value); } @@ -336,7 +392,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint104(uint256 value) internal pure returns (uint104) { - require(value <= type(uint104).max, "SafeCast: value doesn't fit in 104 bits"); + if (value > type(uint104).max) { + revert SafeCastOverflowedUintDowncast(104, value); + } return uint104(value); } @@ -353,7 +411,9 @@ library SafeCast { * _Available since v4.2._ */ function toUint96(uint256 value) internal pure returns (uint96) { - require(value <= type(uint96).max, "SafeCast: value doesn't fit in 96 bits"); + if (value > type(uint96).max) { + revert SafeCastOverflowedUintDowncast(96, value); + } return uint96(value); } @@ -370,7 +430,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint88(uint256 value) internal pure returns (uint88) { - require(value <= type(uint88).max, "SafeCast: value doesn't fit in 88 bits"); + if (value > type(uint88).max) { + revert SafeCastOverflowedUintDowncast(88, value); + } return uint88(value); } @@ -387,7 +449,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint80(uint256 value) internal pure returns (uint80) { - require(value <= type(uint80).max, "SafeCast: value doesn't fit in 80 bits"); + if (value > type(uint80).max) { + revert SafeCastOverflowedUintDowncast(80, value); + } return uint80(value); } @@ -404,7 +468,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint72(uint256 value) internal pure returns (uint72) { - require(value <= type(uint72).max, "SafeCast: value doesn't fit in 72 bits"); + if (value > type(uint72).max) { + revert SafeCastOverflowedUintDowncast(72, value); + } return uint72(value); } @@ -421,7 +487,9 @@ library SafeCast { * _Available since v2.5._ */ function toUint64(uint256 value) internal pure returns (uint64) { - require(value <= type(uint64).max, "SafeCast: value doesn't fit in 64 bits"); + if (value > type(uint64).max) { + revert SafeCastOverflowedUintDowncast(64, value); + } return uint64(value); } @@ -438,7 +506,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint56(uint256 value) internal pure returns (uint56) { - require(value <= type(uint56).max, "SafeCast: value doesn't fit in 56 bits"); + if (value > type(uint56).max) { + revert SafeCastOverflowedUintDowncast(56, value); + } return uint56(value); } @@ -455,7 +525,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint48(uint256 value) internal pure returns (uint48) { - require(value <= type(uint48).max, "SafeCast: value doesn't fit in 48 bits"); + if (value > type(uint48).max) { + revert SafeCastOverflowedUintDowncast(48, value); + } return uint48(value); } @@ -472,7 +544,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint40(uint256 value) internal pure returns (uint40) { - require(value <= type(uint40).max, "SafeCast: value doesn't fit in 40 bits"); + if (value > type(uint40).max) { + revert SafeCastOverflowedUintDowncast(40, value); + } return uint40(value); } @@ -489,7 +563,9 @@ library SafeCast { * _Available since v2.5._ */ function toUint32(uint256 value) internal pure returns (uint32) { - require(value <= type(uint32).max, "SafeCast: value doesn't fit in 32 bits"); + if (value > type(uint32).max) { + revert SafeCastOverflowedUintDowncast(32, value); + } return uint32(value); } @@ -506,7 +582,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint24(uint256 value) internal pure returns (uint24) { - require(value <= type(uint24).max, "SafeCast: value doesn't fit in 24 bits"); + if (value > type(uint24).max) { + revert SafeCastOverflowedUintDowncast(24, value); + } return uint24(value); } @@ -523,7 +601,9 @@ library SafeCast { * _Available since v2.5._ */ function toUint16(uint256 value) internal pure returns (uint16) { - require(value <= type(uint16).max, "SafeCast: value doesn't fit in 16 bits"); + if (value > type(uint16).max) { + revert SafeCastOverflowedUintDowncast(16, value); + } return uint16(value); } @@ -540,7 +620,9 @@ library SafeCast { * _Available since v2.5._ */ function toUint8(uint256 value) internal pure returns (uint8) { - require(value <= type(uint8).max, "SafeCast: value doesn't fit in 8 bits"); + if (value > type(uint8).max) { + revert SafeCastOverflowedUintDowncast(8, value); + } return uint8(value); } @@ -554,7 +636,9 @@ library SafeCast { * _Available since v3.0._ */ function toUint256(int256 value) internal pure returns (uint256) { - require(value >= 0, "SafeCast: value must be positive"); + if (value < 0) { + revert SafeCastOverflowedIntToUint(value); + } return uint256(value); } @@ -573,7 +657,9 @@ library SafeCast { */ function toInt248(int256 value) internal pure returns (int248 downcasted) { downcasted = int248(value); - require(downcasted == value, "SafeCast: value doesn't fit in 248 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(248, value); + } } /** @@ -591,7 +677,9 @@ library SafeCast { */ function toInt240(int256 value) internal pure returns (int240 downcasted) { downcasted = int240(value); - require(downcasted == value, "SafeCast: value doesn't fit in 240 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(240, value); + } } /** @@ -609,7 +697,9 @@ library SafeCast { */ function toInt232(int256 value) internal pure returns (int232 downcasted) { downcasted = int232(value); - require(downcasted == value, "SafeCast: value doesn't fit in 232 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(232, value); + } } /** @@ -627,7 +717,9 @@ library SafeCast { */ function toInt224(int256 value) internal pure returns (int224 downcasted) { downcasted = int224(value); - require(downcasted == value, "SafeCast: value doesn't fit in 224 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(224, value); + } } /** @@ -645,7 +737,9 @@ library SafeCast { */ function toInt216(int256 value) internal pure returns (int216 downcasted) { downcasted = int216(value); - require(downcasted == value, "SafeCast: value doesn't fit in 216 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(216, value); + } } /** @@ -663,7 +757,9 @@ library SafeCast { */ function toInt208(int256 value) internal pure returns (int208 downcasted) { downcasted = int208(value); - require(downcasted == value, "SafeCast: value doesn't fit in 208 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(208, value); + } } /** @@ -681,7 +777,9 @@ library SafeCast { */ function toInt200(int256 value) internal pure returns (int200 downcasted) { downcasted = int200(value); - require(downcasted == value, "SafeCast: value doesn't fit in 200 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(200, value); + } } /** @@ -699,7 +797,9 @@ library SafeCast { */ function toInt192(int256 value) internal pure returns (int192 downcasted) { downcasted = int192(value); - require(downcasted == value, "SafeCast: value doesn't fit in 192 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(192, value); + } } /** @@ -717,7 +817,9 @@ library SafeCast { */ function toInt184(int256 value) internal pure returns (int184 downcasted) { downcasted = int184(value); - require(downcasted == value, "SafeCast: value doesn't fit in 184 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(184, value); + } } /** @@ -735,7 +837,9 @@ library SafeCast { */ function toInt176(int256 value) internal pure returns (int176 downcasted) { downcasted = int176(value); - require(downcasted == value, "SafeCast: value doesn't fit in 176 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(176, value); + } } /** @@ -753,7 +857,9 @@ library SafeCast { */ function toInt168(int256 value) internal pure returns (int168 downcasted) { downcasted = int168(value); - require(downcasted == value, "SafeCast: value doesn't fit in 168 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(168, value); + } } /** @@ -771,7 +877,9 @@ library SafeCast { */ function toInt160(int256 value) internal pure returns (int160 downcasted) { downcasted = int160(value); - require(downcasted == value, "SafeCast: value doesn't fit in 160 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(160, value); + } } /** @@ -789,7 +897,9 @@ library SafeCast { */ function toInt152(int256 value) internal pure returns (int152 downcasted) { downcasted = int152(value); - require(downcasted == value, "SafeCast: value doesn't fit in 152 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(152, value); + } } /** @@ -807,7 +917,9 @@ library SafeCast { */ function toInt144(int256 value) internal pure returns (int144 downcasted) { downcasted = int144(value); - require(downcasted == value, "SafeCast: value doesn't fit in 144 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(144, value); + } } /** @@ -825,7 +937,9 @@ library SafeCast { */ function toInt136(int256 value) internal pure returns (int136 downcasted) { downcasted = int136(value); - require(downcasted == value, "SafeCast: value doesn't fit in 136 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(136, value); + } } /** @@ -843,7 +957,9 @@ library SafeCast { */ function toInt128(int256 value) internal pure returns (int128 downcasted) { downcasted = int128(value); - require(downcasted == value, "SafeCast: value doesn't fit in 128 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(128, value); + } } /** @@ -861,7 +977,9 @@ library SafeCast { */ function toInt120(int256 value) internal pure returns (int120 downcasted) { downcasted = int120(value); - require(downcasted == value, "SafeCast: value doesn't fit in 120 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(120, value); + } } /** @@ -879,7 +997,9 @@ library SafeCast { */ function toInt112(int256 value) internal pure returns (int112 downcasted) { downcasted = int112(value); - require(downcasted == value, "SafeCast: value doesn't fit in 112 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(112, value); + } } /** @@ -897,7 +1017,9 @@ library SafeCast { */ function toInt104(int256 value) internal pure returns (int104 downcasted) { downcasted = int104(value); - require(downcasted == value, "SafeCast: value doesn't fit in 104 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(104, value); + } } /** @@ -915,7 +1037,9 @@ library SafeCast { */ function toInt96(int256 value) internal pure returns (int96 downcasted) { downcasted = int96(value); - require(downcasted == value, "SafeCast: value doesn't fit in 96 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(96, value); + } } /** @@ -933,7 +1057,9 @@ library SafeCast { */ function toInt88(int256 value) internal pure returns (int88 downcasted) { downcasted = int88(value); - require(downcasted == value, "SafeCast: value doesn't fit in 88 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(88, value); + } } /** @@ -951,7 +1077,9 @@ library SafeCast { */ function toInt80(int256 value) internal pure returns (int80 downcasted) { downcasted = int80(value); - require(downcasted == value, "SafeCast: value doesn't fit in 80 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(80, value); + } } /** @@ -969,7 +1097,9 @@ library SafeCast { */ function toInt72(int256 value) internal pure returns (int72 downcasted) { downcasted = int72(value); - require(downcasted == value, "SafeCast: value doesn't fit in 72 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(72, value); + } } /** @@ -987,7 +1117,9 @@ library SafeCast { */ function toInt64(int256 value) internal pure returns (int64 downcasted) { downcasted = int64(value); - require(downcasted == value, "SafeCast: value doesn't fit in 64 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(64, value); + } } /** @@ -1005,7 +1137,9 @@ library SafeCast { */ function toInt56(int256 value) internal pure returns (int56 downcasted) { downcasted = int56(value); - require(downcasted == value, "SafeCast: value doesn't fit in 56 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(56, value); + } } /** @@ -1023,7 +1157,9 @@ library SafeCast { */ function toInt48(int256 value) internal pure returns (int48 downcasted) { downcasted = int48(value); - require(downcasted == value, "SafeCast: value doesn't fit in 48 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(48, value); + } } /** @@ -1041,7 +1177,9 @@ library SafeCast { */ function toInt40(int256 value) internal pure returns (int40 downcasted) { downcasted = int40(value); - require(downcasted == value, "SafeCast: value doesn't fit in 40 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(40, value); + } } /** @@ -1059,7 +1197,9 @@ library SafeCast { */ function toInt32(int256 value) internal pure returns (int32 downcasted) { downcasted = int32(value); - require(downcasted == value, "SafeCast: value doesn't fit in 32 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(32, value); + } } /** @@ -1077,7 +1217,9 @@ library SafeCast { */ function toInt24(int256 value) internal pure returns (int24 downcasted) { downcasted = int24(value); - require(downcasted == value, "SafeCast: value doesn't fit in 24 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(24, value); + } } /** @@ -1095,7 +1237,9 @@ library SafeCast { */ function toInt16(int256 value) internal pure returns (int16 downcasted) { downcasted = int16(value); - require(downcasted == value, "SafeCast: value doesn't fit in 16 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(16, value); + } } /** @@ -1113,7 +1257,9 @@ library SafeCast { */ function toInt8(int256 value) internal pure returns (int8 downcasted) { downcasted = int8(value); - require(downcasted == value, "SafeCast: value doesn't fit in 8 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(8, value); + } } /** @@ -1127,7 +1273,9 @@ library SafeCast { */ function toInt256(uint256 value) internal pure returns (int256) { // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive - require(value <= uint256(type(int256).max), "SafeCast: value doesn't fit in an int256"); + if (value > uint256(type(int256).max)) { + revert SafeCastOverflowedUintToInt(value); + } return int256(value); } } diff --git a/contracts/utils/structs/Checkpoints.sol b/contracts/utils/structs/Checkpoints.sol index a0c45f659..7608d787e 100644 --- a/contracts/utils/structs/Checkpoints.sol +++ b/contracts/utils/structs/Checkpoints.sol @@ -17,6 +17,11 @@ import "../math/SafeCast.sol"; * _Available since v4.5._ */ library Checkpoints { + /** + * @dev A value was attempted to be inserted on a past checkpoint. + */ + error CheckpointUnorderedInsertion(); + struct Trace224 { Checkpoint224[] _checkpoints; } @@ -126,7 +131,9 @@ library Checkpoints { Checkpoint224 memory last = _unsafeAccess(self, pos - 1); // Checkpoint keys must be non-decreasing. - require(last._key <= key, "Checkpoint: decreasing keys"); + if (last._key > key) { + revert CheckpointUnorderedInsertion(); + } // Update or push new checkpoint if (last._key == key) { @@ -309,7 +316,9 @@ library Checkpoints { Checkpoint160 memory last = _unsafeAccess(self, pos - 1); // Checkpoint keys must be non-decreasing. - require(last._key <= key, "Checkpoint: decreasing keys"); + if (last._key > key) { + revert CheckpointUnorderedInsertion(); + } // Update or push new checkpoint if (last._key == key) { diff --git a/contracts/utils/structs/DoubleEndedQueue.sol b/contracts/utils/structs/DoubleEndedQueue.sol index be6e3898f..69db70040 100644 --- a/contracts/utils/structs/DoubleEndedQueue.sol +++ b/contracts/utils/structs/DoubleEndedQueue.sol @@ -22,12 +22,12 @@ library DoubleEndedQueue { /** * @dev An operation (e.g. {front}) couldn't be completed due to the queue being empty. */ - error Empty(); + error QueueEmpty(); /** * @dev An operation (e.g. {at}) couldn't be completed due to an index being out of bounds. */ - error OutOfBounds(); + error QueueOutOfBounds(); /** * @dev Indices are signed integers because the queue can grow in any direction. They are 128 bits so begin and end @@ -61,10 +61,10 @@ library DoubleEndedQueue { /** * @dev Removes the item at the end of the queue and returns it. * - * Reverts with `Empty` if the queue is empty. + * Reverts with `QueueEmpty` if the queue is empty. */ function popBack(Bytes32Deque storage deque) internal returns (bytes32 value) { - if (empty(deque)) revert Empty(); + if (empty(deque)) revert QueueEmpty(); int128 backIndex; unchecked { backIndex = deque._end - 1; @@ -89,10 +89,10 @@ library DoubleEndedQueue { /** * @dev Removes the item at the beginning of the queue and returns it. * - * Reverts with `Empty` if the queue is empty. + * Reverts with `QueueEmpty` if the queue is empty. */ function popFront(Bytes32Deque storage deque) internal returns (bytes32 value) { - if (empty(deque)) revert Empty(); + if (empty(deque)) revert QueueEmpty(); int128 frontIndex = deque._begin; value = deque._data[frontIndex]; delete deque._data[frontIndex]; @@ -104,10 +104,10 @@ library DoubleEndedQueue { /** * @dev Returns the item at the beginning of the queue. * - * Reverts with `Empty` if the queue is empty. + * Reverts with `QueueEmpty` if the queue is empty. */ function front(Bytes32Deque storage deque) internal view returns (bytes32 value) { - if (empty(deque)) revert Empty(); + if (empty(deque)) revert QueueEmpty(); int128 frontIndex = deque._begin; return deque._data[frontIndex]; } @@ -115,10 +115,10 @@ library DoubleEndedQueue { /** * @dev Returns the item at the end of the queue. * - * Reverts with `Empty` if the queue is empty. + * Reverts with `QueueEmpty` if the queue is empty. */ function back(Bytes32Deque storage deque) internal view returns (bytes32 value) { - if (empty(deque)) revert Empty(); + if (empty(deque)) revert QueueEmpty(); int128 backIndex; unchecked { backIndex = deque._end - 1; @@ -130,12 +130,12 @@ library DoubleEndedQueue { * @dev Return the item at a position in the queue given by `index`, with the first item at 0 and last item at * `length(deque) - 1`. * - * Reverts with `OutOfBounds` if the index is out of bounds. + * Reverts with `QueueOutOfBounds` if the index is out of bounds. */ function at(Bytes32Deque storage deque, uint256 index) internal view returns (bytes32 value) { // int256(deque._begin) is a safe upcast int128 idx = SafeCast.toInt128(int256(deque._begin) + SafeCast.toInt256(index)); - if (idx >= deque._end) revert OutOfBounds(); + if (idx >= deque._end) revert QueueOutOfBounds(); return deque._data[idx]; } diff --git a/contracts/utils/structs/EnumerableMap.sol b/contracts/utils/structs/EnumerableMap.sol index 4bd18055d..a474e82b3 100644 --- a/contracts/utils/structs/EnumerableMap.sol +++ b/contracts/utils/structs/EnumerableMap.sol @@ -57,6 +57,11 @@ library EnumerableMap { // This means that we can only create new EnumerableMaps for types that fit // in bytes32. + /** + * @dev Query for a nonexistent map key. + */ + error EnumerableMapNonexistentKey(bytes32 key); + struct Bytes32ToBytes32Map { // Storage of keys EnumerableSet.Bytes32Set _keys; @@ -136,7 +141,9 @@ library EnumerableMap { */ function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) { bytes32 value = map._values[key]; - require(value != 0 || contains(map, key), "EnumerableMap: nonexistent key"); + if (value == 0 && !contains(map, key)) { + revert EnumerableMapNonexistentKey(key); + } return value; } diff --git a/scripts/generate/templates/Checkpoints.js b/scripts/generate/templates/Checkpoints.js index c85cfcbb3..d28134ce7 100644 --- a/scripts/generate/templates/Checkpoints.js +++ b/scripts/generate/templates/Checkpoints.js @@ -19,6 +19,13 @@ import "../math/SafeCast.sol"; */ `; +const errors = `\ + /** + * @dev A value was attempted to be inserted on a past checkpoint. + */ + error CheckpointUnorderedInsertion(); +`; + const template = opts => `\ struct ${opts.historyTypeName} { ${opts.checkpointTypeName}[] ${opts.checkpointFieldName}; @@ -145,7 +152,9 @@ function _insert( ${opts.checkpointTypeName} memory last = _unsafeAccess(self, pos - 1); // Checkpoint keys must be non-decreasing. - require(last.${opts.keyFieldName} <= key, "Checkpoint: decreasing keys"); + if(last.${opts.keyFieldName} > key) { + revert CheckpointUnorderedInsertion(); + } // Update or push new checkpoint if (last.${opts.keyFieldName} == key) { @@ -226,6 +235,7 @@ function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos) module.exports = format( header.trimEnd(), 'library Checkpoints {', + errors, OPTS.flatMap(opts => template(opts)), '}', ); diff --git a/scripts/generate/templates/EnumerableMap.js b/scripts/generate/templates/EnumerableMap.js index 13d3d8686..8899f4819 100644 --- a/scripts/generate/templates/EnumerableMap.js +++ b/scripts/generate/templates/EnumerableMap.js @@ -66,6 +66,11 @@ const defaultMap = () => `\ // This means that we can only create new EnumerableMaps for types that fit // in bytes32. +/** + * @dev Query for a nonexistent map key. + */ +error EnumerableMapNonexistentKey(bytes32 key); + struct Bytes32ToBytes32Map { // Storage of keys EnumerableSet.Bytes32Set _keys; @@ -149,7 +154,9 @@ function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view retu */ function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) { bytes32 value = map._values[key]; - require(value != 0 || contains(map, key), "EnumerableMap: nonexistent key"); + if(value == 0 && !contains(map, key)) { + revert EnumerableMapNonexistentKey(key); + } return value; } diff --git a/scripts/generate/templates/SafeCast.js b/scripts/generate/templates/SafeCast.js index 0d78a2ca3..6a4a80c2b 100644 --- a/scripts/generate/templates/SafeCast.js +++ b/scripts/generate/templates/SafeCast.js @@ -77,6 +77,28 @@ pragma solidity ^0.8.19; */ `; +const errors = `\ + /** + * @dev Value doesn't fit in an uint of \`bits\` size. + */ + error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value); + + /** + * @dev An int value doesn't fit in an uint of \`bits\` size. + */ + error SafeCastOverflowedIntToUint(int256 value); + + /** + * @dev Value doesn't fit in an int of \`bits\` size. + */ + error SafeCastOverflowedIntDowncast(uint8 bits, int256 value); + + /** + * @dev An uint value doesn't fit in an int of \`bits\` size. + */ + error SafeCastOverflowedUintToInt(uint256 value); +`; + const toUintDownCast = length => `\ /** * @dev Returns the downcasted uint${length} from uint256, reverting on @@ -91,7 +113,9 @@ const toUintDownCast = length => `\ * _Available since v${version('toUint(uint)', length)}._ */ function toUint${length}(uint256 value) internal pure returns (uint${length}) { - require(value <= type(uint${length}).max, "SafeCast: value doesn't fit in ${length} bits"); + if (value > type(uint${length}).max) { + revert SafeCastOverflowedUintDowncast(${length}, value); + } return uint${length}(value); } `; @@ -113,7 +137,9 @@ const toIntDownCast = length => `\ */ function toInt${length}(int256 value) internal pure returns (int${length} downcasted) { downcasted = int${length}(value); - require(downcasted == value, "SafeCast: value doesn't fit in ${length} bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(${length}, value); + } } `; /* eslint-enable max-len */ @@ -130,7 +156,9 @@ const toInt = length => `\ */ function toInt${length}(uint${length} value) internal pure returns (int${length}) { // Note: Unsafe cast below is okay because \`type(int${length}).max\` is guaranteed to be positive - require(value <= uint${length}(type(int${length}).max), "SafeCast: value doesn't fit in an int${length}"); + if (value > uint${length}(type(int${length}).max)) { + revert SafeCastOverflowedUintToInt(value); + } return int${length}(value); } `; @@ -146,7 +174,9 @@ const toUint = length => `\ * _Available since v${version('toUint(int)', length)}._ */ function toUint${length}(int${length} value) internal pure returns (uint${length}) { - require(value >= 0, "SafeCast: value must be positive"); + if (value < 0) { + revert SafeCastOverflowedIntToUint(value); + } return uint${length}(value); } `; @@ -155,6 +185,7 @@ function toUint${length}(int${length} value) internal pure returns (uint${length module.exports = format( header.trimEnd(), 'library SafeCast {', + errors, [...LENGTHS.map(toUintDownCast), toUint(256), ...LENGTHS.map(toIntDownCast), toInt(256)], '}', ); diff --git a/scripts/generate/templates/StorageSlot.js b/scripts/generate/templates/StorageSlot.js index b51affc22..3e2263a0c 100644 --- a/scripts/generate/templates/StorageSlot.js +++ b/scripts/generate/templates/StorageSlot.js @@ -38,7 +38,7 @@ pragma solidity ^0.8.19; * } * * function _setImplementation(address newImplementation) internal { - * require(newImplementation.code.length > 0, "ERC1967: new implementation is not a contract"); + * require(newImplementation.code.length > 0); * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; * } * } diff --git a/test/access/AccessControl.behavior.js b/test/access/AccessControl.behavior.js index 3e61616a7..b1729c5d6 100644 --- a/test/access/AccessControl.behavior.js +++ b/test/access/AccessControl.behavior.js @@ -1,4 +1,5 @@ -const { expectEvent, expectRevert, constants, BN } = require('@openzeppelin/test-helpers'); +const { expectEvent, constants, BN } = require('@openzeppelin/test-helpers'); +const { expectRevertCustomError } = require('../helpers/customError'); const { expect } = require('chai'); const { time } = require('@nomicfoundation/hardhat-network-helpers'); @@ -12,7 +13,7 @@ const ROLE = web3.utils.soliditySha3('ROLE'); const OTHER_ROLE = web3.utils.soliditySha3('OTHER_ROLE'); const ZERO = web3.utils.toBN(0); -function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, otherAdmin) { +function shouldBehaveLikeAccessControl(admin, authorized, other, otherAdmin) { shouldSupportInterfaces(['AccessControl']); describe('default admin', function () { @@ -35,9 +36,10 @@ function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, ot }); it('non-admin cannot grant role to other accounts', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.grantRole(ROLE, authorized, { from: other }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], ); }); @@ -69,9 +71,10 @@ function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, ot }); it('non-admin cannot revoke role', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.revokeRole(ROLE, authorized, { from: other }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], ); }); @@ -103,9 +106,10 @@ function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, ot }); it('only the sender can renounce their roles', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.renounceRole(ROLE, authorized, { from: admin }), - `${errorPrefix}: can only renounce roles for self`, + 'AccessControlBadConfirmation', + [], ); }); @@ -146,16 +150,18 @@ function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, ot }); it("a role's previous admins no longer grant roles", async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.grantRole(ROLE, authorized, { from: admin }), - `${errorPrefix}: account ${admin.toLowerCase()} is missing role ${OTHER_ROLE}`, + 'AccessControlUnauthorizedAccount', + [admin.toLowerCase(), OTHER_ROLE], ); }); it("a role's previous admins no longer revoke roles", async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.revokeRole(ROLE, authorized, { from: admin }), - `${errorPrefix}: account ${admin.toLowerCase()} is missing role ${OTHER_ROLE}`, + 'AccessControlUnauthorizedAccount', + [admin.toLowerCase(), OTHER_ROLE], ); }); }); @@ -170,22 +176,24 @@ function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, ot }); it("revert if sender doesn't have role #1", async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.methods['$_checkRole(bytes32)'](ROLE, { from: other }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, ROLE], ); }); it("revert if sender doesn't have role #2", async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.methods['$_checkRole(bytes32)'](OTHER_ROLE, { from: authorized }), - `${errorPrefix}: account ${authorized.toLowerCase()} is missing role ${OTHER_ROLE}`, + 'AccessControlUnauthorizedAccount', + [authorized.toLowerCase(), OTHER_ROLE], ); }); }); } -function shouldBehaveLikeAccessControlEnumerable(errorPrefix, admin, authorized, other, otherAdmin, otherAuthorized) { +function shouldBehaveLikeAccessControlEnumerable(admin, authorized, other, otherAdmin, otherAuthorized) { shouldSupportInterfaces(['AccessControlEnumerable']); describe('enumerating', function () { @@ -215,18 +223,9 @@ function shouldBehaveLikeAccessControlEnumerable(errorPrefix, admin, authorized, }); } -function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defaultAdmin, newDefaultAdmin, other) { +function shouldBehaveLikeAccessControlDefaultAdminRules(delay, defaultAdmin, newDefaultAdmin, other) { shouldSupportInterfaces(['AccessControlDefaultAdminRules']); - function expectNoEvent(receipt, eventName) { - try { - expectEvent(receipt, eventName); - throw new Error(`${eventName} event found`); - } catch (err) { - expect(err.message).to.eq(`No '${eventName}' events found: expected false to equal true`); - } - } - for (const getter of ['owner', 'defaultAdmin']) { describe(`${getter}()`, function () { it('has a default set to the initial default admin', async function () { @@ -366,30 +365,34 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa }); it('should revert if granting default admin role', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }), - `${errorPrefix}: can't directly grant default admin role`, + 'AccessControlEnforcedDefaultAdminRules', + [], ); }); it('should revert if revoking default admin role', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.revokeRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }), - `${errorPrefix}: can't directly revoke default admin role`, + 'AccessControlEnforcedDefaultAdminRules', + [], ); }); it("should revert if defaultAdmin's admin is changed", async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.$_setRoleAdmin(DEFAULT_ADMIN_ROLE, defaultAdmin), - `${errorPrefix}: can't violate default admin rules`, + 'AccessControlEnforcedDefaultAdminRules', + [], ); }); it('should not grant the default admin role twice', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin), - `${errorPrefix}: default admin already granted`, + 'AccessControlEnforcedDefaultAdminRules', + [], ); }); @@ -398,9 +401,10 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa let acceptSchedule; it('reverts if called by non default admin accounts', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: other }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], ); }); @@ -456,7 +460,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa await this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }); const receipt = await this.accessControl.beginDefaultAdminTransfer(other, { from: newDefaultAdmin }); - expectNoEvent(receipt, 'DefaultAdminTransferCanceled'); + expectEvent.notEmitted(receipt, 'DefaultAdminTransferCanceled'); }); }); @@ -506,9 +510,10 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa it('should revert if caller is not pending default admin', async function () { await time.setNextBlockTimestamp(acceptSchedule.addn(1)); - await expectRevert( + await expectRevertCustomError( this.accessControl.acceptDefaultAdminTransfer({ from: other }), - `${errorPrefix}: pending admin must accept`, + 'AccessControlInvalidDefaultAdmin', + [other], ); }); @@ -549,9 +554,10 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa ]) { it(`should revert if block.timestamp is ${tag} to schedule`, async function () { await time.setNextBlockTimestamp(acceptSchedule.toNumber() + fromSchedule); - await expectRevert( + await expectRevertCustomError( this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }), - `${errorPrefix}: transfer delay not passed`, + 'AccessControlEnforcedDefaultAdminDelay', + [acceptSchedule], ); }); } @@ -560,9 +566,10 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa describe('cancels a default admin transfer', function () { it('reverts if called by non default admin accounts', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.cancelDefaultAdminTransfer({ from: other }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], ); }); @@ -600,9 +607,10 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa await time.setNextBlockTimestamp(acceptSchedule.addn(1)); // Previous pending default admin should not be able to accept after cancellation. - await expectRevert( + await expectRevertCustomError( this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }), - `${errorPrefix}: pending admin must accept`, + 'AccessControlInvalidDefaultAdmin', + [newDefaultAdmin], ); }); }); @@ -615,7 +623,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa expect(newAdmin).to.equal(constants.ZERO_ADDRESS); expect(schedule).to.be.bignumber.equal(ZERO); - expectNoEvent(receipt, 'DefaultAdminTransferCanceled'); + expectEvent.notEmitted(receipt, 'DefaultAdminTransferCanceled'); }); }); }); @@ -634,9 +642,10 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa it('reverts if caller is not default admin', async function () { await time.setNextBlockTimestamp(delayPassed); - await expectRevert( + await expectRevertCustomError( this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, other, { from: defaultAdmin }), - `${errorPrefix}: can only renounce roles for self`, + 'AccessControlBadConfirmation', + [], ); }); @@ -693,9 +702,10 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa ]) { it(`reverts if block.timestamp is ${tag} to schedule`, async function () { await time.setNextBlockTimestamp(delayNotPassed.toNumber() + fromSchedule); - await expectRevert( + await expectRevertCustomError( this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }), - `${errorPrefix}: only can renounce in two delayed steps`, + 'AccessControlEnforcedDefaultAdminDelay', + [expectedSchedule], ); }); } @@ -704,11 +714,12 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa describe('changes delay', function () { it('reverts if called by non default admin accounts', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.changeDefaultAdminDelay(time.duration.hours(4), { from: other, }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], ); }); @@ -792,7 +803,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa from: defaultAdmin, }); - const eventMatcher = passed ? expectNoEvent : expectEvent; + const eventMatcher = passed ? expectEvent.notEmitted : expectEvent; eventMatcher(receipt, 'DefaultAdminDelayChangeCanceled'); }); } @@ -803,9 +814,10 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa describe('rollbacks a delay change', function () { it('reverts if called by non default admin accounts', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.rollbackDefaultAdminDelay({ from: other }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], ); }); @@ -841,7 +853,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa const receipt = await this.accessControl.rollbackDefaultAdminDelay({ from: defaultAdmin }); - const eventMatcher = passed ? expectNoEvent : expectEvent; + const eventMatcher = passed ? expectEvent.notEmitted : expectEvent; eventMatcher(receipt, 'DefaultAdminDelayChangeCanceled'); }); } diff --git a/test/access/AccessControl.test.js b/test/access/AccessControl.test.js index 90efad3d0..14463b505 100644 --- a/test/access/AccessControl.test.js +++ b/test/access/AccessControl.test.js @@ -8,5 +8,5 @@ contract('AccessControl', function (accounts) { await this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, accounts[0]); }); - shouldBehaveLikeAccessControl('AccessControl', ...accounts); + shouldBehaveLikeAccessControl(...accounts); }); diff --git a/test/access/AccessControlDefaultAdminRules.test.js b/test/access/AccessControlDefaultAdminRules.test.js index be112481e..b8eae3220 100644 --- a/test/access/AccessControlDefaultAdminRules.test.js +++ b/test/access/AccessControlDefaultAdminRules.test.js @@ -16,10 +16,11 @@ contract('AccessControlDefaultAdminRules', function (accounts) { it('initial admin not zero', async function () { await expectRevert( AccessControlDefaultAdminRules.new(delay, constants.ZERO_ADDRESS), - 'AccessControl: 0 default admin', + 'AccessControlInvalidDefaultAdmin', + [constants.ZERO_ADDRESS], ); }); - shouldBehaveLikeAccessControl('AccessControl', ...accounts); - shouldBehaveLikeAccessControlDefaultAdminRules('AccessControl', delay, ...accounts); + shouldBehaveLikeAccessControl(...accounts); + shouldBehaveLikeAccessControlDefaultAdminRules(delay, ...accounts); }); diff --git a/test/access/AccessControlEnumerable.test.js b/test/access/AccessControlEnumerable.test.js index 2aa59f4c0..0e1879700 100644 --- a/test/access/AccessControlEnumerable.test.js +++ b/test/access/AccessControlEnumerable.test.js @@ -12,6 +12,6 @@ contract('AccessControl', function (accounts) { await this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, accounts[0]); }); - shouldBehaveLikeAccessControl('AccessControl', ...accounts); - shouldBehaveLikeAccessControlEnumerable('AccessControl', ...accounts); + shouldBehaveLikeAccessControl(...accounts); + shouldBehaveLikeAccessControlEnumerable(...accounts); }); diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index 07b8764a5..079d694d7 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -1,4 +1,6 @@ -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent } = require('@openzeppelin/test-helpers'); +const { expectRevertCustomError } = require('../helpers/customError'); + const { ZERO_ADDRESS } = constants; const { expect } = require('chai'); @@ -25,13 +27,18 @@ contract('Ownable', function (accounts) { }); it('prevents non-owners from transferring', async function () { - await expectRevert(this.ownable.transferOwnership(other, { from: other }), 'Ownable: caller is not the owner'); + await expectRevertCustomError( + this.ownable.transferOwnership(other, { from: other }), + 'OwnableUnauthorizedAccount', + [other], + ); }); it('guards ownership against stuck state', async function () { - await expectRevert( + await expectRevertCustomError( this.ownable.transferOwnership(ZERO_ADDRESS, { from: owner }), - 'Ownable: new owner is the zero address', + 'OwnableInvalidOwner', + [ZERO_ADDRESS], ); }); }); @@ -45,7 +52,9 @@ contract('Ownable', function (accounts) { }); it('prevents non-owners from renouncement', async function () { - await expectRevert(this.ownable.renounceOwnership({ from: other }), 'Ownable: caller is not the owner'); + await expectRevertCustomError(this.ownable.renounceOwnership({ from: other }), 'OwnableUnauthorizedAccount', [ + other, + ]); }); it('allows to recover access using the internal _transferOwnership', async function () { diff --git a/test/access/Ownable2Step.test.js b/test/access/Ownable2Step.test.js index dfda6b708..bdbac48fa 100644 --- a/test/access/Ownable2Step.test.js +++ b/test/access/Ownable2Step.test.js @@ -1,6 +1,7 @@ -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent } = require('@openzeppelin/test-helpers'); const { ZERO_ADDRESS } = constants; const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); const Ownable2Step = artifacts.require('$Ownable2Step'); @@ -29,14 +30,15 @@ contract('Ownable2Step', function (accounts) { it('guards transfer against invalid user', async function () { await this.ownable2Step.transferOwnership(accountA, { from: owner }); - await expectRevert( + await expectRevertCustomError( this.ownable2Step.acceptOwnership({ from: accountB }), - 'Ownable2Step: caller is not the new owner', + 'OwnableUnauthorizedAccount', + [accountB], ); }); }); - it('renouncing ownership', async function () { + describe('renouncing ownership', async function () { it('changes owner after renouncing ownership', async function () { await this.ownable2Step.renounceOwnership({ from: owner }); // If renounceOwnership is removed from parent an alternative is needed ... @@ -50,18 +52,19 @@ contract('Ownable2Step', function (accounts) { expect(await this.ownable2Step.pendingOwner()).to.equal(accountA); await this.ownable2Step.renounceOwnership({ from: owner }); expect(await this.ownable2Step.pendingOwner()).to.equal(ZERO_ADDRESS); - await expectRevert( + await expectRevertCustomError( this.ownable2Step.acceptOwnership({ from: accountA }), - 'Ownable2Step: caller is not the new owner', + 'OwnableUnauthorizedAccount', + [accountA], ); }); it('allows to recover access using the internal _transferOwnership', async function () { - await this.ownable.renounceOwnership({ from: owner }); - const receipt = await this.ownable.$_transferOwnership(accountA); + await this.ownable2Step.renounceOwnership({ from: owner }); + const receipt = await this.ownable2Step.$_transferOwnership(accountA); expectEvent(receipt, 'OwnershipTransferred'); - expect(await this.ownable.owner()).to.equal(accountA); + expect(await this.ownable2Step.owner()).to.equal(accountA); }); }); }); diff --git a/test/finance/VestingWallet.test.js b/test/finance/VestingWallet.test.js index 09205a99c..91ca04da0 100644 --- a/test/finance/VestingWallet.test.js +++ b/test/finance/VestingWallet.test.js @@ -1,7 +1,8 @@ -const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, time } = require('@openzeppelin/test-helpers'); const { web3 } = require('@openzeppelin/test-helpers/src/setup'); const { expect } = require('chai'); const { BNmin } = require('../helpers/math'); +const { expectRevertCustomError } = require('../helpers/customError'); const VestingWallet = artifacts.require('VestingWallet'); const ERC20 = artifacts.require('$ERC20'); @@ -20,9 +21,10 @@ contract('VestingWallet', function (accounts) { }); it('rejects zero address for beneficiary', async function () { - await expectRevert( + await expectRevertCustomError( VestingWallet.new(constants.ZERO_ADDRESS, this.start, duration), - 'VestingWallet: beneficiary is zero address', + 'VestingWalletInvalidBeneficiary', + [constants.ZERO_ADDRESS], ); }); diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js index 96feaf3a7..909c38686 100644 --- a/test/governance/Governor.test.js +++ b/test/governance/Governor.test.js @@ -6,11 +6,13 @@ const { fromRpcSig } = require('ethereumjs-util'); const Enums = require('../helpers/enums'); const { getDomain, domainType } = require('../helpers/eip712'); -const { GovernorHelper } = require('../helpers/governance'); +const { GovernorHelper, proposalStatesToBitMap } = require('../helpers/governance'); const { clockFromReceipt } = require('../helpers/time'); +const { expectRevertCustomError } = require('../helpers/customError'); const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); const { shouldBehaveLikeEIP6372 } = require('./utils/EIP6372.behavior'); +const { ZERO_BYTES32 } = require('@openzeppelin/test-helpers/src/constants'); const Governor = artifacts.require('$GovernorMock'); const CallReceiver = artifacts.require('CallReceiverMock'); @@ -237,32 +239,39 @@ contract('Governor', function (accounts) { describe('on propose', function () { it('if proposal already exists', async function () { await this.helper.propose(); - await expectRevert(this.helper.propose(), 'Governor: proposal already exists'); + await expectRevertCustomError(this.helper.propose(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Pending, + ZERO_BYTES32, + ]); }); }); describe('on vote', function () { it('if proposal does not exist', async function () { - await expectRevert( + await expectRevertCustomError( this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: unknown proposal id', + 'GovernorNonexistentProposal', + [this.proposal.id], ); }); it('if voting has not started', async function () { await this.helper.propose(); - await expectRevert( + await expectRevertCustomError( this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: vote not currently active', + 'GovernorUnexpectedProposalState', + [this.proposal.id, Enums.ProposalState.Pending, proposalStatesToBitMap([Enums.ProposalState.Active])], ); }); it('if support value is invalid', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); - await expectRevert( + await expectRevertCustomError( this.helper.vote({ support: web3.utils.toBN('255') }), - 'GovernorVotingSimple: invalid value for enum VoteType', + 'GovernorInvalidVoteType', + [], ); }); @@ -270,50 +279,64 @@ contract('Governor', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await expectRevert( + await expectRevertCustomError( this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'GovernorVotingSimple: vote already cast', + 'GovernorAlreadyCastVote', + [voter1], ); }); it('if voting is over', async function () { await this.helper.propose(); await this.helper.waitForDeadline(); - await expectRevert( + await expectRevertCustomError( this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: vote not currently active', + 'GovernorUnexpectedProposalState', + [this.proposal.id, Enums.ProposalState.Defeated, proposalStatesToBitMap([Enums.ProposalState.Active])], ); }); }); describe('on execute', function () { it('if proposal does not exist', async function () { - await expectRevert(this.helper.execute(), 'Governor: unknown proposal id'); + await expectRevertCustomError(this.helper.execute(), 'GovernorNonexistentProposal', [this.proposal.id]); }); it('if quorum is not reached', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter3 }); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Active, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); }); it('if score not reached', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 }); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Active, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); }); it('if voting is not over', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Active, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); }); it('if receiver revert without reason', async function () { - this.proposal = this.helper.setProposal( + this.helper.setProposal( [ { target: this.receiver.address, @@ -327,11 +350,11 @@ contract('Governor', function (accounts) { await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); await this.helper.waitForDeadline(); - await expectRevert(this.helper.execute(), 'Governor: call reverted without message'); + await expectRevertCustomError(this.helper.execute(), 'FailedInnerCall', []); }); it('if receiver revert with reason', async function () { - this.proposal = this.helper.setProposal( + this.helper.setProposal( [ { target: this.receiver.address, @@ -354,14 +377,20 @@ contract('Governor', function (accounts) { await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); await this.helper.waitForDeadline(); await this.helper.execute(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Executed, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); }); }); }); describe('state', function () { it('Unset', async function () { - await expectRevert(this.mock.state(this.proposal.id), 'Governor: unknown proposal id'); + await expectRevertCustomError(this.mock.state(this.proposal.id), 'GovernorNonexistentProposal', [ + this.proposal.id, + ]); }); it('Pending & Active', async function () { @@ -404,7 +433,9 @@ contract('Governor', function (accounts) { describe('cancel', function () { describe('internal', function () { it('before proposal', async function () { - await expectRevert(this.helper.cancel('internal'), 'Governor: unknown proposal id'); + await expectRevertCustomError(this.helper.cancel('internal'), 'GovernorNonexistentProposal', [ + this.proposal.id, + ]); }); it('after proposal', async function () { @@ -414,9 +445,10 @@ contract('Governor', function (accounts) { expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); await this.helper.waitForSnapshot(); - await expectRevert( + await expectRevertCustomError( this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: vote not currently active', + 'GovernorUnexpectedProposalState', + [this.proposal.id, Enums.ProposalState.Canceled, proposalStatesToBitMap([Enums.ProposalState.Active])], ); }); @@ -429,7 +461,11 @@ contract('Governor', function (accounts) { expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); await this.helper.waitForDeadline(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Canceled, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); }); it('after deadline', async function () { @@ -441,7 +477,11 @@ contract('Governor', function (accounts) { await this.helper.cancel('internal'); expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Canceled, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); }); it('after execution', async function () { @@ -451,13 +491,22 @@ contract('Governor', function (accounts) { await this.helper.waitForDeadline(); await this.helper.execute(); - await expectRevert(this.helper.cancel('internal'), 'Governor: proposal not active'); + await expectRevertCustomError(this.helper.cancel('internal'), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Executed, + proposalStatesToBitMap( + [Enums.ProposalState.Canceled, Enums.ProposalState.Expired, Enums.ProposalState.Executed], + { inverted: true }, + ), + ]); }); }); describe('public', function () { it('before proposal', async function () { - await expectRevert(this.helper.cancel('external'), 'Governor: unknown proposal id'); + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorNonexistentProposal', [ + this.proposal.id, + ]); }); it('after proposal', async function () { @@ -469,14 +518,20 @@ contract('Governor', function (accounts) { it('after proposal - restricted to proposer', async function () { await this.helper.propose(); - await expectRevert(this.helper.cancel('external', { from: owner }), 'Governor: only proposer can cancel'); + await expectRevertCustomError(this.helper.cancel('external', { from: owner }), 'GovernorOnlyProposer', [ + owner, + ]); }); it('after vote started', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(1); // snapshot + 1 block - await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel'); + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Active, + proposalStatesToBitMap([Enums.ProposalState.Pending]), + ]); }); it('after vote', async function () { @@ -484,7 +539,11 @@ contract('Governor', function (accounts) { await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel'); + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Active, + proposalStatesToBitMap([Enums.ProposalState.Pending]), + ]); }); it('after deadline', async function () { @@ -493,7 +552,11 @@ contract('Governor', function (accounts) { await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); await this.helper.waitForDeadline(); - await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel'); + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Succeeded, + proposalStatesToBitMap([Enums.ProposalState.Pending]), + ]); }); it('after execution', async function () { @@ -503,7 +566,11 @@ contract('Governor', function (accounts) { await this.helper.waitForDeadline(); await this.helper.execute(); - await expectRevert(this.helper.cancel('external'), 'Governor: too late to cancel'); + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Executed, + proposalStatesToBitMap([Enums.ProposalState.Pending]), + ]); }); }); }); @@ -511,7 +578,7 @@ contract('Governor', function (accounts) { describe('proposal length', function () { it('empty', async function () { this.helper.setProposal([], ''); - await expectRevert(this.helper.propose(), 'Governor: empty proposal'); + await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [0, 0, 0]); }); it('mismatch #1', async function () { @@ -523,7 +590,7 @@ contract('Governor', function (accounts) { }, '', ); - await expectRevert(this.helper.propose(), 'Governor: invalid proposal length'); + await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [0, 1, 1]); }); it('mismatch #2', async function () { @@ -535,7 +602,7 @@ contract('Governor', function (accounts) { }, '', ); - await expectRevert(this.helper.propose(), 'Governor: invalid proposal length'); + await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [1, 1, 0]); }); it('mismatch #3', async function () { @@ -547,7 +614,7 @@ contract('Governor', function (accounts) { }, '', ); - await expectRevert(this.helper.propose(), 'Governor: invalid proposal length'); + await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [1, 0, 1]); }); }); @@ -636,15 +703,23 @@ contract('Governor', function (accounts) { describe('onlyGovernance updates', function () { it('setVotingDelay is protected', async function () { - await expectRevert(this.mock.setVotingDelay('0'), 'Governor: onlyGovernance'); + await expectRevertCustomError(this.mock.setVotingDelay('0', { from: owner }), 'GovernorOnlyExecutor', [ + owner, + ]); }); it('setVotingPeriod is protected', async function () { - await expectRevert(this.mock.setVotingPeriod('32'), 'Governor: onlyGovernance'); + await expectRevertCustomError(this.mock.setVotingPeriod('32', { from: owner }), 'GovernorOnlyExecutor', [ + owner, + ]); }); it('setProposalThreshold is protected', async function () { - await expectRevert(this.mock.setProposalThreshold('1000000000000000000'), 'Governor: onlyGovernance'); + await expectRevertCustomError( + this.mock.setProposalThreshold('1000000000000000000', { from: owner }), + 'GovernorOnlyExecutor', + [owner], + ); }); it('can setVotingDelay through governance', async function () { @@ -690,11 +765,12 @@ contract('Governor', function (accounts) { }); it('cannot setVotingPeriod to 0 through governance', async function () { + const votingPeriod = 0; this.helper.setProposal( [ { target: this.mock.address, - data: this.mock.contract.methods.setVotingPeriod('0').encodeABI(), + data: this.mock.contract.methods.setVotingPeriod(votingPeriod).encodeABI(), }, ], '', @@ -705,7 +781,7 @@ contract('Governor', function (accounts) { await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); await this.helper.waitForDeadline(); - await expectRevert(this.helper.execute(), 'GovernorSettings: voting period too low'); + await expectRevertCustomError(this.helper.execute(), 'GovernorInvalidVotingPeriod', [votingPeriod]); }); it('can setProposalThreshold to 0 through governance', async function () { diff --git a/test/governance/TimelockController.test.js b/test/governance/TimelockController.test.js index e9ddfaf47..d8fcdce6c 100644 --- a/test/governance/TimelockController.test.js +++ b/test/governance/TimelockController.test.js @@ -4,6 +4,8 @@ const { ZERO_ADDRESS, ZERO_BYTES32 } = constants; const { expect } = require('chai'); const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); +const { expectRevertCustomError } = require('../helpers/customError'); +const { OperationState } = require('../helpers/enums'); const TimelockController = artifacts.require('TimelockController'); const CallReceiverMock = artifacts.require('CallReceiverMock'); @@ -182,7 +184,7 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); - await expectRevert( + await expectRevertCustomError( this.mock.schedule( this.operation.target, this.operation.value, @@ -192,12 +194,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: proposer }, ), - 'TimelockController: operation already scheduled', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Unset], ); }); it('prevent non-proposer from committing', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.schedule( this.operation.target, this.operation.value, @@ -207,12 +210,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: other }, ), - `AccessControl: account ${other.toLowerCase()} is missing role ${PROPOSER_ROLE}`, + `AccessControlUnauthorizedAccount`, + [other, PROPOSER_ROLE], ); }); it('enforce minimum delay', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.schedule( this.operation.target, this.operation.value, @@ -222,7 +226,8 @@ contract('TimelockController', function (accounts) { MINDELAY - 1, { from: proposer }, ), - 'TimelockController: insufficient delay', + 'TimelockInsufficientDelay', + [MINDELAY, MINDELAY - 1], ); }); @@ -252,7 +257,7 @@ contract('TimelockController', function (accounts) { }); it('revert if operation is not scheduled', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.execute( this.operation.target, this.operation.value, @@ -261,7 +266,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -279,7 +285,7 @@ contract('TimelockController', function (accounts) { }); it('revert if execution comes too early 1/2', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.execute( this.operation.target, this.operation.value, @@ -288,7 +294,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -296,7 +303,7 @@ contract('TimelockController', function (accounts) { const timestamp = await this.mock.getTimestamp(this.operation.id); await time.increaseTo(timestamp - 5); // -1 is too tight, test sometime fails - await expectRevert( + await expectRevertCustomError( this.mock.execute( this.operation.target, this.operation.value, @@ -305,7 +312,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -334,7 +342,7 @@ contract('TimelockController', function (accounts) { }); it('prevent non-executor from revealing', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.execute( this.operation.target, this.operation.value, @@ -343,7 +351,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: other }, ), - `AccessControl: account ${other.toLowerCase()} is missing role ${EXECUTOR_ROLE}`, + `AccessControlUnauthorizedAccount`, + [other, EXECUTOR_ROLE], ); }); @@ -389,7 +398,7 @@ contract('TimelockController', function (accounts) { await reentrant.enableRentrancy(this.mock.address, data); // Expect to fail - await expectRevert( + await expectRevertCustomError( this.mock.execute( reentrantOperation.target, reentrantOperation.value, @@ -398,7 +407,8 @@ contract('TimelockController', function (accounts) { reentrantOperation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [reentrantOperation.id, OperationState.Ready], ); // Disable reentrancy @@ -484,7 +494,7 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); - await expectRevert( + await expectRevertCustomError( this.mock.scheduleBatch( this.operation.targets, this.operation.values, @@ -494,12 +504,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: proposer }, ), - 'TimelockController: operation already scheduled', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Unset], ); }); it('length of batch parameter must match #1', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.scheduleBatch( this.operation.targets, [], @@ -509,12 +520,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: proposer }, ), - 'TimelockController: length mismatch', + 'TimelockInvalidOperationLength', + [this.operation.targets.length, this.operation.payloads.length, 0], ); }); it('length of batch parameter must match #1', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.scheduleBatch( this.operation.targets, this.operation.values, @@ -524,12 +536,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: proposer }, ), - 'TimelockController: length mismatch', + 'TimelockInvalidOperationLength', + [this.operation.targets.length, 0, this.operation.payloads.length], ); }); it('prevent non-proposer from committing', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.scheduleBatch( this.operation.targets, this.operation.values, @@ -539,12 +552,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: other }, ), - `AccessControl: account ${other.toLowerCase()} is missing role ${PROPOSER_ROLE}`, + `AccessControlUnauthorizedAccount`, + [other, PROPOSER_ROLE], ); }); it('enforce minimum delay', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.scheduleBatch( this.operation.targets, this.operation.values, @@ -554,7 +568,8 @@ contract('TimelockController', function (accounts) { MINDELAY - 1, { from: proposer }, ), - 'TimelockController: insufficient delay', + 'TimelockInsufficientDelay', + [MINDELAY, MINDELAY - 1], ); }); }); @@ -571,7 +586,7 @@ contract('TimelockController', function (accounts) { }); it('revert if operation is not scheduled', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, this.operation.values, @@ -580,7 +595,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -598,7 +614,7 @@ contract('TimelockController', function (accounts) { }); it('revert if execution comes too early 1/2', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, this.operation.values, @@ -607,7 +623,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -615,7 +632,7 @@ contract('TimelockController', function (accounts) { const timestamp = await this.mock.getTimestamp(this.operation.id); await time.increaseTo(timestamp - 5); // -1 is to tight, test sometime fails - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, this.operation.values, @@ -624,7 +641,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -655,7 +673,7 @@ contract('TimelockController', function (accounts) { }); it('prevent non-executor from revealing', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, this.operation.values, @@ -664,12 +682,13 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: other }, ), - `AccessControl: account ${other.toLowerCase()} is missing role ${EXECUTOR_ROLE}`, + `AccessControlUnauthorizedAccount`, + [other, EXECUTOR_ROLE], ); }); it('length mismatch #1', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( [], this.operation.values, @@ -678,12 +697,13 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: length mismatch', + 'TimelockInvalidOperationLength', + [0, this.operation.payloads.length, this.operation.values.length], ); }); it('length mismatch #2', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, [], @@ -692,12 +712,13 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: length mismatch', + 'TimelockInvalidOperationLength', + [this.operation.targets.length, this.operation.payloads.length, 0], ); }); it('length mismatch #3', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, this.operation.values, @@ -706,7 +727,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: length mismatch', + 'TimelockInvalidOperationLength', + [this.operation.targets.length, 0, this.operation.values.length], ); }); @@ -752,7 +774,7 @@ contract('TimelockController', function (accounts) { await reentrant.enableRentrancy(this.mock.address, data); // Expect to fail - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( reentrantBatchOperation.targets, reentrantBatchOperation.values, @@ -761,7 +783,8 @@ contract('TimelockController', function (accounts) { reentrantBatchOperation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [reentrantBatchOperation.id, OperationState.Ready], ); // Disable reentrancy @@ -796,7 +819,7 @@ contract('TimelockController', function (accounts) { [0, 0, 0], [ this.callreceivermock.contract.methods.mockFunction().encodeABI(), - this.callreceivermock.contract.methods.mockFunctionThrows().encodeABI(), + this.callreceivermock.contract.methods.mockFunctionRevertsNoReason().encodeABI(), this.callreceivermock.contract.methods.mockFunction().encodeABI(), ], ZERO_BYTES32, @@ -813,7 +836,7 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); await time.increase(MINDELAY); - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( operation.targets, operation.values, @@ -822,7 +845,8 @@ contract('TimelockController', function (accounts) { operation.salt, { from: executor }, ), - 'TimelockController: underlying transaction reverted', + 'FailedInnerCall', + [], ); }); }); @@ -854,16 +878,18 @@ contract('TimelockController', function (accounts) { }); it('cannot cancel invalid operation', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.cancel(constants.ZERO_BYTES32, { from: canceller }), - 'TimelockController: operation cannot be cancelled', + 'TimelockUnexpectedOperationState', + [constants.ZERO_BYTES32, OperationState.Pending], ); }); it('prevent non-canceller from canceling', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.cancel(this.operation.id, { from: other }), - `AccessControl: account ${other.toLowerCase()} is missing role ${CANCELLER_ROLE}`, + `AccessControlUnauthorizedAccount`, + [other, CANCELLER_ROLE], ); }); }); @@ -871,7 +897,7 @@ contract('TimelockController', function (accounts) { describe('maintenance', function () { it('prevent unauthorized maintenance', async function () { - await expectRevert(this.mock.updateDelay(0, { from: other }), 'TimelockController: caller must be timelock'); + await expectRevertCustomError(this.mock.updateDelay(0, { from: other }), 'TimelockUnauthorizedCaller', [other]); }); it('timelock scheduled maintenance', async function () { @@ -946,7 +972,7 @@ contract('TimelockController', function (accounts) { }); it('cannot execute before dependency', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.execute( this.operation2.target, this.operation2.value, @@ -955,7 +981,8 @@ contract('TimelockController', function (accounts) { this.operation2.salt, { from: executor }, ), - 'TimelockController: missing dependency', + 'TimelockUnexecutedPredecessor', + [this.operation1.id], ); }); @@ -1032,11 +1059,12 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); await time.increase(MINDELAY); - await expectRevert( + await expectRevertCustomError( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, }), - 'TimelockController: underlying transaction reverted', + 'FailedInnerCall', + [], ); }); @@ -1059,11 +1087,11 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); await time.increase(MINDELAY); - await expectRevert( + // Targeted function reverts with a panic code (0x1) + the timelock bubble the panic code + await expectRevert.unspecified( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, }), - 'TimelockController: underlying transaction reverted', ); }); @@ -1086,12 +1114,13 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); await time.increase(MINDELAY); - await expectRevert( + await expectRevertCustomError( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, - gas: '70000', + gas: '100000', }), - 'TimelockController: underlying transaction reverted', + 'FailedInnerCall', + [], ); }); @@ -1154,11 +1183,12 @@ contract('TimelockController', function (accounts) { expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - await expectRevert( + await expectRevertCustomError( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, }), - 'TimelockController: underlying transaction reverted', + 'FailedInnerCall', + [], ); expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); @@ -1188,11 +1218,12 @@ contract('TimelockController', function (accounts) { expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - await expectRevert( + await expectRevertCustomError( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, }), - 'TimelockController: underlying transaction reverted', + 'FailedInnerCall', + [], ); expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); diff --git a/test/governance/compatibility/GovernorCompatibilityBravo.test.js b/test/governance/compatibility/GovernorCompatibilityBravo.test.js index 9c45277d1..4182dfb4e 100644 --- a/test/governance/compatibility/GovernorCompatibilityBravo.test.js +++ b/test/governance/compatibility/GovernorCompatibilityBravo.test.js @@ -1,9 +1,10 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const RLP = require('rlp'); const Enums = require('../../helpers/enums'); const { GovernorHelper } = require('../../helpers/governance'); const { clockFromReceipt } = require('../../helpers/time'); +const { expectRevertCustomError } = require('../../helpers/customError'); const Timelock = artifacts.require('CompTimelock'); const Governor = artifacts.require('$GovernorCompatibilityBravoMock'); @@ -38,6 +39,16 @@ contract('GovernorCompatibilityBravo', function (accounts) { const proposalThreshold = web3.utils.toWei('10'); const value = web3.utils.toWei('1'); + const votes = { + [owner]: tokenSupply, + [proposer]: proposalThreshold, + [voter1]: web3.utils.toWei('10'), + [voter2]: web3.utils.toWei('7'), + [voter3]: web3.utils.toWei('5'), + [voter4]: web3.utils.toWei('2'), + [other]: 0, + }; + for (const { mode, Token } of TOKENS) { describe(`using ${Token._json.contractName}`, function () { beforeEach(async function () { @@ -65,11 +76,11 @@ contract('GovernorCompatibilityBravo', function (accounts) { await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: proposer, value: proposalThreshold }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: proposer, value: votes[proposer] }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter1, value: votes[voter1] }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: votes[voter2] }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: votes[voter3] }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: votes[voter4] }, { from: owner }); // default proposal this.proposal = this.helper.setProposal( @@ -182,9 +193,10 @@ contract('GovernorCompatibilityBravo', function (accounts) { await this.helper.propose({ from: proposer }); await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await expectRevert( + await expectRevertCustomError( this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'GovernorCompatibilityBravo: vote already cast', + 'GovernorAlreadyCastVote', + [voter1], ); }); @@ -226,36 +238,43 @@ contract('GovernorCompatibilityBravo', function (accounts) { it('with inconsistent array size for selector and arguments', async function () { const target = this.receiver.address; + const signatures = ['mockFunction()']; // One signature + const data = ['0x', this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI()]; // Two data entries this.helper.setProposal( { targets: [target, target], values: [0, 0], - signatures: ['mockFunction()'], // One signature - data: ['0x', this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI()], // Two data entries + signatures, + data, }, '', ); - await expectRevert(this.helper.propose({ from: proposer }), 'GovernorBravo: invalid signatures length'); + await expectRevertCustomError(this.helper.propose({ from: proposer }), 'GovernorInvalidSignaturesLength', [ + signatures.length, + data.length, + ]); }); describe('should revert', function () { describe('on propose', function () { it('if proposal does not meet proposalThreshold', async function () { - await expectRevert( - this.helper.propose({ from: other }), - 'Governor: proposer votes below proposal threshold', - ); + await expectRevertCustomError(this.helper.propose({ from: other }), 'GovernorInsufficientProposerVotes', [ + other, + votes[other], + proposalThreshold, + ]); }); }); describe('on vote', function () { - it('if vote type is invalide', async function () { + it('if vote type is invalid', async function () { await this.helper.propose({ from: proposer }); await this.helper.waitForSnapshot(); - await expectRevert( + await expectRevertCustomError( this.helper.vote({ support: 5 }, { from: voter1 }), - 'GovernorCompatibilityBravo: invalid vote type', + 'GovernorInvalidVoteType', + [], ); }); }); @@ -275,7 +294,11 @@ contract('GovernorCompatibilityBravo', function (accounts) { it('cannot cancel is proposer is still above threshold', async function () { await this.helper.propose({ from: proposer }); - await expectRevert(this.helper.cancel('external'), 'GovernorBravo: proposer above threshold'); + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorInsufficientProposerVotes', [ + proposer, + votes[proposer], + proposalThreshold, + ]); }); }); }); diff --git a/test/governance/extensions/GovernorPreventLateQuorum.test.js b/test/governance/extensions/GovernorPreventLateQuorum.test.js index 4df5adb1c..17ae05a73 100644 --- a/test/governance/extensions/GovernorPreventLateQuorum.test.js +++ b/test/governance/extensions/GovernorPreventLateQuorum.test.js @@ -1,9 +1,10 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const Enums = require('../../helpers/enums'); const { GovernorHelper } = require('../../helpers/governance'); const { clockFromReceipt } = require('../../helpers/time'); +const { expectRevertCustomError } = require('../../helpers/customError'); const Governor = artifacts.require('$GovernorPreventLateQuorumMock'); const CallReceiver = artifacts.require('CallReceiverMock'); @@ -158,7 +159,11 @@ contract('GovernorPreventLateQuorum', function (accounts) { describe('onlyGovernance updates', function () { it('setLateQuorumVoteExtension is protected', async function () { - await expectRevert(this.mock.setLateQuorumVoteExtension(0), 'Governor: onlyGovernance'); + await expectRevertCustomError( + this.mock.setLateQuorumVoteExtension(0, { from: owner }), + 'GovernorOnlyExecutor', + [owner], + ); }); it('can setLateQuorumVoteExtension through governance', async function () { diff --git a/test/governance/extensions/GovernorTimelockCompound.test.js b/test/governance/extensions/GovernorTimelockCompound.test.js index 2cbce2600..8c6680aa8 100644 --- a/test/governance/extensions/GovernorTimelockCompound.test.js +++ b/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], ''); + const { id } = this.helper.setProposal([action, action], ''); await this.helper.propose(); await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); await this.helper.waitForDeadline(); - await expectRevert( - this.helper.queue(), - 'GovernorTimelockCompound: identical proposal action already queued', - ); - await expectRevert(this.helper.execute(), 'GovernorTimelockCompound: proposal not yet queued'); + await expectRevertCustomError(this.helper.queue(), 'GovernorAlreadyQueuedProposal', [id]); + await expectRevertCustomError(this.helper.execute(), 'GovernorNotQueuedProposal', [id]); }); }); @@ -160,7 +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 () { diff --git a/test/governance/extensions/GovernorTimelockControl.test.js b/test/governance/extensions/GovernorTimelockControl.test.js index af57ba90b..ef32148fa 100644 --- a/test/governance/extensions/GovernorTimelockControl.test.js +++ b/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 () { diff --git a/test/governance/extensions/GovernorVotesQuorumFraction.test.js b/test/governance/extensions/GovernorVotesQuorumFraction.test.js index a69338ad8..ece9c78d6 100644 --- a/test/governance/extensions/GovernorVotesQuorumFraction.test.js +++ b/test/governance/extensions/GovernorVotesQuorumFraction.test.js @@ -1,9 +1,10 @@ -const { expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expectEvent, time } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const Enums = require('../../helpers/enums'); -const { GovernorHelper } = require('../../helpers/governance'); +const { GovernorHelper, proposalStatesToBitMap } = require('../../helpers/governance'); const { clock } = require('../../helpers/time'); +const { expectRevertCustomError } = require('../../helpers/customError'); const Governor = artifacts.require('$GovernorMock'); const CallReceiver = artifacts.require('CallReceiverMock'); @@ -84,12 +85,20 @@ contract('GovernorVotesQuorumFraction', function (accounts) { await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); await this.helper.waitForDeadline(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Defeated, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); }); describe('onlyGovernance updates', function () { it('updateQuorumNumerator is protected', async function () { - await expectRevert(this.mock.updateQuorumNumerator(newRatio), 'Governor: onlyGovernance'); + await expectRevertCustomError( + this.mock.updateQuorumNumerator(newRatio, { from: owner }), + 'GovernorOnlyExecutor', + [owner], + ); }); it('can updateQuorumNumerator through governance', async function () { @@ -129,11 +138,12 @@ contract('GovernorVotesQuorumFraction', function (accounts) { }); it('cannot updateQuorumNumerator over the maximum', async function () { + const quorumNumerator = 101; this.helper.setProposal( [ { target: this.mock.address, - data: this.mock.contract.methods.updateQuorumNumerator('101').encodeABI(), + data: this.mock.contract.methods.updateQuorumNumerator(quorumNumerator).encodeABI(), }, ], '', @@ -144,10 +154,12 @@ contract('GovernorVotesQuorumFraction', function (accounts) { await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); await this.helper.waitForDeadline(); - await expectRevert( - this.helper.execute(), - 'GovernorVotesQuorumFraction: quorumNumerator over quorumDenominator', - ); + const quorumDenominator = await this.mock.quorumDenominator(); + + await expectRevertCustomError(this.helper.execute(), 'GovernorInvalidQuorumFraction', [ + quorumNumerator, + quorumDenominator, + ]); }); }); }); diff --git a/test/governance/utils/Votes.behavior.js b/test/governance/utils/Votes.behavior.js index 37062e19c..20ebdba4f 100644 --- a/test/governance/utils/Votes.behavior.js +++ b/test/governance/utils/Votes.behavior.js @@ -1,4 +1,4 @@ -const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, time } = require('@openzeppelin/test-helpers'); const { MAX_UINT256, ZERO_ADDRESS } = constants; @@ -9,6 +9,7 @@ const Wallet = require('ethereumjs-wallet').default; const { shouldBehaveLikeEIP6372 } = require('./EIP6372.behavior'); const { getDomain, domainType } = require('../../helpers/eip712'); const { clockFromReceipt } = require('../../helpers/time'); +const { expectRevertCustomError } = require('../../helpers/customError'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -176,7 +177,11 @@ function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungibl await this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s); - await expectRevert(this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s), 'Votes: invalid nonce'); + await expectRevertCustomError( + this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s), + 'InvalidAccountNonce', + [delegator.address, nonce + 1], + ); }); it('rejects bad delegatee', async function () { @@ -208,9 +213,10 @@ function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungibl delegator.getPrivateKey(), ); - await expectRevert( + await expectRevertCustomError( this.votes.delegateBySig(delegatee, nonce + 1, MAX_UINT256, v, r, s), - 'Votes: invalid nonce', + 'InvalidAccountNonce', + [delegator.address, 0], ); }); @@ -226,7 +232,11 @@ function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungibl delegator.getPrivateKey(), ); - await expectRevert(this.votes.delegateBySig(delegatee, nonce, expiry, v, r, s), 'Votes: signature expired'); + await expectRevertCustomError( + this.votes.delegateBySig(delegatee, nonce, expiry, v, r, s), + 'VotesExpiredSignature', + [expiry], + ); }); }); }); @@ -237,7 +247,12 @@ function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungibl }); it('reverts if block number >= current block', async function () { - await expectRevert(this.votes.getPastTotalSupply(5e10), 'future lookup'); + const timepoint = 5e10; + const clock = await this.votes.clock(); + await expectRevertCustomError(this.votes.getPastTotalSupply(timepoint), 'ERC5805FutureLookup', [ + timepoint, + clock, + ]); }); it('returns 0 if there are no checkpoints', async function () { @@ -285,7 +300,10 @@ function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungibl expect(await this.votes.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal(weight[2]); expect(await this.votes.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal(weight[2]); expect(await this.votes.getPastTotalSupply(t5.timepoint)).to.be.bignumber.equal('0'); - await expectRevert(this.votes.getPastTotalSupply(t5.timepoint + 1), 'Votes: future lookup'); + await expectRevertCustomError(this.votes.getPastTotalSupply(t5.timepoint + 1), 'ERC5805FutureLookup', [ + t5.timepoint + 1, // timepoint + t5.timepoint + 1, // clock + ]); }); }); @@ -300,7 +318,12 @@ function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungibl describe('getPastVotes', function () { it('reverts if block number >= current block', async function () { - await expectRevert(this.votes.getPastVotes(accounts[2], 5e10), 'future lookup'); + const clock = await this.votes.clock(); + const timepoint = 5e10; // far in the future + await expectRevertCustomError(this.votes.getPastVotes(accounts[2], timepoint), 'ERC5805FutureLookup', [ + timepoint, + clock, + ]); }); it('returns 0 if there are no checkpoints', async function () { diff --git a/test/governance/utils/Votes.test.js b/test/governance/utils/Votes.test.js index 184ce0cec..b2b80f9fe 100644 --- a/test/governance/utils/Votes.test.js +++ b/test/governance/utils/Votes.test.js @@ -1,7 +1,8 @@ -const { constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { clockFromReceipt } = require('../../helpers/time'); const { BNsum } = require('../../helpers/math'); +const { expectRevertCustomError } = require('../../helpers/customError'); require('array.prototype.at/auto'); @@ -45,7 +46,11 @@ contract('Votes', function (accounts) { it('reverts if block number >= current block', async function () { const lastTxTimepoint = await clockFromReceipt[mode](this.txs.at(-1).receipt); - await expectRevert(this.votes.getPastTotalSupply(lastTxTimepoint + 1), 'Votes: future lookup'); + const clock = await this.votes.clock(); + await expectRevertCustomError(this.votes.getPastTotalSupply(lastTxTimepoint + 1), 'ERC5805FutureLookup', [ + lastTxTimepoint + 1, + clock, + ]); }); it('delegates', async function () { diff --git a/test/helpers/customError.js b/test/helpers/customError.js index 3cfcd7277..a193ab0cf 100644 --- a/test/helpers/customError.js +++ b/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(/(?\w+)\((?.*)\)/); + if (!match) { + expect.fail(`Couldn't parse "${error}" as a custom error`); + } + // Extract the error name and parameters + const errorName = match.groups.name; + const argMatches = [...match.groups.args.matchAll(/-?\w+/g)]; + + // Assert error name + expect(errorName).to.be.equal( + expectedErrorName, + `Unexpected custom error name (with found args: [${argMatches.map(([a]) => a)}])`, + ); + + // Coerce to string for comparison since `arg` can be either a number or hex. + const sanitizedExpected = args.map(arg => arg.toString().toLowerCase()); + const sanitizedActual = argMatches.map(([arg]) => arg.toString().toLowerCase()); + + // Assert argument equality + expect(sanitizedActual).to.have.members(sanitizedExpected, `Unexpected ${errorName} arguments`); } } diff --git a/test/helpers/enums.js b/test/helpers/enums.js index cc650abf4..d4a4fdbd0 100644 --- a/test/helpers/enums.js +++ b/test/helpers/enums.js @@ -7,4 +7,5 @@ module.exports = { ProposalState: Enum('Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'), VoteType: Enum('Against', 'For', 'Abstain'), Rounding: Enum('Down', 'Up', 'Zero'), + OperationState: Enum('Unset', 'Pending', 'Ready', 'Done'), }; diff --git a/test/helpers/governance.js b/test/helpers/governance.js index 4b38b7588..665c21605 100644 --- a/test/helpers/governance.js +++ b/test/helpers/governance.js @@ -1,4 +1,5 @@ const { forward } = require('../helpers/time'); +const { ProposalState } = require('./enums'); function zip(...args) { return Array(Math.max(...args.map(array => array.length))) @@ -196,6 +197,44 @@ class GovernorHelper { } } +/** + * Encodes a list ProposalStates into a bytes32 representation where each bit enabled corresponds to + * the underlying position in the `ProposalState` enum. For example: + * + * 0x000...10000 + * ^^^^^^------ ... + * ^----- Succeeded + * ^---- Defeated + * ^--- Canceled + * ^-- Active + * ^- Pending + */ +function proposalStatesToBitMap(proposalStates, options = {}) { + if (!Array.isArray(proposalStates)) { + proposalStates = [proposalStates]; + } + const statesCount = Object.keys(ProposalState).length; + let result = 0; + + const uniqueProposalStates = new Set(proposalStates.map(bn => bn.toNumber())); // Remove duplicates + for (const state of uniqueProposalStates) { + if (state < 0 || state >= statesCount) { + expect.fail(`ProposalState ${state} out of possible states (0...${statesCount}-1)`); + } else { + result |= 1 << state; + } + } + + if (options.inverted) { + const mask = 2 ** statesCount - 1; + result = result ^ mask; + } + + const hex = web3.utils.numberToHex(result); + return web3.utils.padLeft(hex, 64); +} + module.exports = { GovernorHelper, + proposalStatesToBitMap, }; diff --git a/test/metatx/MinimalForwarder.test.js b/test/metatx/MinimalForwarder.test.js index 4884cc760..c775c5e44 100644 --- a/test/metatx/MinimalForwarder.test.js +++ b/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, + ]); }); }); diff --git a/test/proxy/Clones.test.js b/test/proxy/Clones.test.js index 947b2ed95..2edd1999c 100644 --- a/test/proxy/Clones.test.js +++ b/test/proxy/Clones.test.js @@ -1,7 +1,9 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { computeCreate2Address } = require('../helpers/create2'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); + const shouldBehaveLikeClone = require('./Clones.behaviour'); const Clones = artifacts.require('$Clones'); @@ -36,7 +38,7 @@ contract('Clones', function (accounts) { // deploy once expectEvent(await factory.$cloneDeterministic(implementation, salt), 'return$cloneDeterministic'); // deploy twice - await expectRevert(factory.$cloneDeterministic(implementation, salt), 'ERC1167: create2 failed'); + await expectRevertCustomError(factory.$cloneDeterministic(implementation, salt), 'ERC1167FailedCreateClone', []); }); it('address prediction', async function () { diff --git a/test/proxy/beacon/BeaconProxy.test.js b/test/proxy/beacon/BeaconProxy.test.js index 68db10ddc..63d982397 100644 --- a/test/proxy/beacon/BeaconProxy.test.js +++ b/test/proxy/beacon/BeaconProxy.test.js @@ -1,6 +1,8 @@ const { expectRevert } = require('@openzeppelin/test-helpers'); const { getSlot, BeaconSlot } = require('../../helpers/erc1967'); +const { expectRevertCustomError } = require('../../helpers/customError'); + const { expect } = require('chai'); const UpgradeableBeacon = artifacts.require('UpgradeableBeacon'); @@ -15,7 +17,7 @@ contract('BeaconProxy', function (accounts) { describe('bad beacon is not accepted', async function () { it('non-contract beacon', async function () { - await expectRevert(BeaconProxy.new(anotherAccount, '0x'), 'ERC1967: new beacon is not a contract'); + await expectRevertCustomError(BeaconProxy.new(anotherAccount, '0x'), 'ERC1967InvalidBeacon', [anotherAccount]); }); it('non-compliant beacon', async function () { @@ -25,7 +27,10 @@ contract('BeaconProxy', function (accounts) { it('non-contract implementation', async function () { const beacon = await BadBeaconNotContract.new(); - await expectRevert(BeaconProxy.new(beacon.address, '0x'), 'ERC1967: beacon implementation is not a contract'); + const implementation = await beacon.implementation(); + await expectRevertCustomError(BeaconProxy.new(beacon.address, '0x'), 'ERC1967InvalidImplementation', [ + implementation, + ]); }); }); diff --git a/test/proxy/beacon/UpgradeableBeacon.test.js b/test/proxy/beacon/UpgradeableBeacon.test.js index c19b250b9..4c58f1740 100644 --- a/test/proxy/beacon/UpgradeableBeacon.test.js +++ b/test/proxy/beacon/UpgradeableBeacon.test.js @@ -1,6 +1,8 @@ -const { expectRevert, expectEvent } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../helpers/customError'); + const UpgradeableBeacon = artifacts.require('UpgradeableBeacon'); const Implementation1 = artifacts.require('Implementation1'); const Implementation2 = artifacts.require('Implementation2'); @@ -9,10 +11,7 @@ contract('UpgradeableBeacon', function (accounts) { const [owner, other] = accounts; it('cannot be created with non-contract implementation', async function () { - await expectRevert( - UpgradeableBeacon.new(accounts[0], owner), - 'UpgradeableBeacon: implementation is not a contract', - ); + await expectRevertCustomError(UpgradeableBeacon.new(other, owner), 'BeaconInvalidImplementation', [other]); }); context('once deployed', async function () { @@ -33,15 +32,16 @@ contract('UpgradeableBeacon', function (accounts) { }); it('cannot be upgraded to a non-contract', async function () { - await expectRevert( - this.beacon.upgradeTo(other, { from: owner }), - 'UpgradeableBeacon: implementation is not a contract', - ); + await expectRevertCustomError(this.beacon.upgradeTo(other, { from: owner }), 'BeaconInvalidImplementation', [ + other, + ]); }); it('cannot be upgraded by other account', async function () { const v2 = await Implementation2.new(); - await expectRevert(this.beacon.upgradeTo(v2.address, { from: other }), 'Ownable: caller is not the owner'); + await expectRevertCustomError(this.beacon.upgradeTo(v2.address, { from: other }), 'OwnableUnauthorizedAccount', [ + other, + ]); }); }); }); diff --git a/test/proxy/transparent/ProxyAdmin.test.js b/test/proxy/transparent/ProxyAdmin.test.js index 85b6695df..d660ffc56 100644 --- a/test/proxy/transparent/ProxyAdmin.test.js +++ b/test/proxy/transparent/ProxyAdmin.test.js @@ -1,5 +1,4 @@ const { expectRevert } = require('@openzeppelin/test-helpers'); -const { getAddressInSlot, ImplementationSlot, AdminSlot } = require('../../helpers/erc1967'); const { expect } = require('chai'); const ImplV1 = artifacts.require('DummyImplementation'); const ImplV2 = artifacts.require('DummyImplementationV2'); @@ -7,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], ); }); }); diff --git a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js index 4012cfffc..c6e949156 100644 --- a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js +++ b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js @@ -1,6 +1,7 @@ const { BN, expectRevert, expectEvent, constants } = require('@openzeppelin/test-helpers'); const { ZERO_ADDRESS } = constants; const { getAddressInSlot, ImplementationSlot, AdminSlot } = require('../../helpers/erc1967'); +const { expectRevertCustomError } = require('../../helpers/customError'); const { expect } = require('chai'); const { web3 } = require('hardhat'); @@ -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', + [], ); }); diff --git a/test/proxy/utils/Initializable.test.js b/test/proxy/utils/Initializable.test.js index 39c820b9d..e3e0fc02f 100644 --- a/test/proxy/utils/Initializable.test.js +++ b/test/proxy/utils/Initializable.test.js @@ -1,5 +1,6 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../helpers/customError'); const InitializableMock = artifacts.require('InitializableMock'); const ConstructorInitializableMock = artifacts.require('ConstructorInitializableMock'); @@ -40,13 +41,13 @@ contract('Initializable', function () { }); it('initializer does not run again', async function () { - await expectRevert(this.contract.initialize(), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.initialize(), 'AlreadyInitialized', []); }); }); describe('nested under an initializer', function () { it('initializer modifier reverts', async function () { - await expectRevert(this.contract.initializerNested(), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.initializerNested(), 'AlreadyInitialized', []); }); it('onlyInitializing modifier succeeds', async function () { @@ -56,7 +57,7 @@ contract('Initializable', function () { }); it('cannot call onlyInitializable function outside the scope of an initializable function', async function () { - await expectRevert(this.contract.initializeOnlyInitializing(), 'Initializable: contract is not initializing'); + await expectRevertCustomError(this.contract.initializeOnlyInitializing(), 'NotInitializing', []); }); }); @@ -98,9 +99,9 @@ contract('Initializable', function () { it('cannot nest reinitializers', async function () { expect(await this.contract.counter()).to.be.bignumber.equal('0'); - await expectRevert(this.contract.nestedReinitialize(2, 2), 'Initializable: contract is already initialized'); - await expectRevert(this.contract.nestedReinitialize(2, 3), 'Initializable: contract is already initialized'); - await expectRevert(this.contract.nestedReinitialize(3, 2), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.nestedReinitialize(2, 2), 'AlreadyInitialized', []); + await expectRevertCustomError(this.contract.nestedReinitialize(2, 3), 'AlreadyInitialized', []); + await expectRevertCustomError(this.contract.nestedReinitialize(3, 2), 'AlreadyInitialized', []); }); it('can chain reinitializers', async function () { @@ -119,18 +120,18 @@ contract('Initializable', function () { describe('contract locking', function () { it('prevents initialization', async function () { await this.contract.disableInitializers(); - await expectRevert(this.contract.initialize(), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.initialize(), 'AlreadyInitialized', []); }); it('prevents re-initialization', async function () { await this.contract.disableInitializers(); - await expectRevert(this.contract.reinitialize(255), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.reinitialize(255), 'AlreadyInitialized', []); }); it('can lock contract after initialization', async function () { await this.contract.initialize(); await this.contract.disableInitializers(); - await expectRevert(this.contract.reinitialize(255), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.reinitialize(255), 'AlreadyInitialized', []); }); }); }); @@ -205,8 +206,8 @@ contract('Initializable', function () { describe('disabling initialization', function () { it('old and new patterns in bad sequence', async function () { - await expectRevert(DisableBad1.new(), 'Initializable: contract is already initialized'); - await expectRevert(DisableBad2.new(), 'Initializable: contract is initializing'); + await expectRevertCustomError(DisableBad1.new(), 'AlreadyInitialized', []); + await expectRevertCustomError(DisableBad2.new(), 'AlreadyInitialized', []); }); it('old and new patterns in good sequence', async function () { diff --git a/test/proxy/utils/UUPSUpgradeable.test.js b/test/proxy/utils/UUPSUpgradeable.test.js index b0c1b3f6f..ea1b1d51f 100644 --- a/test/proxy/utils/UUPSUpgradeable.test.js +++ b/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 () { diff --git a/test/security/Pausable.test.js b/test/security/Pausable.test.js index 5cca11e47..e60a62c74 100644 --- a/test/security/Pausable.test.js +++ b/test/security/Pausable.test.js @@ -1,7 +1,8 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); - +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); + const PausableMock = artifacts.require('PausableMock'); contract('Pausable', function (accounts) { @@ -24,7 +25,7 @@ contract('Pausable', function (accounts) { }); it('cannot take drastic measure in non-pause', async function () { - await expectRevert(this.pausable.drasticMeasure(), 'Pausable: not paused'); + await expectRevertCustomError(this.pausable.drasticMeasure(), 'ExpectedPause', []); expect(await this.pausable.drasticMeasureTaken()).to.equal(false); }); @@ -38,7 +39,7 @@ contract('Pausable', function (accounts) { }); it('cannot perform normal process in pause', async function () { - await expectRevert(this.pausable.normalProcess(), 'Pausable: paused'); + await expectRevertCustomError(this.pausable.normalProcess(), 'EnforcedPause', []); }); it('can take a drastic measure in a pause', async function () { @@ -47,7 +48,7 @@ contract('Pausable', function (accounts) { }); it('reverts when re-pausing', async function () { - await expectRevert(this.pausable.pause(), 'Pausable: paused'); + await expectRevertCustomError(this.pausable.pause(), 'EnforcedPause', []); }); describe('unpausing', function () { @@ -72,11 +73,11 @@ contract('Pausable', function (accounts) { }); it('should prevent drastic measure', async function () { - await expectRevert(this.pausable.drasticMeasure(), 'Pausable: not paused'); + await expectRevertCustomError(this.pausable.drasticMeasure(), 'ExpectedPause', []); }); it('reverts when re-unpausing', async function () { - await expectRevert(this.pausable.unpause(), 'Pausable: not paused'); + await expectRevertCustomError(this.pausable.unpause(), 'ExpectedPause', []); }); }); }); diff --git a/test/security/ReentrancyGuard.test.js b/test/security/ReentrancyGuard.test.js index 1a80bc860..15355c098 100644 --- a/test/security/ReentrancyGuard.test.js +++ b/test/security/ReentrancyGuard.test.js @@ -1,7 +1,8 @@ const { expectRevert } = require('@openzeppelin/test-helpers'); - const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); + const ReentrancyMock = artifacts.require('ReentrancyMock'); const ReentrancyAttack = artifacts.require('ReentrancyAttack'); @@ -19,7 +20,7 @@ contract('ReentrancyGuard', function () { it('does not allow remote callback', async function () { const attacker = await ReentrancyAttack.new(); - await expectRevert(this.reentrancyMock.countAndCall(attacker.address), 'ReentrancyAttack: failed call'); + await expectRevert(this.reentrancyMock.countAndCall(attacker.address), 'ReentrancyAttack: failed call', []); }); it('_reentrancyGuardEntered should be true when guarded', async function () { @@ -34,10 +35,10 @@ contract('ReentrancyGuard', function () { // I put them here as documentation, and to monitor any changes // in the side-effects. it('does not allow local recursion', async function () { - await expectRevert(this.reentrancyMock.countLocalRecursive(10), 'ReentrancyGuard: reentrant call'); + await expectRevertCustomError(this.reentrancyMock.countLocalRecursive(10), 'ReentrancyGuardReentrantCall', []); }); it('does not allow indirect local recursion', async function () { - await expectRevert(this.reentrancyMock.countThisRecursive(10), 'ReentrancyMock: failed call'); + await expectRevert(this.reentrancyMock.countThisRecursive(10), 'ReentrancyMock: failed call', []); }); }); diff --git a/test/token/ERC1155/ERC1155.behavior.js b/test/token/ERC1155/ERC1155.behavior.js index c41c69c62..5e87f8d52 100644 --- a/test/token/ERC1155/ERC1155.behavior.js +++ b/test/token/ERC1155/ERC1155.behavior.js @@ -4,6 +4,7 @@ const { ZERO_ADDRESS } = constants; const { expect } = require('chai'); const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); +const { expectRevertCustomError } = require('../../helpers/customError'); const ERC1155ReceiverMock = artifacts.require('ERC1155ReceiverMock'); @@ -56,21 +57,19 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m describe('balanceOfBatch', function () { it("reverts when input arrays don't match up", async function () { - await expectRevert( - this.token.balanceOfBatch( - [firstTokenHolder, secondTokenHolder, firstTokenHolder, secondTokenHolder], - [firstTokenId, secondTokenId, unknownTokenId], - ), - 'ERC1155: accounts and ids length mismatch', - ); + const accounts1 = [firstTokenHolder, secondTokenHolder, firstTokenHolder, secondTokenHolder]; + const ids1 = [firstTokenId, secondTokenId, unknownTokenId]; + await expectRevertCustomError(this.token.balanceOfBatch(accounts1, ids1), 'ERC1155InvalidArrayLength', [ + accounts1.length, + ids1.length, + ]); - await expectRevert( - this.token.balanceOfBatch( - [firstTokenHolder, secondTokenHolder], - [firstTokenId, secondTokenId, unknownTokenId], - ), - 'ERC1155: accounts and ids length mismatch', - ); + const accounts2 = [firstTokenHolder, secondTokenHolder]; + const ids2 = [firstTokenId, secondTokenId, unknownTokenId]; + await expectRevertCustomError(this.token.balanceOfBatch(accounts2, ids2), 'ERC1155InvalidArrayLength', [ + accounts2.length, + ids2.length, + ]); }); it('should return 0 as the balance when one of the addresses is the zero address', async function () { @@ -152,9 +151,10 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts if attempting to approve self as an operator', async function () { - await expectRevert( + await expectRevertCustomError( this.token.setApprovalForAll(multiTokenHolder, true, { from: multiTokenHolder }), - 'ERC1155: setting approval status for self', + 'ERC1155InvalidOperator', + [multiTokenHolder], ); }); }); @@ -170,20 +170,22 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts when transferring more than balance', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(multiTokenHolder, recipient, firstTokenId, firstAmount.addn(1), '0x', { from: multiTokenHolder, }), - 'ERC1155: insufficient balance for transfer', + 'ERC1155InsufficientBalance', + [multiTokenHolder, firstAmount, firstAmount.addn(1), firstTokenId], ); }); it('reverts when transferring to zero address', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(multiTokenHolder, ZERO_ADDRESS, firstTokenId, firstAmount, '0x', { from: multiTokenHolder, }), - 'ERC1155: transfer to the zero address', + 'ERC1155InvalidReceiver', + [ZERO_ADDRESS], ); }); @@ -247,11 +249,12 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(multiTokenHolder, recipient, firstTokenId, firstAmount, '0x', { from: proxy, }), - 'ERC1155: caller is not token owner or approved', + 'ERC1155InsufficientApprovalForAll', + [proxy, multiTokenHolder], ); }); }); @@ -371,11 +374,12 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { from: multiTokenHolder, }), - 'ERC1155: ERC1155Receiver rejected tokens', + 'ERC1155InvalidReceiver', + [this.receiver.address], ); }); }); @@ -423,7 +427,7 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts when transferring amount more than any of balances', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom( multiTokenHolder, recipient, @@ -432,38 +436,36 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m '0x', { from: multiTokenHolder }, ), - 'ERC1155: insufficient balance for transfer', + 'ERC1155InsufficientBalance', + [multiTokenHolder, secondAmount, secondAmount.addn(1), secondTokenId], ); }); it("reverts when ids array length doesn't match amounts array length", async function () { - await expectRevert( - this.token.safeBatchTransferFrom( - multiTokenHolder, - recipient, - [firstTokenId], - [firstAmount, secondAmount], - '0x', - { from: multiTokenHolder }, - ), - 'ERC1155: ids and amounts length mismatch', + const ids1 = [firstTokenId]; + const amounts1 = [firstAmount, secondAmount]; + + await expectRevertCustomError( + this.token.safeBatchTransferFrom(multiTokenHolder, recipient, ids1, amounts1, '0x', { + from: multiTokenHolder, + }), + 'ERC1155InvalidArrayLength', + [ids1.length, amounts1.length], ); - await expectRevert( - this.token.safeBatchTransferFrom( - multiTokenHolder, - recipient, - [firstTokenId, secondTokenId], - [firstAmount], - '0x', - { from: multiTokenHolder }, - ), - 'ERC1155: ids and amounts length mismatch', + const ids2 = [firstTokenId, secondTokenId]; + const amounts2 = [firstAmount]; + await expectRevertCustomError( + this.token.safeBatchTransferFrom(multiTokenHolder, recipient, ids2, amounts2, '0x', { + from: multiTokenHolder, + }), + 'ERC1155InvalidArrayLength', + [ids2.length, amounts2.length], ); }); it('reverts when transferring to zero address', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom( multiTokenHolder, ZERO_ADDRESS, @@ -472,7 +474,8 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m '0x', { from: multiTokenHolder }, ), - 'ERC1155: transfer to the zero address', + 'ERC1155InvalidReceiver', + [ZERO_ADDRESS], ); }); @@ -530,7 +533,7 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom( multiTokenHolder, recipient, @@ -539,7 +542,8 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m '0x', { from: proxy }, ), - 'ERC1155: caller is not token owner or approved', + 'ERC1155InsufficientApprovalForAll', + [proxy, multiTokenHolder], ); }); }); @@ -661,7 +665,7 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom( multiTokenHolder, this.receiver.address, @@ -670,7 +674,8 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m '0x', { from: multiTokenHolder }, ), - 'ERC1155: ERC1155Receiver rejected tokens', + 'ERC1155InvalidReceiver', + [this.receiver.address], ); }); }); diff --git a/test/token/ERC1155/ERC1155.test.js b/test/token/ERC1155/ERC1155.test.js index 48197eeb5..23555dd54 100644 --- a/test/token/ERC1155/ERC1155.test.js +++ b/test/token/ERC1155/ERC1155.test.js @@ -1,8 +1,10 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); const { ZERO_ADDRESS } = constants; const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../helpers/customError'); + const { shouldBehaveLikeERC1155 } = require('./ERC1155.behavior'); const ERC1155Mock = artifacts.require('$ERC1155'); @@ -30,9 +32,10 @@ contract('ERC1155', function (accounts) { describe('_mint', function () { it('reverts with a zero destination address', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_mint(ZERO_ADDRESS, tokenId, mintAmount, data), - 'ERC1155: mint to the zero address', + 'ERC1155InvalidReceiver', + [ZERO_ADDRESS], ); }); @@ -59,21 +62,24 @@ contract('ERC1155', function (accounts) { describe('_mintBatch', function () { it('reverts with a zero destination address', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_mintBatch(ZERO_ADDRESS, tokenBatchIds, mintAmounts, data), - 'ERC1155: mint to the zero address', + 'ERC1155InvalidReceiver', + [ZERO_ADDRESS], ); }); it('reverts if length of inputs do not match', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_mintBatch(tokenBatchHolder, tokenBatchIds, mintAmounts.slice(1), data), - 'ERC1155: ids and amounts length mismatch', + 'ERC1155InvalidArrayLength', + [tokenBatchIds.length, mintAmounts.length - 1], ); - await expectRevert( + await expectRevertCustomError( this.token.$_mintBatch(tokenBatchHolder, tokenBatchIds.slice(1), mintAmounts, data), - 'ERC1155: ids and amounts length mismatch', + 'ERC1155InvalidArrayLength', + [tokenBatchIds.length - 1, mintAmounts.length], ); }); @@ -107,22 +113,26 @@ contract('ERC1155', function (accounts) { describe('_burn', function () { it("reverts when burning the zero account's tokens", async function () { - await expectRevert(this.token.$_burn(ZERO_ADDRESS, tokenId, mintAmount), 'ERC1155: burn from the zero address'); + await expectRevertCustomError(this.token.$_burn(ZERO_ADDRESS, tokenId, mintAmount), 'ERC1155InvalidSender', [ + ZERO_ADDRESS, + ]); }); it('reverts when burning a non-existent token id', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_burn(tokenHolder, tokenId, mintAmount), - 'ERC1155: insufficient balance for transfer', + 'ERC1155InsufficientBalance', + [tokenHolder, 0, mintAmount, tokenId], ); }); it('reverts when burning more than available tokens', async function () { await this.token.$_mint(tokenHolder, tokenId, mintAmount, data, { from: operator }); - await expectRevert( + await expectRevertCustomError( this.token.$_burn(tokenHolder, tokenId, mintAmount.addn(1)), - 'ERC1155: insufficient balance for transfer', + 'ERC1155InsufficientBalance', + [tokenHolder, mintAmount, mintAmount.addn(1), tokenId], ); }); @@ -150,28 +160,32 @@ contract('ERC1155', function (accounts) { describe('_burnBatch', function () { it("reverts when burning the zero account's tokens", async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_burnBatch(ZERO_ADDRESS, tokenBatchIds, burnAmounts), - 'ERC1155: burn from the zero address', + 'ERC1155InvalidSender', + [ZERO_ADDRESS], ); }); it('reverts if length of inputs do not match', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds, burnAmounts.slice(1)), - 'ERC1155: ids and amounts length mismatch', + 'ERC1155InvalidArrayLength', + [tokenBatchIds.length, burnAmounts.length - 1], ); - await expectRevert( + await expectRevertCustomError( this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds.slice(1), burnAmounts), - 'ERC1155: ids and amounts length mismatch', + 'ERC1155InvalidArrayLength', + [tokenBatchIds.length - 1, burnAmounts.length], ); }); it('reverts when burning a non-existent token id', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds, burnAmounts), - 'ERC1155: insufficient balance for transfer', + 'ERC1155InsufficientBalance', + [tokenBatchHolder, 0, tokenBatchIds[0], burnAmounts[0]], ); }); diff --git a/test/token/ERC1155/extensions/ERC1155Burnable.test.js b/test/token/ERC1155/extensions/ERC1155Burnable.test.js index f80d9935a..6af2308f8 100644 --- a/test/token/ERC1155/extensions/ERC1155Burnable.test.js +++ b/test/token/ERC1155/extensions/ERC1155Burnable.test.js @@ -1,7 +1,9 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); + const ERC1155Burnable = artifacts.require('$ERC1155Burnable'); contract('ERC1155Burnable', function (accounts) { @@ -34,9 +36,10 @@ contract('ERC1155Burnable', function (accounts) { }); it("unapproved accounts cannot burn the holder's tokens", async function () { - await expectRevert( + await expectRevertCustomError( this.token.burn(holder, tokenIds[0], amounts[0].subn(1), { from: other }), - 'ERC1155: caller is not token owner or approved', + 'ERC1155InsufficientApprovalForAll', + [other, holder], ); }); }); @@ -58,9 +61,10 @@ contract('ERC1155Burnable', function (accounts) { }); it("unapproved accounts cannot burn the holder's tokens", async function () { - await expectRevert( + await expectRevertCustomError( this.token.burnBatch(holder, tokenIds, [amounts[0].subn(1), amounts[1].subn(2)], { from: other }), - 'ERC1155: caller is not token owner or approved', + 'ERC1155InsufficientApprovalForAll', + [other, holder], ); }); }); diff --git a/test/token/ERC1155/extensions/ERC1155Pausable.test.js b/test/token/ERC1155/extensions/ERC1155Pausable.test.js index f4d5cedec..b0ac54bdb 100644 --- a/test/token/ERC1155/extensions/ERC1155Pausable.test.js +++ b/test/token/ERC1155/extensions/ERC1155Pausable.test.js @@ -1,6 +1,7 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC1155Pausable = artifacts.require('$ERC1155Pausable'); @@ -28,60 +29,64 @@ contract('ERC1155Pausable', function (accounts) { }); it('reverts when trying to safeTransferFrom from holder', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(holder, receiver, firstTokenId, firstTokenAmount, '0x', { from: holder }), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to safeTransferFrom from operator', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(holder, receiver, firstTokenId, firstTokenAmount, '0x', { from: operator }), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to safeBatchTransferFrom from holder', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom(holder, receiver, [firstTokenId], [firstTokenAmount], '0x', { from: holder }), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to safeBatchTransferFrom from operator', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom(holder, receiver, [firstTokenId], [firstTokenAmount], '0x', { from: operator, }), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to mint', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_mint(holder, secondTokenId, secondTokenAmount, '0x'), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to mintBatch', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_mintBatch(holder, [secondTokenId], [secondTokenAmount], '0x'), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to burn', async function () { - await expectRevert( - this.token.$_burn(holder, firstTokenId, firstTokenAmount), - 'ERC1155Pausable: token transfer while paused', - ); + await expectRevertCustomError(this.token.$_burn(holder, firstTokenId, firstTokenAmount), 'EnforcedPause', []); }); it('reverts when trying to burnBatch', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_burnBatch(holder, [firstTokenId], [firstTokenAmount]), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); diff --git a/test/token/ERC20/ERC20.behavior.js b/test/token/ERC20/ERC20.behavior.js index 41e47f065..7f547b112 100644 --- a/test/token/ERC20/ERC20.behavior.js +++ b/test/token/ERC20/ERC20.behavior.js @@ -1,8 +1,10 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { ZERO_ADDRESS, MAX_UINT256 } = constants; -function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipient, anotherAccount) { +const { expectRevertCustomError } = require('../../helpers/customError'); + +function shouldBehaveLikeERC20(initialSupply, initialHolder, recipient, anotherAccount) { describe('total supply', function () { it('returns the total amount of tokens', async function () { expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply); @@ -24,7 +26,7 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi }); describe('transfer', function () { - shouldBehaveLikeERC20Transfer(errorPrefix, initialHolder, recipient, initialSupply, function (from, to, value) { + shouldBehaveLikeERC20Transfer(initialHolder, recipient, initialSupply, function (from, to, value) { return this.token.transfer(to, value, { from }); }); }); @@ -85,9 +87,10 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(tokenOwner, to, amount, { from: spender }), - `${errorPrefix}: transfer amount exceeds balance`, + 'ERC20InsufficientBalance', + [tokenOwner, amount - 1, amount], ); }); }); @@ -104,9 +107,10 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi const amount = initialSupply; it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(tokenOwner, to, amount, { from: spender }), - `${errorPrefix}: insufficient allowance`, + 'ERC20InsufficientAllowance', + [spender, allowance, amount], ); }); }); @@ -119,9 +123,10 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(tokenOwner, to, amount, { from: spender }), - `${errorPrefix}: transfer amount exceeds balance`, + 'ERC20InsufficientBalance', + [tokenOwner, amount - 1, amount], ); }); }); @@ -153,9 +158,10 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(tokenOwner, to, amount, { from: spender }), - `${errorPrefix}: transfer to the zero address`, + 'ERC20InvalidReceiver', + [ZERO_ADDRESS], ); }); }); @@ -167,31 +173,33 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi const to = recipient; it('reverts', async function () { - await expectRevert(this.token.transferFrom(tokenOwner, to, amount, { from: spender }), 'from the zero address'); + await expectRevertCustomError( + this.token.transferFrom(tokenOwner, to, amount, { from: spender }), + 'ERC20InvalidApprover', + [ZERO_ADDRESS], + ); }); }); }); describe('approve', function () { - shouldBehaveLikeERC20Approve( - errorPrefix, - initialHolder, - recipient, - initialSupply, - function (owner, spender, amount) { - return this.token.approve(spender, amount, { from: owner }); - }, - ); + shouldBehaveLikeERC20Approve(initialHolder, recipient, initialSupply, function (owner, spender, amount) { + return this.token.approve(spender, amount, { from: owner }); + }); }); } -function shouldBehaveLikeERC20Transfer(errorPrefix, from, to, balance, transfer) { +function shouldBehaveLikeERC20Transfer(from, to, balance, transfer) { describe('when the recipient is not the zero address', function () { describe('when the sender does not have enough balance', function () { const amount = balance.addn(1); it('reverts', async function () { - await expectRevert(transfer.call(this, from, to, amount), `${errorPrefix}: transfer amount exceeds balance`); + await expectRevertCustomError(transfer.call(this, from, to, amount), 'ERC20InsufficientBalance', [ + from, + balance, + amount, + ]); }); }); @@ -230,15 +238,14 @@ function shouldBehaveLikeERC20Transfer(errorPrefix, from, to, balance, transfer) describe('when the recipient is the zero address', function () { it('reverts', async function () { - await expectRevert( - transfer.call(this, from, ZERO_ADDRESS, balance), - `${errorPrefix}: transfer to the zero address`, - ); + await expectRevertCustomError(transfer.call(this, from, ZERO_ADDRESS, balance), 'ERC20InvalidReceiver', [ + ZERO_ADDRESS, + ]); }); }); } -function shouldBehaveLikeERC20Approve(errorPrefix, owner, spender, supply, approve) { +function shouldBehaveLikeERC20Approve(owner, spender, supply, approve) { describe('when the spender is not the zero address', function () { describe('when the sender has enough balance', function () { const amount = supply; @@ -307,10 +314,9 @@ function shouldBehaveLikeERC20Approve(errorPrefix, owner, spender, supply, appro describe('when the spender is the zero address', function () { it('reverts', async function () { - await expectRevert( - approve.call(this, owner, ZERO_ADDRESS, supply), - `${errorPrefix}: approve to the zero address`, - ); + await expectRevertCustomError(approve.call(this, owner, ZERO_ADDRESS, supply), `ERC20InvalidSpender`, [ + ZERO_ADDRESS, + ]); }); }); } diff --git a/test/token/ERC20/ERC20.test.js b/test/token/ERC20/ERC20.test.js index c29197578..7b97c56f1 100644 --- a/test/token/ERC20/ERC20.test.js +++ b/test/token/ERC20/ERC20.test.js @@ -7,6 +7,7 @@ const { shouldBehaveLikeERC20Transfer, shouldBehaveLikeERC20Approve, } = require('./ERC20.behavior'); +const { expectRevertCustomError } = require('../../helpers/customError'); const ERC20 = artifacts.require('$ERC20'); const ERC20Decimals = artifacts.require('$ERC20DecimalsMock'); @@ -45,7 +46,7 @@ contract('ERC20', function (accounts) { }); }); - shouldBehaveLikeERC20('ERC20', initialSupply, initialHolder, recipient, anotherAccount); + shouldBehaveLikeERC20(initialSupply, initialHolder, recipient, anotherAccount); describe('decrease allowance', function () { describe('when the spender is not the zero address', function () { @@ -54,9 +55,11 @@ contract('ERC20', function (accounts) { function shouldDecreaseApproval(amount) { describe('when there was no approved amount before', function () { it('reverts', async function () { - await expectRevert( + const allowance = await this.token.allowance(initialHolder, spender); + await expectRevertCustomError( this.token.decreaseAllowance(spender, amount, { from: initialHolder }), - 'ERC20: decreased allowance below zero', + 'ERC20FailedDecreaseAllowance', + [spender, allowance, amount], ); }); }); @@ -88,9 +91,10 @@ contract('ERC20', function (accounts) { }); it('reverts when more than the full allowance is removed', async function () { - await expectRevert( + await expectRevertCustomError( this.token.decreaseAllowance(spender, approvedAmount.addn(1), { from: initialHolder }), - 'ERC20: decreased allowance below zero', + 'ERC20FailedDecreaseAllowance', + [spender, approvedAmount, approvedAmount.addn(1)], ); }); }); @@ -114,9 +118,10 @@ contract('ERC20', function (accounts) { const spender = ZERO_ADDRESS; it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.decreaseAllowance(spender, amount, { from: initialHolder }), - 'ERC20: decreased allowance below zero', + 'ERC20FailedDecreaseAllowance', + [spender, 0, amount], ); }); }); @@ -195,9 +200,10 @@ contract('ERC20', function (accounts) { const spender = ZERO_ADDRESS; it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.increaseAllowance(spender, amount, { from: initialHolder }), - 'ERC20: approve to the zero address', + 'ERC20InvalidSpender', + [ZERO_ADDRESS], ); }); }); @@ -206,7 +212,7 @@ contract('ERC20', function (accounts) { describe('_mint', function () { const amount = new BN(50); it('rejects a null account', async function () { - await expectRevert(this.token.$_mint(ZERO_ADDRESS, amount), 'ERC20: mint to the zero address'); + await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, amount), 'ERC20InvalidReceiver', [ZERO_ADDRESS]); }); it('rejects overflow', async function () { @@ -241,14 +247,15 @@ contract('ERC20', function (accounts) { describe('_burn', function () { it('rejects a null account', async function () { - await expectRevert(this.token.$_burn(ZERO_ADDRESS, new BN(1)), 'ERC20: burn from the zero address'); + await expectRevertCustomError(this.token.$_burn(ZERO_ADDRESS, new BN(1)), 'ERC20InvalidSender', [ZERO_ADDRESS]); }); describe('for a non zero account', function () { it('rejects burning more than balance', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_burn(initialHolder, initialSupply.addn(1)), - 'ERC20: transfer amount exceeds balance', + 'ERC20InsufficientBalance', + [initialHolder, initialSupply, initialSupply.addn(1)], ); }); @@ -325,30 +332,32 @@ contract('ERC20', function (accounts) { }); describe('_transfer', function () { - shouldBehaveLikeERC20Transfer('ERC20', initialHolder, recipient, initialSupply, function (from, to, amount) { + shouldBehaveLikeERC20Transfer(initialHolder, recipient, initialSupply, function (from, to, amount) { return this.token.$_transfer(from, to, amount); }); describe('when the sender is the zero address', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_transfer(ZERO_ADDRESS, recipient, initialSupply), - 'ERC20: transfer from the zero address', + 'ERC20InvalidSender', + [ZERO_ADDRESS], ); }); }); }); describe('_approve', function () { - shouldBehaveLikeERC20Approve('ERC20', initialHolder, recipient, initialSupply, function (owner, spender, amount) { + shouldBehaveLikeERC20Approve(initialHolder, recipient, initialSupply, function (owner, spender, amount) { return this.token.$_approve(owner, spender, amount); }); describe('when the owner is the zero address', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_approve(ZERO_ADDRESS, recipient, initialSupply), - 'ERC20: approve from the zero address', + 'ERC20InvalidApprover', + [ZERO_ADDRESS], ); }); }); diff --git a/test/token/ERC20/extensions/ERC20Burnable.behavior.js b/test/token/ERC20/extensions/ERC20Burnable.behavior.js index 448dda4ab..848e54b79 100644 --- a/test/token/ERC20/extensions/ERC20Burnable.behavior.js +++ b/test/token/ERC20/extensions/ERC20Burnable.behavior.js @@ -1,7 +1,8 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); const { ZERO_ADDRESS } = constants; const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); function shouldBehaveLikeERC20Burnable(owner, initialBalance, [burner]) { describe('burn', function () { @@ -37,7 +38,11 @@ function shouldBehaveLikeERC20Burnable(owner, initialBalance, [burner]) { const amount = initialBalance.addn(1); it('reverts', async function () { - await expectRevert(this.token.burn(amount, { from: owner }), 'ERC20: transfer amount exceeds balance'); + await expectRevertCustomError(this.token.burn(amount, { from: owner }), 'ERC20InsufficientBalance', [ + owner, + initialBalance, + amount, + ]); }); }); }); @@ -83,9 +88,10 @@ function shouldBehaveLikeERC20Burnable(owner, initialBalance, [burner]) { it('reverts', async function () { await this.token.approve(burner, amount, { from: owner }); - await expectRevert( + await expectRevertCustomError( this.token.burnFrom(owner, amount, { from: burner }), - 'ERC20: transfer amount exceeds balance', + 'ERC20InsufficientBalance', + [owner, initialBalance, amount], ); }); }); @@ -95,9 +101,10 @@ function shouldBehaveLikeERC20Burnable(owner, initialBalance, [burner]) { it('reverts', async function () { await this.token.approve(burner, allowance, { from: owner }); - await expectRevert( + await expectRevertCustomError( this.token.burnFrom(owner, allowance.addn(1), { from: burner }), - 'ERC20: insufficient allowance', + 'ERC20InsufficientAllowance', + [burner, allowance, allowance.addn(1)], ); }); }); diff --git a/test/token/ERC20/extensions/ERC20Capped.behavior.js b/test/token/ERC20/extensions/ERC20Capped.behavior.js index 97bad1db1..c40e4fcc4 100644 --- a/test/token/ERC20/extensions/ERC20Capped.behavior.js +++ b/test/token/ERC20/extensions/ERC20Capped.behavior.js @@ -1,6 +1,5 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); - const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); function shouldBehaveLikeERC20Capped(accounts, cap) { describe('capped token', function () { @@ -17,12 +16,12 @@ function shouldBehaveLikeERC20Capped(accounts, cap) { it('fails to mint if the amount exceeds the cap', async function () { await this.token.$_mint(user, cap.subn(1)); - await expectRevert(this.token.$_mint(user, 2), 'ERC20Capped: cap exceeded'); + await expectRevertCustomError(this.token.$_mint(user, 2), 'ERC20ExceededCap', [cap.addn(1), cap]); }); it('fails to mint after cap is reached', async function () { await this.token.$_mint(user, cap); - await expectRevert(this.token.$_mint(user, 1), 'ERC20Capped: cap exceeded'); + await expectRevertCustomError(this.token.$_mint(user, 1), 'ERC20ExceededCap', [cap.addn(1), cap]); }); }); } diff --git a/test/token/ERC20/extensions/ERC20Capped.test.js b/test/token/ERC20/extensions/ERC20Capped.test.js index a86d38c1a..1f4a2bee3 100644 --- a/test/token/ERC20/extensions/ERC20Capped.test.js +++ b/test/token/ERC20/extensions/ERC20Capped.test.js @@ -1,5 +1,6 @@ -const { ether, expectRevert } = require('@openzeppelin/test-helpers'); +const { ether } = require('@openzeppelin/test-helpers'); const { shouldBehaveLikeERC20Capped } = require('./ERC20Capped.behavior'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC20Capped = artifacts.require('$ERC20Capped'); @@ -10,7 +11,7 @@ contract('ERC20Capped', function (accounts) { const symbol = 'MTKN'; it('requires a non-zero cap', async function () { - await expectRevert(ERC20Capped.new(name, symbol, 0), 'ERC20Capped: cap is 0'); + await expectRevertCustomError(ERC20Capped.new(name, symbol, 0), 'ERC20InvalidCap', [0]); }); context('once deployed', async function () { diff --git a/test/token/ERC20/extensions/ERC20FlashMint.test.js b/test/token/ERC20/extensions/ERC20FlashMint.test.js index ee9bedd26..a646704e2 100644 --- a/test/token/ERC20/extensions/ERC20FlashMint.test.js +++ b/test/token/ERC20/extensions/ERC20FlashMint.test.js @@ -2,6 +2,7 @@ const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const { MAX_UINT256, ZERO_ADDRESS } = constants; const ERC20FlashMintMock = artifacts.require('$ERC20FlashMintMock'); @@ -37,7 +38,9 @@ contract('ERC20FlashMint', function (accounts) { }); it('token mismatch', async function () { - await expectRevert(this.token.flashFee(ZERO_ADDRESS, loanAmount), 'ERC20FlashMint: wrong token'); + await expectRevertCustomError(this.token.flashFee(ZERO_ADDRESS, loanAmount), 'ERC3156UnsupportedToken', [ + ZERO_ADDRESS, + ]); }); }); @@ -79,26 +82,29 @@ contract('ERC20FlashMint', function (accounts) { it('missing return value', async function () { const receiver = await ERC3156FlashBorrowerMock.new(false, true); - await expectRevert( + await expectRevertCustomError( this.token.flashLoan(receiver.address, this.token.address, loanAmount, '0x'), - 'ERC20FlashMint: invalid return value', + 'ERC3156InvalidReceiver', + [receiver.address], ); }); it('missing approval', async function () { const receiver = await ERC3156FlashBorrowerMock.new(true, false); - await expectRevert( + await expectRevertCustomError( this.token.flashLoan(receiver.address, this.token.address, loanAmount, '0x'), - 'ERC20: insufficient allowance', + 'ERC20InsufficientAllowance', + [this.token.address, 0, loanAmount], ); }); it('unavailable funds', async function () { const receiver = await ERC3156FlashBorrowerMock.new(true, true); const data = this.token.contract.methods.transfer(other, 10).encodeABI(); - await expectRevert( + await expectRevertCustomError( this.token.flashLoan(receiver.address, this.token.address, loanAmount, data), - 'ERC20: transfer amount exceeds balance', + 'ERC20InsufficientBalance', + [receiver.address, loanAmount - 10, loanAmount], ); }); diff --git a/test/token/ERC20/extensions/ERC20Pausable.test.js b/test/token/ERC20/extensions/ERC20Pausable.test.js index ead442b99..72bfc7769 100644 --- a/test/token/ERC20/extensions/ERC20Pausable.test.js +++ b/test/token/ERC20/extensions/ERC20Pausable.test.js @@ -1,6 +1,7 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC20Pausable = artifacts.require('$ERC20Pausable'); @@ -39,9 +40,10 @@ contract('ERC20Pausable', function (accounts) { it('reverts when trying to transfer when paused', async function () { await this.token.$_pause(); - await expectRevert( + await expectRevertCustomError( this.token.transfer(recipient, initialSupply, { from: holder }), - 'ERC20Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); }); @@ -73,9 +75,10 @@ contract('ERC20Pausable', function (accounts) { it('reverts when trying to transfer from when paused', async function () { await this.token.$_pause(); - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(holder, recipient, allowance, { from: anotherAccount }), - 'ERC20Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); }); @@ -101,7 +104,7 @@ contract('ERC20Pausable', function (accounts) { it('reverts when trying to mint when paused', async function () { await this.token.$_pause(); - await expectRevert(this.token.$_mint(recipient, amount), 'ERC20Pausable: token transfer while paused'); + await expectRevertCustomError(this.token.$_mint(recipient, amount), 'EnforcedPause', []); }); }); @@ -126,7 +129,7 @@ contract('ERC20Pausable', function (accounts) { it('reverts when trying to burn when paused', async function () { await this.token.$_pause(); - await expectRevert(this.token.$_burn(holder, amount), 'ERC20Pausable: token transfer while paused'); + await expectRevertCustomError(this.token.$_burn(holder, amount), 'EnforcedPause', []); }); }); }); diff --git a/test/token/ERC20/extensions/draft-ERC20Permit.test.js b/test/token/ERC20/extensions/ERC20Permit.test.js similarity index 73% rename from test/token/ERC20/extensions/draft-ERC20Permit.test.js rename to test/token/ERC20/extensions/ERC20Permit.test.js index 33c43c479..388716d53 100644 --- a/test/token/ERC20/extensions/draft-ERC20Permit.test.js +++ b/test/token/ERC20/extensions/ERC20Permit.test.js @@ -1,6 +1,6 @@ /* eslint-disable */ -const { BN, constants, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { BN, constants, time } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { MAX_UINT256 } = constants; @@ -12,13 +12,13 @@ const ERC20Permit = artifacts.require('$ERC20Permit'); const { Permit, getDomain, domainType, domainSeparator } = require('../../../helpers/eip712'); const { getChainId } = require('../../../helpers/chainid'); +const { expectRevertCustomError } = require('../../../helpers/customError'); contract('ERC20Permit', function (accounts) { const [initialHolder, spender] = accounts; const name = 'My Token'; const symbol = 'MTKN'; - const version = '1'; const initialSupply = new BN(100); @@ -65,15 +65,25 @@ contract('ERC20Permit', function (accounts) { }); it('rejects reused signature', async function () { - const { v, r, s } = await buildData(this.token) - .then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data })) - .then(fromRpcSig); + const sig = await buildData(this.token).then(data => + ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }), + ); + const { r, s, v } = fromRpcSig(sig); await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - await expectRevert( + const domain = await getDomain(this.token); + const typedMessage = { + primaryType: 'Permit', + types: { EIP712Domain: domainType(domain), Permit }, + domain, + message: { owner, spender, value, nonce: nonce + 1, deadline: maxDeadline }, + }; + + await expectRevertCustomError( this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC20Permit: invalid signature', + 'ERC2612InvalidSigner', + [ethSigUtil.recoverTypedSignature({ data: typedMessage, sig }), owner], ); }); @@ -84,9 +94,10 @@ contract('ERC20Permit', function (accounts) { .then(data => ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data })) .then(fromRpcSig); - await expectRevert( + await expectRevertCustomError( this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC20Permit: invalid signature', + 'ERC2612InvalidSigner', + [await otherWallet.getAddressString(), owner], ); }); @@ -97,7 +108,11 @@ contract('ERC20Permit', function (accounts) { .then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data })) .then(fromRpcSig); - await expectRevert(this.token.permit(owner, spender, value, deadline, v, r, s), 'ERC20Permit: expired deadline'); + await expectRevertCustomError( + this.token.permit(owner, spender, value, deadline, v, r, s), + 'ERC2612ExpiredSignature', + [deadline], + ); }); }); }); diff --git a/test/token/ERC20/extensions/ERC20Votes.test.js b/test/token/ERC20/extensions/ERC20Votes.test.js index e4ff58cd9..714a98adc 100644 --- a/test/token/ERC20/extensions/ERC20Votes.test.js +++ b/test/token/ERC20/extensions/ERC20Votes.test.js @@ -1,6 +1,6 @@ /* eslint-disable */ -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent, time } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { MAX_UINT256, ZERO_ADDRESS } = constants; @@ -12,6 +12,7 @@ const Wallet = require('ethereumjs-wallet').default; const { batchInBlock } = require('../../../helpers/txpool'); const { getDomain, domainType, domainSeparator } = require('../../../helpers/eip712'); const { clock, clockFromReceipt } = require('../../../helpers/time'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -48,7 +49,10 @@ contract('ERC20Votes', function (accounts) { it('minting restriction', async function () { const amount = new BN('2').pow(new BN('224')); - await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes'); + await expectRevertCustomError(this.token.$_mint(holder, amount), 'ERC20ExceededSafeSupply', [ + amount, + amount.subn(1), + ]); }); it('recent checkpoints', async function () { @@ -164,9 +168,10 @@ contract('ERC20Votes', function (accounts) { await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - await expectRevert( + await expectRevertCustomError( this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), - 'Votes: invalid nonce', + 'InvalidAccountNonce', + [delegatorAddress, nonce + 1], ); }); @@ -185,15 +190,25 @@ contract('ERC20Votes', function (accounts) { }); it('rejects bad nonce', async function () { - const { v, r, s } = await buildData(this.token, { + const sig = await buildData(this.token, { delegatee: delegatorAddress, nonce, expiry: MAX_UINT256, - }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); + }).then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })); + const { r, s, v } = fromRpcSig(sig); - await expectRevert( + const domain = await getDomain(this.token); + const typedMessage = { + primaryType: 'Delegation', + types: { EIP712Domain: domainType(domain), Delegation }, + domain, + message: { delegatee: delegatorAddress, nonce: nonce + 1, expiry: MAX_UINT256 }, + }; + + await expectRevertCustomError( this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), - 'Votes: invalid nonce', + 'InvalidAccountNonce', + [ethSigUtil.recoverTypedSignature({ data: typedMessage, sig }), nonce], ); }); @@ -205,9 +220,10 @@ contract('ERC20Votes', function (accounts) { expiry, }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - await expectRevert( + await expectRevertCustomError( this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), - 'Votes: signature expired', + 'VotesExpiredSignature', + [expiry], ); }); }); @@ -414,7 +430,8 @@ contract('ERC20Votes', function (accounts) { describe('getPastVotes', function () { it('reverts if block number >= current block', async function () { - await expectRevert(this.token.getPastVotes(other1, 5e10), 'Votes: future lookup'); + const clock = await this.token.clock(); + await expectRevertCustomError(this.token.getPastVotes(other1, 5e10), 'ERC5805FutureLookup', [5e10, clock]); }); it('returns 0 if there are no checkpoints', async function () { @@ -502,7 +519,8 @@ contract('ERC20Votes', function (accounts) { }); it('reverts if block number >= current block', async function () { - await expectRevert(this.token.getPastTotalSupply(5e10), 'Votes: future lookup'); + const clock = await this.token.clock(); + await expectRevertCustomError(this.token.getPastTotalSupply(5e10), 'ERC5805FutureLookup', [5e10, clock]); }); it('returns 0 if there are no checkpoints', async function () { diff --git a/test/token/ERC20/extensions/ERC20Wrapper.test.js b/test/token/ERC20/extensions/ERC20Wrapper.test.js index 774a9cbda..ffb97e8a2 100644 --- a/test/token/ERC20/extensions/ERC20Wrapper.test.js +++ b/test/token/ERC20/extensions/ERC20Wrapper.test.js @@ -1,14 +1,15 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { ZERO_ADDRESS, MAX_UINT256 } = constants; const { shouldBehaveLikeERC20 } = require('../ERC20.behavior'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const NotAnERC20 = artifacts.require('CallReceiverMock'); const ERC20Decimals = artifacts.require('$ERC20DecimalsMock'); const ERC20Wrapper = artifacts.require('$ERC20Wrapper'); -contract('ERC20', function (accounts) { +contract('ERC20Wrapper', function (accounts) { const [initialHolder, recipient, anotherAccount] = accounts; const name = 'My Token'; @@ -66,17 +67,19 @@ contract('ERC20', function (accounts) { }); it('missing approval', async function () { - await expectRevert( + await expectRevertCustomError( this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }), - 'ERC20: insufficient allowance', + 'ERC20InsufficientAllowance', + [this.token.address, 0, initialSupply], ); }); it('missing balance', async function () { await this.underlying.approve(this.token.address, MAX_UINT256, { from: initialHolder }); - await expectRevert( + await expectRevertCustomError( this.token.depositFor(initialHolder, MAX_UINT256, { from: initialHolder }), - 'ERC20: transfer amount exceeds balance', + 'ERC20InsufficientBalance', + [initialHolder, initialSupply, MAX_UINT256], ); }); @@ -103,9 +106,10 @@ contract('ERC20', function (accounts) { }); it('missing balance', async function () { - await expectRevert( + await expectRevertCustomError( this.token.withdrawTo(initialHolder, MAX_UINT256, { from: initialHolder }), - 'ERC20: transfer amount exceeds balance', + 'ERC20InsufficientBalance', + [initialHolder, initialSupply, MAX_UINT256], ); }); @@ -185,6 +189,6 @@ contract('ERC20', function (accounts) { await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }); }); - shouldBehaveLikeERC20('ERC20', initialSupply, initialHolder, recipient, anotherAccount); + shouldBehaveLikeERC20(initialSupply, initialHolder, recipient, anotherAccount); }); }); diff --git a/test/token/ERC20/extensions/ERC4626.test.js b/test/token/ERC20/extensions/ERC4626.test.js index 55b3e5d20..d67486a60 100644 --- a/test/token/ERC20/extensions/ERC4626.test.js +++ b/test/token/ERC20/extensions/ERC4626.test.js @@ -2,6 +2,7 @@ const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-hel const { expect } = require('chai'); const { Enum } = require('../../../helpers/enums'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC20Decimals = artifacts.require('$ERC20DecimalsMock'); const ERC4626 = artifacts.require('$ERC4626'); @@ -635,9 +636,11 @@ contract('ERC4626', function (accounts) { }); it('withdraw with approval', async function () { - await expectRevert( + const assets = await this.vault.previewWithdraw(parseToken(1)); + await expectRevertCustomError( this.vault.withdraw(parseToken(1), recipient, holder, { from: other }), - 'ERC20: insufficient allowance', + 'ERC20InsufficientAllowance', + [other, 0, assets], ); await this.vault.withdraw(parseToken(1), recipient, holder, { from: spender }); @@ -677,9 +680,10 @@ contract('ERC4626', function (accounts) { }); it('redeem with approval', async function () { - await expectRevert( + await expectRevertCustomError( this.vault.redeem(parseShare(100), recipient, holder, { from: other }), - 'ERC20: insufficient allowance', + 'ERC20InsufficientAllowance', + [other, 0, parseShare(100)], ); await this.vault.redeem(parseShare(100), recipient, holder, { from: spender }); diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index b0daf4384..eb6e26755 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -8,6 +8,7 @@ const ERC20PermitNoRevertMock = artifacts.require('$ERC20PermitNoRevertMock'); const ERC20ForceApproveMock = artifacts.require('$ERC20ForceApproveMock'); const { getDomain, domainType, Permit } = require('../../../helpers/eip712'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const { fromRpcSig } = require('ethereumjs-util'); const ethSigUtil = require('eth-sig-util'); @@ -17,7 +18,7 @@ const name = 'ERC20Mock'; const symbol = 'ERC20Mock'; contract('SafeERC20', function (accounts) { - const [hasNoCode] = accounts; + const [hasNoCode, receiver, spender] = accounts; before(async function () { this.mock = await SafeERC20.new(); @@ -28,7 +29,35 @@ contract('SafeERC20', function (accounts) { this.token = { address: hasNoCode }; }); - shouldRevertOnAllCalls(accounts, 'Address: call to non-contract'); + it('reverts on transfer', async function () { + await expectRevertCustomError(this.mock.$safeTransfer(this.token.address, receiver, 0), 'AddressEmptyCode', [ + this.token.address, + ]); + }); + + it('reverts on transferFrom', async function () { + await expectRevertCustomError( + this.mock.$safeTransferFrom(this.token.address, this.mock.address, receiver, 0), + 'AddressEmptyCode', + [this.token.address], + ); + }); + + it('reverts on increaseAllowance', async function () { + // Call to 'token.allowance' does not return any data, resulting in a decoding error (revert without reason) + await expectRevert.unspecified(this.mock.$safeIncreaseAllowance(this.token.address, spender, 0)); + }); + + it('reverts on decreaseAllowance', async function () { + // Call to 'token.allowance' does not return any data, resulting in a decoding error (revert without reason) + await expectRevert.unspecified(this.mock.$safeDecreaseAllowance(this.token.address, spender, 0)); + }); + + it('reverts on forceApprove', async function () { + await expectRevertCustomError(this.mock.$forceApprove(this.token.address, spender, 0), 'AddressEmptyCode', [ + this.token.address, + ]); + }); }); describe('with token that returns false on all calls', function () { @@ -36,7 +65,45 @@ contract('SafeERC20', function (accounts) { this.token = await ERC20ReturnFalseMock.new(name, symbol); }); - shouldRevertOnAllCalls(accounts, 'SafeERC20: ERC20 operation did not succeed'); + it('reverts on transfer', async function () { + await expectRevertCustomError( + this.mock.$safeTransfer(this.token.address, receiver, 0), + 'SafeERC20FailedOperation', + [this.token.address], + ); + }); + + it('reverts on transferFrom', async function () { + await expectRevertCustomError( + this.mock.$safeTransferFrom(this.token.address, this.mock.address, receiver, 0), + 'SafeERC20FailedOperation', + [this.token.address], + ); + }); + + it('reverts on increaseAllowance', async function () { + await expectRevertCustomError( + this.mock.$safeIncreaseAllowance(this.token.address, spender, 0), + 'SafeERC20FailedOperation', + [this.token.address], + ); + }); + + it('reverts on decreaseAllowance', async function () { + await expectRevertCustomError( + this.mock.$safeDecreaseAllowance(this.token.address, spender, 0), + 'SafeERC20FailedOperation', + [this.token.address], + ); + }); + + it('reverts on forceApprove', async function () { + await expectRevertCustomError( + this.mock.$forceApprove(this.token.address, spender, 0), + 'SafeERC20FailedOperation', + [this.token.address], + ); + }); }); describe('with token that returns true on all calls', function () { @@ -118,7 +185,7 @@ contract('SafeERC20', function (accounts) { ); expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); // invalid call revert when called through the SafeERC20 library - await expectRevert( + await expectRevertCustomError( this.mock.$safePermit( this.token.address, this.data.message.owner, @@ -129,7 +196,8 @@ contract('SafeERC20', function (accounts) { this.signature.r, this.signature.s, ), - 'SafeERC20: permit did not succeed', + 'SafeERC20FailedOperation', + [this.token.address], ); expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); }); @@ -154,7 +222,7 @@ contract('SafeERC20', function (accounts) { ); // invalid call revert when called through the SafeERC20 library - await expectRevert( + await expectRevertCustomError( this.mock.$safePermit( this.token.address, this.data.message.owner, @@ -165,7 +233,8 @@ contract('SafeERC20', function (accounts) { invalidSignature.r, invalidSignature.s, ), - 'SafeERC20: permit did not succeed', + 'SafeERC20FailedOperation', + [this.token.address], ); }); }); @@ -200,30 +269,6 @@ contract('SafeERC20', function (accounts) { }); }); -function shouldRevertOnAllCalls([receiver, spender], reason) { - it('reverts on transfer', async function () { - await expectRevert(this.mock.$safeTransfer(this.token.address, receiver, 0), reason); - }); - - it('reverts on transferFrom', async function () { - await expectRevert(this.mock.$safeTransferFrom(this.token.address, this.mock.address, receiver, 0), reason); - }); - - it('reverts on increaseAllowance', async function () { - // [TODO] make sure it's reverting for the right reason - await expectRevert.unspecified(this.mock.$safeIncreaseAllowance(this.token.address, spender, 0)); - }); - - it('reverts on decreaseAllowance', async function () { - // [TODO] make sure it's reverting for the right reason - await expectRevert.unspecified(this.mock.$safeDecreaseAllowance(this.token.address, spender, 0)); - }); - - it('reverts on forceApprove', async function () { - await expectRevert(this.mock.$forceApprove(this.token.address, spender, 0), reason); - }); -} - function shouldOnlyRevertOnErrors([owner, receiver, spender]) { describe('transfers', function () { beforeEach(async function () { @@ -273,9 +318,10 @@ function shouldOnlyRevertOnErrors([owner, receiver, spender]) { }); it('reverts when decreasing the allowance', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.$safeDecreaseAllowance(this.token.address, spender, 10), - 'SafeERC20: decreased allowance below zero', + 'SafeERC20FailedDecreaseAllowance', + [spender, 0, 10], ); }); }); @@ -306,9 +352,10 @@ function shouldOnlyRevertOnErrors([owner, receiver, spender]) { }); it('reverts when decreasing the allowance to a negative value', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.$safeDecreaseAllowance(this.token.address, spender, 200), - 'SafeERC20: decreased allowance below zero', + 'SafeERC20FailedDecreaseAllowance', + [spender, 100, 200], ); }); }); diff --git a/test/token/ERC721/ERC721.behavior.js b/test/token/ERC721/ERC721.behavior.js index 6867db31f..7df429202 100644 --- a/test/token/ERC721/ERC721.behavior.js +++ b/test/token/ERC721/ERC721.behavior.js @@ -3,6 +3,7 @@ const { expect } = require('chai'); const { ZERO_ADDRESS } = constants; const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); +const { expectRevertCustomError } = require('../../helpers/customError'); const ERC721ReceiverMock = artifacts.require('ERC721ReceiverMock'); const NonERC721ReceiverMock = artifacts.require('CallReceiverMock'); @@ -20,7 +21,7 @@ const baseURI = 'https://api.example.com/v1/'; const RECEIVER_MAGIC_VALUE = '0x150b7a02'; -function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherApproved, operator, other) { +function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, operator, other) { shouldSupportInterfaces(['ERC165', 'ERC721']); context('with minted tokens', function () { @@ -45,7 +46,7 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when querying the zero address', function () { it('throws', async function () { - await expectRevert(this.token.balanceOf(ZERO_ADDRESS), 'ERC721: address zero is not a valid owner'); + await expectRevertCustomError(this.token.balanceOf(ZERO_ADDRESS), 'ERC721InvalidOwner', [ZERO_ADDRESS]); }); }); }); @@ -63,7 +64,7 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA const tokenId = nonExistentTokenId; it('reverts', async function () { - await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); }); }); }); @@ -172,36 +173,40 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when the address of the previous owner is incorrect', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( transferFunction.call(this, other, other, tokenId, { from: owner }), - 'ERC721: transfer from incorrect owner', + 'ERC721IncorrectOwner', + [other, tokenId, owner], ); }); }); context('when the sender is not authorized for the token id', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( transferFunction.call(this, owner, other, tokenId, { from: other }), - 'ERC721: caller is not token owner or approved', + 'ERC721InsufficientApproval', + [other, tokenId], ); }); }); context('when the given token ID does not exist', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( transferFunction.call(this, owner, other, nonExistentTokenId, { from: owner }), - 'ERC721: invalid token ID', + 'ERC721NonexistentToken', + [nonExistentTokenId], ); }); }); context('when the address to transfer the token to is the zero address', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( transferFunction.call(this, owner, ZERO_ADDRESS, tokenId, { from: owner }), - 'ERC721: transfer to the zero address', + 'ERC721InvalidReceiver', + [ZERO_ADDRESS], ); }); }); @@ -259,9 +264,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('with an invalid token id', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( transferFun.call(this, owner, this.receiver.address, nonExistentTokenId, { from: owner }), - 'ERC721: invalid token ID', + 'ERC721NonexistentToken', + [nonExistentTokenId], ); }); }); @@ -279,9 +285,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('to a receiver contract returning unexpected value', function () { it('reverts', async function () { const invalidReceiver = await ERC721ReceiverMock.new('0x42', Error.None); - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(owner, invalidReceiver.address, tokenId, { from: owner }), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [invalidReceiver.address], ); }); }); @@ -299,9 +306,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('to a receiver contract that reverts without message', function () { it('reverts', async function () { const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithoutMessage); - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [revertingReceiver.address], ); }); }); @@ -318,9 +326,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('to a contract that does not implement the required function', function () { it('reverts', async function () { const nonReceiver = await NonERC721ReceiverMock.new(); - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(owner, nonReceiver.address, tokenId, { from: owner }), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [nonReceiver.address], ); }); }); @@ -357,9 +366,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('to a receiver contract returning unexpected value', function () { it('reverts', async function () { const invalidReceiver = await ERC721ReceiverMock.new('0x42', Error.None); - await expectRevert( + await expectRevertCustomError( this.token.$_safeMint(invalidReceiver.address, tokenId), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [invalidReceiver.address], ); }); }); @@ -377,9 +387,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('to a receiver contract that reverts without message', function () { it('reverts', async function () { const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithoutMessage); - await expectRevert( + await expectRevertCustomError( this.token.$_safeMint(revertingReceiver.address, tokenId), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [revertingReceiver.address], ); }); }); @@ -394,9 +405,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('to a contract that does not implement the required function', function () { it('reverts', async function () { const nonReceiver = await NonERC721ReceiverMock.new(); - await expectRevert( + await expectRevertCustomError( this.token.$_safeMint(nonReceiver.address, tokenId), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [nonReceiver.address], ); }); }); @@ -484,15 +496,18 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when the address that receives the approval is the owner', function () { it('reverts', async function () { - await expectRevert(this.token.approve(owner, tokenId, { from: owner }), 'ERC721: approval to current owner'); + await expectRevertCustomError(this.token.approve(owner, tokenId, { from: owner }), 'ERC721InvalidOperator', [ + owner, + ]); }); }); context('when the sender does not own the given token ID', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.approve(approved, tokenId, { from: other }), - 'ERC721: approve caller is not token owner or approved', + 'ERC721InvalidApprover', + [other], ); }); }); @@ -500,9 +515,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when the sender is approved for the given token ID', function () { it('reverts', async function () { await this.token.approve(approved, tokenId, { from: owner }); - await expectRevert( + await expectRevertCustomError( this.token.approve(anotherApproved, tokenId, { from: approved }), - 'ERC721: approve caller is not token owner or approved for all', + 'ERC721InvalidApprover', + [approved], ); }); }); @@ -519,9 +535,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when the given token ID does not exist', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.approve(approved, nonExistentTokenId, { from: operator }), - 'ERC721: invalid token ID', + 'ERC721NonexistentToken', + [nonExistentTokenId], ); }); }); @@ -600,7 +617,11 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when the operator is the owner', function () { it('reverts', async function () { - await expectRevert(this.token.setApprovalForAll(owner, true, { from: owner }), 'ERC721: approve to caller'); + await expectRevertCustomError( + this.token.setApprovalForAll(owner, true, { from: owner }), + 'ERC721InvalidOperator', + [owner], + ); }); }); }); @@ -608,7 +629,9 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('getApproved', async function () { context('when token is not minted', async function () { it('reverts', async function () { - await expectRevert(this.token.getApproved(nonExistentTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.getApproved(nonExistentTokenId), 'ERC721NonexistentToken', [ + nonExistentTokenId, + ]); }); }); @@ -632,7 +655,9 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('_mint(address, uint256)', function () { it('reverts with a null destination address', async function () { - await expectRevert(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721: mint to the zero address'); + await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721InvalidReceiver', [ + ZERO_ADDRESS, + ]); }); context('with minted token', async function () { @@ -650,14 +675,16 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA }); it('reverts when adding a token id that already exists', async function () { - await expectRevert(this.token.$_mint(owner, firstTokenId), 'ERC721: token already minted'); + await expectRevertCustomError(this.token.$_mint(owner, firstTokenId), 'ERC721InvalidSender', [ZERO_ADDRESS]); }); }); }); describe('_burn', function () { it('reverts when burning a non-existent token id', async function () { - await expectRevert(this.token.$_burn(nonExistentTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.$_burn(nonExistentTokenId), 'ERC721NonexistentToken', [ + nonExistentTokenId, + ]); }); context('with minted tokens', function () { @@ -677,18 +704,18 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA it('deletes the token', async function () { expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1'); - await expectRevert(this.token.ownerOf(firstTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.ownerOf(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); }); it('reverts when burning a token id that has been deleted', async function () { - await expectRevert(this.token.$_burn(firstTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.$_burn(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); }); }); }); }); } -function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved, anotherApproved, operator, other) { +function shouldBehaveLikeERC721Enumerable(owner, newOwner, approved, anotherApproved, operator, other) { shouldSupportInterfaces(['ERC721Enumerable']); context('with minted tokens', function () { @@ -713,13 +740,13 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved describe('when the index is greater than or equal to the total tokens owned by the given address', function () { it('reverts', async function () { - await expectRevert(this.token.tokenOfOwnerByIndex(owner, 2), 'ERC721Enumerable: owner index out of bounds'); + await expectRevertCustomError(this.token.tokenOfOwnerByIndex(owner, 2), 'ERC721OutOfBoundsIndex', [owner, 2]); }); }); describe('when the given address does not own any token', function () { it('reverts', async function () { - await expectRevert(this.token.tokenOfOwnerByIndex(other, 0), 'ERC721Enumerable: owner index out of bounds'); + await expectRevertCustomError(this.token.tokenOfOwnerByIndex(other, 0), 'ERC721OutOfBoundsIndex', [other, 0]); }); }); @@ -740,7 +767,7 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved it('returns empty collection for original owner', async function () { expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('0'); - await expectRevert(this.token.tokenOfOwnerByIndex(owner, 0), 'ERC721Enumerable: owner index out of bounds'); + await expectRevertCustomError(this.token.tokenOfOwnerByIndex(owner, 0), 'ERC721OutOfBoundsIndex', [owner, 0]); }); }); }); @@ -755,7 +782,7 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved }); it('reverts if index is greater than supply', async function () { - await expectRevert(this.token.tokenByIndex(2), 'ERC721Enumerable: global index out of bounds'); + await expectRevertCustomError(this.token.tokenByIndex(2), 'ERC721OutOfBoundsIndex', [ZERO_ADDRESS, 2]); }); [firstTokenId, secondTokenId].forEach(function (tokenId) { @@ -781,7 +808,9 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved describe('_mint(address, uint256)', function () { it('reverts with a null destination address', async function () { - await expectRevert(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721: mint to the zero address'); + await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721InvalidReceiver', [ + ZERO_ADDRESS, + ]); }); context('with minted token', async function () { @@ -801,7 +830,7 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved describe('_burn', function () { it('reverts when burning a non-existent token id', async function () { - await expectRevert(this.token.$_burn(firstTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.$_burn(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); }); context('with minted tokens', function () { @@ -826,14 +855,14 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved it('burns all tokens', async function () { await this.token.$_burn(secondTokenId, { from: owner }); expect(await this.token.totalSupply()).to.be.bignumber.equal('0'); - await expectRevert(this.token.tokenByIndex(0), 'ERC721Enumerable: global index out of bounds'); + await expectRevertCustomError(this.token.tokenByIndex(0), 'ERC721OutOfBoundsIndex', [ZERO_ADDRESS, 0]); }); }); }); }); } -function shouldBehaveLikeERC721Metadata(errorPrefix, name, symbol, owner) { +function shouldBehaveLikeERC721Metadata(name, symbol, owner) { shouldSupportInterfaces(['ERC721Metadata']); describe('metadata', function () { @@ -855,7 +884,9 @@ function shouldBehaveLikeERC721Metadata(errorPrefix, name, symbol, owner) { }); it('reverts when queried for non existent token id', async function () { - await expectRevert(this.token.tokenURI(nonExistentTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.tokenURI(nonExistentTokenId), 'ERC721NonexistentToken', [ + nonExistentTokenId, + ]); }); describe('base URI', function () { diff --git a/test/token/ERC721/ERC721.test.js b/test/token/ERC721/ERC721.test.js index 312430cb9..372dd5069 100644 --- a/test/token/ERC721/ERC721.test.js +++ b/test/token/ERC721/ERC721.test.js @@ -10,6 +10,6 @@ contract('ERC721', function (accounts) { this.token = await ERC721.new(name, symbol); }); - shouldBehaveLikeERC721('ERC721', ...accounts); - shouldBehaveLikeERC721Metadata('ERC721', name, symbol, ...accounts); + shouldBehaveLikeERC721(...accounts); + shouldBehaveLikeERC721Metadata(name, symbol, ...accounts); }); diff --git a/test/token/ERC721/ERC721Enumerable.test.js b/test/token/ERC721/ERC721Enumerable.test.js index b32f22dd6..31c28d177 100644 --- a/test/token/ERC721/ERC721Enumerable.test.js +++ b/test/token/ERC721/ERC721Enumerable.test.js @@ -14,7 +14,7 @@ contract('ERC721Enumerable', function (accounts) { this.token = await ERC721Enumerable.new(name, symbol); }); - shouldBehaveLikeERC721('ERC721', ...accounts); - shouldBehaveLikeERC721Metadata('ERC721', name, symbol, ...accounts); - shouldBehaveLikeERC721Enumerable('ERC721', ...accounts); + shouldBehaveLikeERC721(...accounts); + shouldBehaveLikeERC721Metadata(name, symbol, ...accounts); + shouldBehaveLikeERC721Enumerable(...accounts); }); diff --git a/test/token/ERC721/extensions/ERC721Burnable.test.js b/test/token/ERC721/extensions/ERC721Burnable.test.js index 6a4bc6dbc..c6c076919 100644 --- a/test/token/ERC721/extensions/ERC721Burnable.test.js +++ b/test/token/ERC721/extensions/ERC721Burnable.test.js @@ -1,6 +1,7 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC721Burnable = artifacts.require('$ERC721Burnable'); @@ -34,7 +35,7 @@ contract('ERC721Burnable', function (accounts) { }); it('burns the given token ID and adjusts the balance of the owner', async function () { - await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1'); }); @@ -55,14 +56,16 @@ contract('ERC721Burnable', function (accounts) { context('getApproved', function () { it('reverts', async function () { - await expectRevert(this.token.getApproved(tokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.getApproved(tokenId), 'ERC721NonexistentToken', [tokenId]); }); }); }); describe('when the given token ID was not tracked by this contract', function () { it('reverts', async function () { - await expectRevert(this.token.burn(unknownTokenId, { from: owner }), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.burn(unknownTokenId, { from: owner }), 'ERC721NonexistentToken', [ + unknownTokenId, + ]); }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Consecutive.test.js b/test/token/ERC721/extensions/ERC721Consecutive.test.js index d4f0b4f8a..172da86a6 100644 --- a/test/token/ERC721/extensions/ERC721Consecutive.test.js +++ b/test/token/ERC721/extensions/ERC721Consecutive.test.js @@ -1,6 +1,8 @@ -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { sum } = require('../../../helpers/math'); +const { expectRevertCustomError } = require('../../../helpers/customError'); +const { ZERO_ADDRESS } = require('@openzeppelin/test-helpers/src/constants'); const ERC721ConsecutiveMock = artifacts.require('$ERC721ConsecutiveMock'); const ERC721ConsecutiveEnumerableMock = artifacts.require('$ERC721ConsecutiveEnumerableMock'); @@ -87,10 +89,7 @@ contract('ERC721Consecutive', function (accounts) { describe('minting after construction', function () { it('consecutive minting is not possible after construction', async function () { - await expectRevert( - this.token.$_mintConsecutive(user1, 10), - 'ERC721Consecutive: batch minting restricted to constructor', - ); + await expectRevertCustomError(this.token.$_mintConsecutive(user1, 10), 'ERC721ForbiddenBatchMint', []); }); it('simple minting is possible after construction', async function () { @@ -110,7 +109,7 @@ contract('ERC721Consecutive', function (accounts) { expect(await this.token.$_exists(tokenId)).to.be.equal(true); - await expectRevert(this.token.$_mint(user1, tokenId), 'ERC721: token already minted'); + await expectRevertCustomError(this.token.$_mint(user1, tokenId), 'ERC721InvalidSender', [ZERO_ADDRESS]); }); }); @@ -130,7 +129,7 @@ contract('ERC721Consecutive', function (accounts) { tokenId, }); - await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); expectEvent(await this.token.$_mint(user2, tokenId), 'Transfer', { from: constants.ZERO_ADDRESS, @@ -145,7 +144,7 @@ contract('ERC721Consecutive', function (accounts) { const tokenId = web3.utils.toBN(sum(...batches.map(({ amount }) => amount)) + offset); expect(await this.token.$_exists(tokenId)).to.be.equal(false); - await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); // mint await this.token.$_mint(user1, tokenId); @@ -161,7 +160,7 @@ contract('ERC721Consecutive', function (accounts) { }); expect(await this.token.$_exists(tokenId)).to.be.equal(false); - await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); // re-mint expectEvent(await this.token.$_mint(user2, tokenId), 'Transfer', { @@ -179,35 +178,39 @@ contract('ERC721Consecutive', function (accounts) { describe('invalid use', function () { it('cannot mint a batch larger than 5000', async function () { - await expectRevert( + await expectRevertCustomError( ERC721ConsecutiveMock.new(name, symbol, 0, [], [user1], ['5001']), - 'ERC721Consecutive: batch too large', + 'ERC721ExceededMaxBatchMint', + [5000, 5001], ); }); it('cannot use single minting during construction', async function () { - await expectRevert( + await expectRevertCustomError( ERC721ConsecutiveNoConstructorMintMock.new(name, symbol), - "ERC721Consecutive: can't mint during construction", + 'ERC721ForbiddenMint', + [], ); }); it('cannot use single minting during construction', async function () { - await expectRevert( + await expectRevertCustomError( ERC721ConsecutiveNoConstructorMintMock.new(name, symbol), - "ERC721Consecutive: can't mint during construction", + 'ERC721ForbiddenMint', + [], ); }); it('consecutive mint not compatible with enumerability', async function () { - await expectRevert( + await expectRevertCustomError( ERC721ConsecutiveEnumerableMock.new( name, symbol, batches.map(({ receiver }) => receiver), batches.map(({ amount }) => amount), ), - 'ERC721Enumerable: consecutive transfers not supported', + 'ERC721EnumerableForbiddenBatchMint', + [], ); }); }); diff --git a/test/token/ERC721/extensions/ERC721Pausable.test.js b/test/token/ERC721/extensions/ERC721Pausable.test.js index c7fc8233f..ec99dea96 100644 --- a/test/token/ERC721/extensions/ERC721Pausable.test.js +++ b/test/token/ERC721/extensions/ERC721Pausable.test.js @@ -1,6 +1,7 @@ -const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC721Pausable = artifacts.require('$ERC721Pausable'); @@ -26,34 +27,37 @@ contract('ERC721Pausable', function (accounts) { }); it('reverts when trying to transferFrom', async function () { - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(owner, receiver, firstTokenId, { from: owner }), - 'ERC721Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to safeTransferFrom', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(owner, receiver, firstTokenId, { from: owner }), - 'ERC721Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to safeTransferFrom with data', async function () { - await expectRevert( + await expectRevertCustomError( this.token.methods['safeTransferFrom(address,address,uint256,bytes)'](owner, receiver, firstTokenId, mockData, { from: owner, }), - 'ERC721Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to mint', async function () { - await expectRevert(this.token.$_mint(receiver, secondTokenId), 'ERC721Pausable: token transfer while paused'); + await expectRevertCustomError(this.token.$_mint(receiver, secondTokenId), 'EnforcedPause', []); }); it('reverts when trying to burn', async function () { - await expectRevert(this.token.$_burn(firstTokenId), 'ERC721Pausable: token transfer while paused'); + await expectRevertCustomError(this.token.$_burn(firstTokenId), 'EnforcedPause', []); }); describe('getApproved', function () { diff --git a/test/token/ERC721/extensions/ERC721URIStorage.test.js b/test/token/ERC721/extensions/ERC721URIStorage.test.js index 60c80066c..34738cae1 100644 --- a/test/token/ERC721/extensions/ERC721URIStorage.test.js +++ b/test/token/ERC721/extensions/ERC721URIStorage.test.js @@ -1,7 +1,8 @@ -const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC721URIStorageMock = artifacts.require('$ERC721URIStorageMock'); @@ -33,7 +34,9 @@ contract('ERC721URIStorage', function (accounts) { }); it('reverts when queried for non existent token id', async function () { - await expectRevert(this.token.tokenURI(nonExistentTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.tokenURI(nonExistentTokenId), 'ERC721NonexistentToken', [ + nonExistentTokenId, + ]); }); it('can be set for a token id', async function () { @@ -48,10 +51,9 @@ contract('ERC721URIStorage', function (accounts) { }); it('reverts when setting for non existent token id', async function () { - await expectRevert( - this.token.$_setTokenURI(nonExistentTokenId, sampleUri), - 'ERC721URIStorage: URI set of nonexistent token', - ); + await expectRevertCustomError(this.token.$_setTokenURI(nonExistentTokenId, sampleUri), 'ERC721NonexistentToken', [ + nonExistentTokenId, + ]); }); it('base URI can be set', async function () { @@ -85,7 +87,7 @@ contract('ERC721URIStorage', function (accounts) { await this.token.$_burn(firstTokenId, { from: owner }); expect(await this.token.$_exists(firstTokenId)).to.equal(false); - await expectRevert(this.token.tokenURI(firstTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.tokenURI(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); }); it('tokens with URI can be burnt ', async function () { @@ -94,7 +96,7 @@ contract('ERC721URIStorage', function (accounts) { await this.token.$_burn(firstTokenId, { from: owner }); expect(await this.token.$_exists(firstTokenId)).to.equal(false); - await expectRevert(this.token.tokenURI(firstTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.tokenURI(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Wrapper.test.js b/test/token/ERC721/extensions/ERC721Wrapper.test.js index 6e46d2e5a..683997744 100644 --- a/test/token/ERC721/extensions/ERC721Wrapper.test.js +++ b/test/token/ERC721/extensions/ERC721Wrapper.test.js @@ -1,7 +1,8 @@ -const { BN, expectEvent, constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, expectEvent, constants } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { shouldBehaveLikeERC721 } = require('../ERC721.behavior'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC721 = artifacts.require('$ERC721'); const ERC721Wrapper = artifacts.require('$ERC721Wrapper'); @@ -115,9 +116,10 @@ contract('ERC721Wrapper', function (accounts) { }); it('reverts with missing approval', async function () { - await expectRevert( + await expectRevertCustomError( this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }), - 'ERC721: caller is not token owner or approved', + 'ERC721InsufficientApproval', + [this.token.address, firstTokenId], ); }); }); @@ -178,9 +180,10 @@ contract('ERC721Wrapper', function (accounts) { }); it("doesn't work for a non-owner nor approved", async function () { - await expectRevert( + await expectRevertCustomError( this.token.withdrawTo(initialHolder, [firstTokenId], { from: anotherAccount }), - 'ERC721Wrapper: caller is not token owner or approved', + 'ERC721InsufficientApproval', + [anotherAccount, firstTokenId], ); }); @@ -230,7 +233,7 @@ contract('ERC721Wrapper', function (accounts) { describe('onERC721Received', function () { it('only allows calls from underlying', async function () { - await expectRevert( + await expectRevertCustomError( this.token.onERC721Received( initialHolder, this.token.address, @@ -238,7 +241,8 @@ contract('ERC721Wrapper', function (accounts) { anotherAccount, // Correct data { from: anotherAccount }, ), - 'ERC721Wrapper: caller is not underlying', + 'ERC721UnsupportedToken', + [anotherAccount], ); }); @@ -270,14 +274,16 @@ contract('ERC721Wrapper', function (accounts) { }); it('reverts if there is nothing to recover', async function () { - await expectRevert( - this.token.$_recover(initialHolder, firstTokenId), - 'ERC721Wrapper: wrapper is not token owner', - ); + const owner = await this.underlying.ownerOf(firstTokenId); + await expectRevertCustomError(this.token.$_recover(initialHolder, firstTokenId), 'ERC721IncorrectOwner', [ + this.token.address, + firstTokenId, + owner, + ]); }); }); describe('ERC712 behavior', function () { - shouldBehaveLikeERC721('ERC721', ...accounts); + shouldBehaveLikeERC721(...accounts); }); }); diff --git a/test/token/common/ERC2981.behavior.js b/test/token/common/ERC2981.behavior.js index 5d0f67715..15efa239f 100644 --- a/test/token/common/ERC2981.behavior.js +++ b/test/token/common/ERC2981.behavior.js @@ -1,8 +1,9 @@ -const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { ZERO_ADDRESS } = constants; const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); +const { expectRevertCustomError } = require('../../helpers/customError'); function shouldBehaveLikeERC2981() { const royaltyFraction = new BN('10'); @@ -60,11 +61,18 @@ function shouldBehaveLikeERC2981() { }); it('reverts if invalid parameters', async function () { - await expectRevert(this.token.$_setDefaultRoyalty(ZERO_ADDRESS, royaltyFraction), 'ERC2981: invalid receiver'); + const royaltyDenominator = await this.token.$_feeDenominator(); + await expectRevertCustomError( + this.token.$_setDefaultRoyalty(ZERO_ADDRESS, royaltyFraction), + 'ERC2981InvalidDefaultRoyaltyReceiver', + [ZERO_ADDRESS], + ); - await expectRevert( - this.token.$_setDefaultRoyalty(this.account1, new BN('11000')), - 'ERC2981: royalty fee will exceed salePrice', + const anotherRoyaltyFraction = new BN('11000'); + await expectRevertCustomError( + this.token.$_setDefaultRoyalty(this.account1, anotherRoyaltyFraction), + 'ERC2981InvalidDefaultRoyalty', + [anotherRoyaltyFraction, royaltyDenominator], ); }); }); @@ -104,14 +112,18 @@ function shouldBehaveLikeERC2981() { }); it('reverts if invalid parameters', async function () { - await expectRevert( + const royaltyDenominator = await this.token.$_feeDenominator(); + await expectRevertCustomError( this.token.$_setTokenRoyalty(this.tokenId1, ZERO_ADDRESS, royaltyFraction), - 'ERC2981: Invalid parameters', + 'ERC2981InvalidTokenRoyaltyReceiver', + [this.tokenId1.toString(), ZERO_ADDRESS], ); - await expectRevert( - this.token.$_setTokenRoyalty(this.tokenId1, this.account1, new BN('11000')), - 'ERC2981: royalty fee will exceed salePrice', + const anotherRoyaltyFraction = new BN('11000'); + await expectRevertCustomError( + this.token.$_setTokenRoyalty(this.tokenId1, this.account1, anotherRoyaltyFraction), + 'ERC2981InvalidTokenRoyalty', + [this.tokenId1.toString(), anotherRoyaltyFraction, royaltyDenominator], ); }); diff --git a/test/utils/Address.test.js b/test/utils/Address.test.js index ea72ab610..beded18e1 100644 --- a/test/utils/Address.test.js +++ b/test/utils/Address.test.js @@ -1,7 +1,9 @@ const { balance, constants, ether, expectRevert, send, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); const Address = artifacts.require('$Address'); +const AddressFnPointerMock = artifacts.require('$AddressFnPointerMock'); const EtherReceiver = artifacts.require('EtherReceiverMock'); const CallReceiverMock = artifacts.require('CallReceiverMock'); @@ -10,6 +12,7 @@ contract('Address', function (accounts) { beforeEach(async function () { this.mock = await Address.new(); + this.mockFnPointer = await AddressFnPointerMock.new(); }); describe('sendValue', function () { @@ -25,7 +28,9 @@ contract('Address', function (accounts) { }); it('reverts when sending non-zero amounts', async function () { - await expectRevert(this.mock.$sendValue(other, 1), 'Address: insufficient balance'); + await expectRevertCustomError(this.mock.$sendValue(other, 1), 'AddressInsufficientBalance', [ + this.mock.address, + ]); }); }); @@ -52,7 +57,9 @@ contract('Address', function (accounts) { }); it('reverts when sending more than the balance', async function () { - await expectRevert(this.mock.$sendValue(recipient, funds.addn(1)), 'Address: insufficient balance'); + await expectRevertCustomError(this.mock.$sendValue(recipient, funds.addn(1)), 'AddressInsufficientBalance', [ + this.mock.address, + ]); }); context('with contract recipient', function () { @@ -71,10 +78,7 @@ contract('Address', function (accounts) { it('reverts on recipient revert', async function () { await this.target.setAcceptEther(false); - await expectRevert( - this.mock.$sendValue(this.target.address, funds), - 'Address: unable to send value, recipient may have reverted', - ); + await expectRevertCustomError(this.mock.$sendValue(this.target.address, funds), 'FailedInnerCall', []); }); }); }); @@ -91,7 +95,7 @@ contract('Address', function (accounts) { const receipt = await this.mock.$functionCall(this.target.address, abiEncodedCall); - expectEvent(receipt, 'return$functionCall_address_bytes', { + expectEvent(receipt, 'return$functionCall', { ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']), }); await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); @@ -108,9 +112,10 @@ contract('Address', function (accounts) { it('reverts when the called function reverts with no reason', async function () { const abiEncodedCall = this.target.contract.methods.mockFunctionRevertsNoReason().encodeABI(); - await expectRevert( + await expectRevertCustomError( this.mock.$functionCall(this.target.address, abiEncodedCall), - 'Address: low-level call failed', + 'FailedInnerCall', + [], ); }); @@ -123,9 +128,10 @@ contract('Address', function (accounts) { it('reverts when the called function runs out of gas', async function () { const abiEncodedCall = this.target.contract.methods.mockFunctionOutOfGas().encodeABI(); - await expectRevert( + await expectRevertCustomError( this.mock.$functionCall(this.target.address, abiEncodedCall, { gas: '120000' }), - 'Address: low-level call failed', + 'FailedInnerCall', + [], ); }); @@ -135,9 +141,12 @@ contract('Address', function (accounts) { await expectRevert.unspecified(this.mock.$functionCall(this.target.address, abiEncodedCall)); }); - it('bubbles up error message if specified', async function () { - const errorMsg = 'Address: expected error'; - await expectRevert(this.mock.$functionCall(this.target.address, '0x12345678', errorMsg), errorMsg); + it('bubbles up error if specified', async function () { + await expectRevertCustomError( + this.mockFnPointer.functionCall(this.target.address, '0x12345678'), + 'CustomRevert', + [], + ); }); it('reverts when function does not exist', async function () { @@ -150,9 +159,10 @@ contract('Address', function (accounts) { [], ); - await expectRevert( + await expectRevertCustomError( this.mock.$functionCall(this.target.address, abiEncodedCall), - 'Address: low-level call failed', + 'FailedInnerCall', + [], ); }); }); @@ -162,7 +172,9 @@ contract('Address', function (accounts) { const [recipient] = accounts; const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); - await expectRevert(this.mock.$functionCall(recipient, abiEncodedCall), 'Address: call to non-contract'); + await expectRevertCustomError(this.mock.$functionCall(recipient, abiEncodedCall), 'AddressEmptyCode', [ + recipient, + ]); }); }); }); @@ -177,7 +189,7 @@ contract('Address', function (accounts) { const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); const receipt = await this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, 0); - expectEvent(receipt, 'return$functionCallWithValue_address_bytes_uint256', { + expectEvent(receipt, 'return$functionCallWithValue', { ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']), }); await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); @@ -190,9 +202,10 @@ contract('Address', function (accounts) { it('reverts if insufficient sender balance', async function () { const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); - await expectRevert( + await expectRevertCustomError( this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount), - 'Address: insufficient balance for call', + 'AddressInsufficientBalance', + [this.mock.address], ); }); @@ -204,7 +217,7 @@ contract('Address', function (accounts) { await send.ether(other, this.mock.address, amount); const receipt = await this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount); - expectEvent(receipt, 'return$functionCallWithValue_address_bytes_uint256', { + expectEvent(receipt, 'return$functionCallWithValue', { ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']), }); await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); @@ -223,7 +236,7 @@ contract('Address', function (accounts) { from: other, value: amount, }); - expectEvent(receipt, 'return$functionCallWithValue_address_bytes_uint256', { + expectEvent(receipt, 'return$functionCallWithValue', { ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']), }); await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); @@ -235,15 +248,19 @@ contract('Address', function (accounts) { const abiEncodedCall = this.target.contract.methods.mockFunctionNonPayable().encodeABI(); await send.ether(other, this.mock.address, amount); - await expectRevert( + await expectRevertCustomError( this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount), - 'Address: low-level call with value failed', + 'FailedInnerCall', + [], ); }); - it('bubbles up error message if specified', async function () { - const errorMsg = 'Address: expected error'; - await expectRevert(this.mock.$functionCallWithValue(this.target.address, '0x12345678', 0, errorMsg), errorMsg); + it('bubbles up error if specified', async function () { + await expectRevertCustomError( + this.mockFnPointer.functionCallWithValue(this.target.address, '0x12345678', 0), + 'CustomRevert', + [], + ); }); }); }); @@ -264,9 +281,10 @@ contract('Address', function (accounts) { it('reverts on a non-static function', async function () { const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); - await expectRevert( + await expectRevertCustomError( this.mock.$functionStaticCall(this.target.address, abiEncodedCall), - 'Address: low-level static call failed', + 'FailedInnerCall', + [], ); }); @@ -283,12 +301,17 @@ contract('Address', function (accounts) { const [recipient] = accounts; const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); - await expectRevert(this.mock.$functionStaticCall(recipient, abiEncodedCall), 'Address: call to non-contract'); + await expectRevertCustomError(this.mock.$functionStaticCall(recipient, abiEncodedCall), 'AddressEmptyCode', [ + recipient, + ]); }); - it('bubbles up error message if specified', async function () { - const errorMsg = 'Address: expected error'; - await expectRevert(this.mock.$functionCallWithValue(this.target.address, '0x12345678', 0, errorMsg), errorMsg); + it('bubbles up error if specified', async function () { + await expectRevertCustomError( + this.mockFnPointer.functionCallWithValue(this.target.address, '0x12345678', 0), + 'CustomRevert', + [], + ); }); }); @@ -308,7 +331,7 @@ contract('Address', function (accounts) { expectEvent( await this.mock.$functionDelegateCall(this.target.address, abiEncodedCall), - 'return$functionDelegateCall_address_bytes', + 'return$functionDelegateCall', { ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']) }, ); @@ -328,24 +351,32 @@ contract('Address', function (accounts) { const [recipient] = accounts; const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); - await expectRevert(this.mock.$functionDelegateCall(recipient, abiEncodedCall), 'Address: call to non-contract'); + await expectRevertCustomError(this.mock.$functionDelegateCall(recipient, abiEncodedCall), 'AddressEmptyCode', [ + recipient, + ]); }); - it('bubbles up error message if specified', async function () { - const errorMsg = 'Address: expected error'; - await expectRevert(this.mock.$functionCallWithValue(this.target.address, '0x12345678', 0, errorMsg), errorMsg); + it('bubbles up error if specified', async function () { + await expectRevertCustomError( + this.mockFnPointer.functionCallWithValue(this.target.address, '0x12345678', 0), + 'CustomRevert', + [], + ); }); }); describe('verifyCallResult', function () { it('returns returndata on success', async function () { const returndata = '0x123abc'; - expect(await this.mock.$verifyCallResult(true, returndata, '')).to.equal(returndata); + expect(await this.mockFnPointer.verifyCallResult(true, returndata)).to.equal(returndata); }); - it('reverts with return data and error m', async function () { - const errorMsg = 'Address: expected error'; - await expectRevert(this.mock.$verifyCallResult(false, '0x', errorMsg), errorMsg); + it('reverts with return data and error', async function () { + await expectRevertCustomError(this.mockFnPointer.verifyCallResult(false, '0x'), 'CustomRevert', []); + }); + + it('reverts expecting error if provided onRevert is a non-reverting function', async function () { + await expectRevertCustomError(this.mockFnPointer.verifyCallResultVoid(false, '0x'), 'FailedInnerCall', []); }); }); }); diff --git a/test/utils/Create2.test.js b/test/utils/Create2.test.js index 526602600..f88d5504c 100644 --- a/test/utils/Create2.test.js +++ b/test/utils/Create2.test.js @@ -1,6 +1,7 @@ const { balance, ether, expectEvent, expectRevert, send } = require('@openzeppelin/test-helpers'); const { computeCreate2Address } = require('../helpers/create2'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); const Create2 = artifacts.require('$Create2'); const VestingWallet = artifacts.require('VestingWallet'); @@ -78,15 +79,22 @@ contract('Create2', function (accounts) { it('fails deploying a contract in an existent address', async function () { expectEvent(await this.factory.$deploy(0, saltHex, constructorByteCode), 'return$deploy'); - await expectRevert(this.factory.$deploy(0, saltHex, constructorByteCode), 'Create2: Failed on deploy'); + // TODO: Make sure it actually throws "Create2FailedDeployment". + // For some unknown reason, the revert reason sometimes return: + // `revert with unrecognized return data or custom error` + await expectRevert.unspecified(this.factory.$deploy(0, saltHex, constructorByteCode)); }); it('fails deploying a contract if the bytecode length is zero', async function () { - await expectRevert(this.factory.$deploy(0, saltHex, '0x'), 'Create2: bytecode length is zero'); + await expectRevertCustomError(this.factory.$deploy(0, saltHex, '0x'), 'Create2EmptyBytecode', []); }); it('fails deploying a contract if factory contract does not have sufficient balance', async function () { - await expectRevert(this.factory.$deploy(1, saltHex, constructorByteCode), 'Create2: insufficient balance'); + await expectRevertCustomError( + this.factory.$deploy(1, saltHex, constructorByteCode), + 'Create2InsufficientBalance', + [0, 1], + ); }); }); }); diff --git a/test/utils/Multicall.test.js b/test/utils/Multicall.test.js index cfb800769..65443cd0a 100644 --- a/test/utils/Multicall.test.js +++ b/test/utils/Multicall.test.js @@ -1,4 +1,5 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); +const { expectRevertCustomError } = require('../helpers/customError'); const ERC20MulticallMock = artifacts.require('$ERC20MulticallMock'); @@ -50,7 +51,7 @@ contract('Multicall', function (accounts) { { from: deployer }, ); - await expectRevert(call, 'ERC20: transfer amount exceeds balance'); + await expectRevertCustomError(call, 'ERC20InsufficientBalance', [deployer, 0, amount]); expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0')); }); @@ -63,6 +64,6 @@ contract('Multicall', function (accounts) { { from: deployer }, ); - await expectRevert(call, 'ERC20: transfer amount exceeds balance'); + await expectRevertCustomError(call, 'ERC20InsufficientBalance', [deployer, 0, amount]); }); }); diff --git a/test/utils/Nonces.test.js b/test/utils/Nonces.test.js index 4a01bb1bc..361eeeeec 100644 --- a/test/utils/Nonces.test.js +++ b/test/utils/Nonces.test.js @@ -1,4 +1,5 @@ const expectEvent = require('@openzeppelin/test-helpers/src/expectEvent'); +const { expectRevertCustomError } = require('../helpers/customError'); require('@openzeppelin/test-helpers'); @@ -15,22 +16,57 @@ contract('Nonces', function (accounts) { expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); }); - it('increment a nonce', async function () { - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); + describe('_useNonce', function () { + it('increments a nonce', async function () { + expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); - const { receipt } = await this.nonces.$_useNonce(sender); - expectEvent(receipt, 'return$_useNonce', ['0']); + const { receipt } = await this.nonces.$_useNonce(sender); + expectEvent(receipt, 'return$_useNonce', ['0']); - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); + expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); + }); + + it("increments only sender's nonce", async function () { + expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); + expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + + await this.nonces.$_useNonce(sender); + + expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); + expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + }); }); - it('nonce is specific to address argument', async function () { - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); - expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + describe('_useCheckedNonce', function () { + it('increments a nonce', async function () { + const currentNonce = await this.nonces.nonces(sender); + expect(currentNonce).to.be.bignumber.equal('0'); - await this.nonces.$_useNonce(sender); + const { receipt } = await this.nonces.$_useCheckedNonce(sender, currentNonce); + expectEvent(receipt, 'return$_useCheckedNonce', [currentNonce]); - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); - expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); + }); + + it("increments only sender's nonce", async function () { + const currentNonce = await this.nonces.nonces(sender); + + expect(currentNonce).to.be.bignumber.equal('0'); + expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + + await this.nonces.$_useCheckedNonce(sender, currentNonce); + + expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); + expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + }); + + it('reverts when nonce is not the expected', async function () { + const currentNonce = await this.nonces.nonces(sender); + await expectRevertCustomError( + this.nonces.$_useCheckedNonce(sender, currentNonce.addn(1)), + 'InvalidAccountNonce', + [sender, currentNonce], + ); + }); }); }); diff --git a/test/utils/ShortStrings.test.js b/test/utils/ShortStrings.test.js index f5cd82fbd..189281d38 100644 --- a/test/utils/ShortStrings.test.js +++ b/test/utils/ShortStrings.test.js @@ -29,7 +29,7 @@ contract('ShortStrings', function () { const decoded = await this.mock.$toString(encoded); expect(decoded).to.be.equal(str); } else { - await expectRevertCustomError(this.mock.$toShortString(str), `StringTooLong("${str}")`); + await expectRevertCustomError(this.mock.$toShortString(str), 'StringTooLong', [str]); } }); @@ -41,7 +41,7 @@ contract('ShortStrings', function () { if (str.length < 32) { expect(await promise).to.be.equal(str); } else { - await expectRevertCustomError(promise, 'InvalidShortString()'); + await expectRevertCustomError(promise, 'InvalidShortString', []); } const length = await this.mock.$byteLengthWithFallback(ret0, 0); diff --git a/test/utils/Strings.test.js b/test/utils/Strings.test.js index 6658871a0..09b958a61 100644 --- a/test/utils/Strings.test.js +++ b/test/utils/Strings.test.js @@ -1,4 +1,5 @@ -const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants } = require('@openzeppelin/test-helpers'); +const { expectRevertCustomError } = require('../helpers/customError'); const { expect } = require('chai'); @@ -92,9 +93,11 @@ contract('Strings', function () { }); it('converts a positive number (short)', async function () { - await expectRevert( - this.strings.methods['$toHexString(uint256,uint256)'](0x4132, 1), - 'Strings: hex length insufficient', + const length = 1; + await expectRevertCustomError( + this.strings.methods['$toHexString(uint256,uint256)'](0x4132, length), + `StringsInsufficientHexLength`, + [0x4132, length], ); }); diff --git a/test/utils/cryptography/ECDSA.test.js b/test/utils/cryptography/ECDSA.test.js index ae737086b..3fd112a18 100644 --- a/test/utils/cryptography/ECDSA.test.js +++ b/test/utils/cryptography/ECDSA.test.js @@ -1,4 +1,5 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); +require('@openzeppelin/test-helpers'); +const { expectRevertCustomError } = require('../../helpers/customError'); const { toEthSignedMessageHash, toDataWithIntendedValidatorHash } = require('../../helpers/sign'); const { expect } = require('chai'); @@ -51,17 +52,18 @@ contract('ECDSA', function (accounts) { context('recover with invalid signature', function () { it('with short signature', async function () { - await expectRevert(this.ecdsa.$recover(TEST_MESSAGE, '0x1234'), 'ECDSA: invalid signature length'); + await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, '0x1234'), 'ECDSAInvalidSignatureLength', [2]); }); it('with long signature', async function () { - await expectRevert( + await expectRevertCustomError( // eslint-disable-next-line max-len this.ecdsa.$recover( TEST_MESSAGE, '0x01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789', ), - 'ECDSA: invalid signature length', + 'ECDSAInvalidSignatureLength', + [85], ); }); }); @@ -93,7 +95,7 @@ contract('ECDSA', function (accounts) { // eslint-disable-next-line max-len const signature = '0x332ce75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e01c'; - await expectRevert(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSA: invalid signature'); + await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSAInvalidSignature', []); }); }); @@ -141,11 +143,12 @@ contract('ECDSA', function (accounts) { it('reverts wrong v values', async function () { for (const v of ['00', '01']) { const signature = signatureWithoutV + v; - await expectRevert(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSA: invalid signature'); + await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSAInvalidSignature', []); - await expectRevert( + await expectRevertCustomError( this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)), - 'ECDSA: invalid signature', + 'ECDSAInvalidSignature', + [], ); } }); @@ -153,9 +156,10 @@ contract('ECDSA', function (accounts) { it('rejects short EIP2098 format', async function () { const v = '1b'; // 27 = 1b. const signature = signatureWithoutV + v; - await expectRevert( + await expectRevertCustomError( this.ecdsa.$recover(TEST_MESSAGE, to2098Format(signature)), - 'ECDSA: invalid signature length', + 'ECDSAInvalidSignatureLength', + [64], ); }); }); @@ -203,11 +207,12 @@ contract('ECDSA', function (accounts) { it('reverts invalid v values', async function () { for (const v of ['00', '01']) { const signature = signatureWithoutV + v; - await expectRevert(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSA: invalid signature'); + await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSAInvalidSignature', []); - await expectRevert( + await expectRevertCustomError( this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)), - 'ECDSA: invalid signature', + 'ECDSAInvalidSignature', + [], ); } }); @@ -215,9 +220,10 @@ contract('ECDSA', function (accounts) { it('rejects short EIP2098 format', async function () { const v = '1c'; // 27 = 1b. const signature = signatureWithoutV + v; - await expectRevert( + await expectRevertCustomError( this.ecdsa.$recover(TEST_MESSAGE, to2098Format(signature)), - 'ECDSA: invalid signature length', + 'ECDSAInvalidSignatureLength', + [64], ); }); }); @@ -227,10 +233,12 @@ contract('ECDSA', function (accounts) { // eslint-disable-next-line max-len const highSSignature = '0xe742ff452d41413616a5bf43fe15dd88294e983d3d36206c2712f39083d638bde0a0fc89be718fbc1033e1d30d78be1c68081562ed2e97af876f286f3453231d1b'; - await expectRevert(this.ecdsa.$recover(message, highSSignature), "ECDSA: invalid signature 's' value"); - await expectRevert( - this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(highSSignature)), - "ECDSA: invalid signature 's' value", + const [r, v, s] = split(highSSignature); + await expectRevertCustomError(this.ecdsa.$recover(message, highSSignature), 'ECDSAInvalidSignatureS', [s]); + await expectRevertCustomError( + this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, r, v, s), + 'ECDSAInvalidSignatureS', + [s], ); expect(() => to2098Format(highSSignature)).to.throw("invalid signature 's' value"); }); diff --git a/test/utils/cryptography/MerkleProof.test.js b/test/utils/cryptography/MerkleProof.test.js index 62157b56a..43ef76bfa 100644 --- a/test/utils/cryptography/MerkleProof.test.js +++ b/test/utils/cryptography/MerkleProof.test.js @@ -1,10 +1,10 @@ -require('@openzeppelin/test-helpers'); - const { expectRevert } = require('@openzeppelin/test-helpers'); + const { MerkleTree } = require('merkletreejs'); const keccak256 = require('keccak256'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../helpers/customError'); const MerkleProof = artifacts.require('$MerkleProof'); @@ -106,23 +106,25 @@ contract('MerkleProof', function () { const root = merkleTree.getRoot(); - await expectRevert( + await expectRevertCustomError( this.merkleProof.$multiProofVerify( [leaves[1], fill, merkleTree.layers[1][1]], [false, false, false], root, [leaves[0], badLeaf], // A, E ), - 'MerkleProof: invalid multiproof', + 'MerkleProofInvalidMultiproof', + [], ); - await expectRevert( + await expectRevertCustomError( this.merkleProof.$multiProofVerifyCalldata( [leaves[1], fill, merkleTree.layers[1][1]], [false, false, false], root, [leaves[0], badLeaf], // A, E ), - 'MerkleProof: invalid multiproof', + 'MerkleProofInvalidMultiproof', + [], ); }); diff --git a/test/utils/math/SafeCast.test.js b/test/utils/math/SafeCast.test.js index 63223f5d1..4b8ec5a72 100644 --- a/test/utils/math/SafeCast.test.js +++ b/test/utils/math/SafeCast.test.js @@ -1,6 +1,7 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { range } = require('../../../scripts/helpers'); +const { expectRevertCustomError } = require('../../helpers/customError'); const SafeCast = artifacts.require('$SafeCast'); @@ -26,16 +27,18 @@ contract('SafeCast', async function () { }); it(`reverts when downcasting 2^${bits} (${maxValue.addn(1)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toUint${bits}`](maxValue.addn(1)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedUintDowncast`, + [bits, maxValue.addn(1)], ); }); it(`reverts when downcasting 2^${bits} + 1 (${maxValue.addn(2)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toUint${bits}`](maxValue.addn(2)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedUintDowncast`, + [bits, maxValue.addn(2)], ); }); }); @@ -60,11 +63,11 @@ contract('SafeCast', async function () { }); it('reverts when casting -1', async function () { - await expectRevert(this.safeCast.$toUint256(-1), 'SafeCast: value must be positive'); + await expectRevertCustomError(this.safeCast.$toUint256(-1), `SafeCastOverflowedIntToUint`, [-1]); }); it(`reverts when casting INT256_MIN (${minInt256})`, async function () { - await expectRevert(this.safeCast.$toUint256(minInt256), 'SafeCast: value must be positive'); + await expectRevertCustomError(this.safeCast.$toUint256(minInt256), `SafeCastOverflowedIntToUint`, [minInt256]); }); }); @@ -94,30 +97,34 @@ contract('SafeCast', async function () { }); it(`reverts when downcasting -2^${bits - 1} - 1 (${minValue.subn(1)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toInt${bits}`](minValue.subn(1)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedIntDowncast`, + [bits, minValue.subn(1)], ); }); it(`reverts when downcasting -2^${bits - 1} - 2 (${minValue.subn(2)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toInt${bits}`](minValue.subn(2)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedIntDowncast`, + [bits, minValue.subn(2)], ); }); it(`reverts when downcasting 2^${bits - 1} (${maxValue.addn(1)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toInt${bits}`](maxValue.addn(1)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedIntDowncast`, + [bits, maxValue.addn(1)], ); }); it(`reverts when downcasting 2^${bits - 1} + 1 (${maxValue.addn(2)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toInt${bits}`](maxValue.addn(2)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedIntDowncast`, + [bits, maxValue.addn(2)], ); }); }); @@ -142,11 +149,13 @@ contract('SafeCast', async function () { }); it(`reverts when casting INT256_MAX + 1 (${maxInt256.addn(1)})`, async function () { - await expectRevert(this.safeCast.$toInt256(maxInt256.addn(1)), "SafeCast: value doesn't fit in an int256"); + await expectRevertCustomError(this.safeCast.$toInt256(maxInt256.addn(1)), 'SafeCastOverflowedUintToInt', [ + maxInt256.addn(1), + ]); }); it(`reverts when casting UINT256_MAX (${maxUint256})`, async function () { - await expectRevert(this.safeCast.$toInt256(maxUint256), "SafeCast: value doesn't fit in an int256"); + await expectRevertCustomError(this.safeCast.$toInt256(maxUint256), 'SafeCastOverflowedUintToInt', [maxUint256]); }); }); }); diff --git a/test/utils/structs/Checkpoints.test.js b/test/utils/structs/Checkpoints.test.js index ad95373a4..d82180127 100644 --- a/test/utils/structs/Checkpoints.test.js +++ b/test/utils/structs/Checkpoints.test.js @@ -1,7 +1,9 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); +require('@openzeppelin/test-helpers'); + const { expect } = require('chai'); const { VALUE_SIZES } = require('../../../scripts/generate/templates/Checkpoints.opts.js'); +const { expectRevertCustomError } = require('../../helpers/customError.js'); const $Checkpoints = artifacts.require('$Checkpoints'); @@ -77,7 +79,11 @@ contract('Checkpoints', function () { }); it('cannot push values in the past', async function () { - await expectRevert(this.methods.push(last(this.checkpoints).key - 1, '0'), 'Checkpoint: decreasing keys'); + await expectRevertCustomError( + this.methods.push(last(this.checkpoints).key - 1, '0'), + 'CheckpointUnorderedInsertion', + [], + ); }); it('can update last value', async function () { diff --git a/test/utils/structs/DoubleEndedQueue.test.js b/test/utils/structs/DoubleEndedQueue.test.js index 2fbb8dc25..cbf37d76b 100644 --- a/test/utils/structs/DoubleEndedQueue.test.js +++ b/test/utils/structs/DoubleEndedQueue.test.js @@ -30,10 +30,10 @@ contract('DoubleEndedQueue', function () { }); it('reverts on accesses', async function () { - await expectRevertCustomError(this.deque.$popBack(0), 'Empty()'); - await expectRevertCustomError(this.deque.$popFront(0), 'Empty()'); - await expectRevertCustomError(this.deque.$back(0), 'Empty()'); - await expectRevertCustomError(this.deque.$front(0), 'Empty()'); + await expectRevertCustomError(this.deque.$popBack(0), 'QueueEmpty', []); + await expectRevertCustomError(this.deque.$popFront(0), 'QueueEmpty', []); + await expectRevertCustomError(this.deque.$back(0), 'QueueEmpty', []); + await expectRevertCustomError(this.deque.$front(0), 'QueueEmpty', []); }); }); @@ -54,7 +54,7 @@ contract('DoubleEndedQueue', function () { }); it('out of bounds access', async function () { - await expectRevertCustomError(this.deque.$at(0, this.content.length), 'OutOfBounds()'); + await expectRevertCustomError(this.deque.$at(0, this.content.length), 'QueueOutOfBounds', []); }); describe('push', function () { diff --git a/test/utils/structs/EnumerableMap.behavior.js b/test/utils/structs/EnumerableMap.behavior.js index 3db45df6a..67b19e39a 100644 --- a/test/utils/structs/EnumerableMap.behavior.js +++ b/test/utils/structs/EnumerableMap.behavior.js @@ -1,7 +1,8 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const zip = require('lodash.zip'); +const { expectRevertCustomError } = require('../../helpers/customError'); function shouldBehaveLikeMap(keys, values, zeroValue, methods, events) { const [keyA, keyB, keyC] = keys; @@ -150,7 +151,10 @@ function shouldBehaveLikeMap(keys, values, zeroValue, methods, events) { expect(await methods.get(this.map, keyA).then(r => r.toString())).to.be.equal(valueA.toString()); }); it('missing value', async function () { - await expectRevert(methods.get(this.map, keyB), 'EnumerableMap: nonexistent key'); + const key = web3.utils.toHex(keyB); + await expectRevertCustomError(methods.get(this.map, keyB), 'EnumerableMapNonexistentKey', [ + key.length == 66 ? key : web3.utils.padLeft(key, 64, '0'), + ]); }); }); From fe08f58c69ab97ccb3ad54a568e7ac8335e01396 Mon Sep 17 00:00:00 2001 From: Zack Reneau-Wedeen Date: Tue, 13 Jun 2023 15:50:16 -0400 Subject: [PATCH 166/197] Update README.md (#4335) --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aba991710..8b44917fe 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,9 @@ Finally, you may want to take a look at the [guides on our blog](https://blog.op This project is maintained by [OpenZeppelin](https://openzeppelin.com) with the goal of providing a secure and reliable library of smart contract components for the ecosystem. We address security through risk management in various areas such as engineering and open source best practices, scoping and API design, multi-layered review processes, and incident response preparedness. -The security policy is detailed in [`SECURITY.md`](./SECURITY.md), and specifies how you can report security vulnerabilities, which versions will receive security patches, and how to stay informed about them. We run a [bug bounty program on Immunefi](https://immunefi.com/bounty/openzeppelin) to reward the responsible disclosure of vulnerabilities. +The [OpenZeppelin Contracts Security Center](https://contracts.openzeppelin.com/security) contains more details about the secure development process. + +The security policy is detailed in [`SECURITY.md`](./SECURITY.md) as well, and specifies how you can report security vulnerabilities, which versions will receive security patches, and how to stay informed about them. We run a [bug bounty program on Immunefi](https://immunefi.com/bounty/openzeppelin) to reward the responsible disclosure of vulnerabilities. The engineering guidelines we follow to promote project quality can be found in [`GUIDELINES.md`](./GUIDELINES.md). From d6c7cee32191850d3635222826985f46996e64fd Mon Sep 17 00:00:00 2001 From: Zack Reneau-Wedeen Date: Tue, 13 Jun 2023 16:32:24 -0400 Subject: [PATCH 167/197] Update index.adoc (#4336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- docs/modules/ROOT/pages/index.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index b3b51abfc..b46a8d5a3 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -45,6 +45,8 @@ To keep your system secure, you should **always** use the installed code as-is, Please report any security issues you find via our https://www.immunefi.com/bounty/openzeppelin[bug bounty program on Immunefi] or directly to security@openzeppelin.org. +The https://contracts.openzeppelin.com/security[Security Center] contains more details about the secure development process. + [[next-steps]] == Learn More From 604025400f9be5c32581bb6ab03a46bbc09c5562 Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Tue, 13 Jun 2023 23:54:09 -0300 Subject: [PATCH 168/197] Add EIP-712 `name` and `version` getters (#4303) Co-authored-by: Hadrien Croubois Co-authored-by: ernestognw --- .changeset/little-falcons-build.md | 5 ++ contracts/utils/cryptography/EIP712.sol | 30 ++++++++- scripts/upgradeable/upgradeable.patch | 90 +++++++++++++------------ test/utils/cryptography/EIP712.test.js | 8 +++ 4 files changed, 88 insertions(+), 45 deletions(-) create mode 100644 .changeset/little-falcons-build.md diff --git a/.changeset/little-falcons-build.md b/.changeset/little-falcons-build.md new file mode 100644 index 000000000..b310a8ae6 --- /dev/null +++ b/.changeset/little-falcons-build.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`EIP712`: Add internal getters for the name and version strings diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol index b9c9c9d19..2628014f1 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -130,12 +130,38 @@ abstract contract EIP712 is IERC5267 { { return ( hex"0f", // 01111 - _name.toStringWithFallback(_nameFallback), - _version.toStringWithFallback(_versionFallback), + _EIP712Name(), + _EIP712Version(), block.chainid, address(this), bytes32(0), new uint256[](0) ); } + + /** + * @dev The name parameter for the EIP712 domain. + * + * NOTE: By default this function reads _name which is an immutable value. + * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). + * + * _Available since v5.0._ + */ + // solhint-disable-next-line func-name-mixedcase + function _EIP712Name() internal view returns (string memory) { + return _name.toStringWithFallback(_nameFallback); + } + + /** + * @dev The version parameter for the EIP712 domain. + * + * NOTE: By default this function reads _version which is an immutable value. + * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). + * + * _Available since v5.0._ + */ + // solhint-disable-next-line func-name-mixedcase + function _EIP712Version() internal view returns (string memory) { + return _version.toStringWithFallback(_versionFallback); + } } diff --git a/scripts/upgradeable/upgradeable.patch b/scripts/upgradeable/upgradeable.patch index b29e7d12b..554775ffe 100644 --- a/scripts/upgradeable/upgradeable.patch +++ b/scripts/upgradeable/upgradeable.patch @@ -59,10 +59,10 @@ index ff596b0c..00000000 - - diff --git a/README.md b/README.md -index 9fc95518..53130e3c 100644 +index aba99171..6656267b 100644 --- a/README.md +++ b/README.md -@@ -16,17 +16,20 @@ +@@ -19,17 +19,20 @@ :building_construction: **Want to scale your decentralized application?** Check out [OpenZeppelin Defender](https://openzeppelin.com/defender) — a secure platform for automating and monitoring your operations. @@ -85,7 +85,7 @@ index 9fc95518..53130e3c 100644 ### Usage -@@ -35,10 +38,11 @@ Once installed, you can use the contracts in the library by importing them: +@@ -38,10 +41,11 @@ Once installed, you can use the contracts in the library by importing them: ```solidity pragma solidity ^0.8.19; @@ -101,7 +101,7 @@ index 9fc95518..53130e3c 100644 } ``` diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol -index bb70d19f..38513771 100644 +index 5b7e1b15..1ca745d6 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -18,6 +18,8 @@ import "../utils/Context.sol"; @@ -114,7 +114,7 @@ index bb70d19f..38513771 100644 contract VestingWallet is Context { event EtherReleased(uint256 amount); diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol -index 64431711..885f0e42 100644 +index 5d8318f4..ef3cde55 100644 --- a/contracts/governance/extensions/GovernorVotes.sol +++ b/contracts/governance/extensions/GovernorVotes.sol @@ -10,6 +10,8 @@ import "../../interfaces/IERC5805.sol"; @@ -127,7 +127,7 @@ index 64431711..885f0e42 100644 abstract contract GovernorVotes is Governor { IERC5805 public immutable token; diff --git a/contracts/package.json b/contracts/package.json -index 55e70b17..ceefb984 100644 +index 4d0f576b..822fd471 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,5 +1,5 @@ @@ -135,7 +135,7 @@ index 55e70b17..ceefb984 100644 - "name": "@openzeppelin/contracts", + "name": "@openzeppelin/contracts-upgradeable", "description": "Secure Smart Contract library for Solidity", - "version": "4.8.2", + "version": "4.9.0", "files": [ @@ -13,7 +13,7 @@ }, @@ -147,7 +147,7 @@ index 55e70b17..ceefb984 100644 "keywords": [ "solidity", diff --git a/contracts/token/ERC20/extensions/ERC20Capped.sol b/contracts/token/ERC20/extensions/ERC20Capped.sol -index 16f830d1..9ef98148 100644 +index cda07265..d314148c 100644 --- a/contracts/token/ERC20/extensions/ERC20Capped.sol +++ b/contracts/token/ERC20/extensions/ERC20Capped.sol @@ -7,6 +7,8 @@ import "../ERC20.sol"; @@ -160,20 +160,20 @@ index 16f830d1..9ef98148 100644 abstract contract ERC20Capped is ERC20 { uint256 private immutable _cap; diff --git a/contracts/token/ERC20/extensions/ERC20Permit.sol b/contracts/token/ERC20/extensions/ERC20Permit.sol -index a357199b..9dc8e894 100644 +index 9379e445..e02f0644 100644 --- a/contracts/token/ERC20/extensions/ERC20Permit.sol +++ b/contracts/token/ERC20/extensions/ERC20Permit.sol -@@ -18,6 +18,8 @@ import "../../../utils/Counters.sol"; +@@ -18,6 +18,8 @@ import "../../../utils/Nonces.sol"; * need to send a transaction, and thus is not required to hold Ether at all. * * _Available since v3.4._ + * + * @custom:storage-size 51 */ - abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712 { - using Counters for Counters.Counter; + abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { + // solhint-disable-next-line var-name-mixedcase diff --git a/contracts/token/ERC20/extensions/ERC20Wrapper.sol b/contracts/token/ERC20/extensions/ERC20Wrapper.sol -index bfe782e4..7264fe32 100644 +index bf2b225c..0e5b3628 100644 --- a/contracts/token/ERC20/extensions/ERC20Wrapper.sol +++ b/contracts/token/ERC20/extensions/ERC20Wrapper.sol @@ -14,6 +14,8 @@ import "../utils/SafeERC20.sol"; @@ -186,7 +186,7 @@ index bfe782e4..7264fe32 100644 abstract contract ERC20Wrapper is ERC20 { IERC20 private immutable _underlying; diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol -index 6a4e1cad..55d8eced 100644 +index 2628014f..7d5193c8 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -4,7 +4,6 @@ @@ -268,7 +268,7 @@ index 6a4e1cad..55d8eced 100644 } /** -@@ -129,14 +114,80 @@ abstract contract EIP712 is IERC5267 { +@@ -128,6 +113,10 @@ abstract contract EIP712 is IERC5267 { uint256[] memory extensions ) { @@ -278,34 +278,34 @@ index 6a4e1cad..55d8eced 100644 + return ( hex"0f", // 01111 -- _name.toStringWithFallback(_nameFallback), -- _version.toStringWithFallback(_versionFallback), -+ _EIP712Name(), -+ _EIP712Version(), - block.chainid, - address(this), - bytes32(0), - new uint256[](0) - ); - } -+ -+ /** -+ * @dev The name parameter for the EIP712 domain. -+ * + _EIP712Name(), +@@ -142,26 +131,62 @@ abstract contract EIP712 is IERC5267 { + /** + * @dev The name parameter for the EIP712 domain. + * +- * NOTE: By default this function reads _name which is an immutable value. +- * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). +- * +- * _Available since v5.0._ + * NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs + * are a concern. -+ */ -+ function _EIP712Name() internal virtual view returns (string memory) { + */ +- // solhint-disable-next-line func-name-mixedcase +- function _EIP712Name() internal view returns (string memory) { +- return _name.toStringWithFallback(_nameFallback); ++ function _EIP712Name() internal view virtual returns (string memory) { + return _name; -+ } -+ -+ /** -+ * @dev The version parameter for the EIP712 domain. -+ * + } + + /** + * @dev The version parameter for the EIP712 domain. + * +- * NOTE: By default this function reads _version which is an immutable value. +- * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). + * NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs + * are a concern. + */ -+ function _EIP712Version() internal virtual view returns (string memory) { ++ function _EIP712Version() internal view virtual returns (string memory) { + return _version; + } + @@ -332,9 +332,13 @@ index 6a4e1cad..55d8eced 100644 + + /** + * @dev The hash of the version parameter for the EIP712 domain. -+ * + * +- * _Available since v5.0._ + * NOTE: In previous versions this function was virtual. In this version you should override `_EIP712Version` instead. -+ */ + */ +- // solhint-disable-next-line func-name-mixedcase +- function _EIP712Version() internal view returns (string memory) { +- return _version.toStringWithFallback(_versionFallback); + function _EIP712VersionHash() internal view returns (bytes32) { + string memory version = _EIP712Version(); + if (bytes(version).length > 0) { @@ -349,13 +353,13 @@ index 6a4e1cad..55d8eced 100644 + return keccak256(""); + } + } -+ } + } } diff --git a/package.json b/package.json -index 8458dd61..b4672240 100644 +index c070915f..9a513cac 100644 --- a/package.json +++ b/package.json -@@ -36,7 +36,7 @@ +@@ -33,7 +33,7 @@ }, "repository": { "type": "git", @@ -365,7 +369,7 @@ index 8458dd61..b4672240 100644 "keywords": [ "solidity", diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js -index 54a4e772..ba4602ed 100644 +index 7ea535b7..32e3a370 100644 --- a/test/utils/cryptography/EIP712.test.js +++ b/test/utils/cryptography/EIP712.test.js @@ -47,26 +47,6 @@ contract('EIP712', function (accounts) { diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js index 54a4e7723..7ea535b7f 100644 --- a/test/utils/cryptography/EIP712.test.js +++ b/test/utils/cryptography/EIP712.test.js @@ -98,6 +98,14 @@ contract('EIP712', function (accounts) { await this.eip712.verify(signature, wallet.getAddressString(), message.to, message.contents); }); + + it('name', async function () { + expect(await this.eip712.$_EIP712Name()).to.be.equal(name); + }); + + it('version', async function () { + expect(await this.eip712.$_EIP712Version()).to.be.equal(version); + }); }); } }); From 7cc2cbfeb56a00bfe055ebe0d27da902d95ed3d3 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 14 Jun 2023 16:00:38 +0200 Subject: [PATCH 169/197] Cleanup the structure of GovernorTimelockControl.test.js (#4302) --- .../GovernorTimelockControl.test.js | 552 +++++++++--------- 1 file changed, 261 insertions(+), 291 deletions(-) diff --git a/test/governance/extensions/GovernorTimelockControl.test.js b/test/governance/extensions/GovernorTimelockControl.test.js index ef32148fa..10a7f05b2 100644 --- a/test/governance/extensions/GovernorTimelockControl.test.js +++ b/test/governance/extensions/GovernorTimelockControl.test.js @@ -136,318 +136,261 @@ contract('GovernorTimelockControl', function (accounts) { await this.helper.propose(); await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); await this.helper.waitForDeadline(); - const txQueue = await this.helper.queue(); + await this.helper.queue(); + await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Queued, + proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ]); + }); + }); + + describe('on execute', function () { + it('if not queued', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(+1); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + + await expectRevertCustomError(this.helper.execute(), 'TimelockUnexpectedOperationState', [ + this.proposal.timelockid, + Enums.OperationState.Ready, + ]); + }); + + it('if too early', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + + await expectRevertCustomError(this.helper.execute(), 'TimelockUnexpectedOperationState', [ + this.proposal.timelockid, + Enums.OperationState.Ready, + ]); + }); + + it('if already executed', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { - id: this.proposal.timelockid, - }); - - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txExecute.tx, this.timelock, 'CallExecuted', { - id: this.proposal.timelockid, - }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + await this.helper.execute(); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Executed, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); }); - describe('should revert', function () { - describe('on queue', function () { - it('if already queued', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Queued, - proposalStatesToBitMap([Enums.ProposalState.Succeeded]), - ]); - }); - }); + it('if already executed by another proposer', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); - describe('on execute', function () { - it('if not queued', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(+1); + await this.timelock.executeBatch( + ...this.proposal.shortProposal.slice(0, 3), + '0x0', + this.proposal.shortProposal[3], + ); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Executed, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); + }); + }); + }); - await expectRevertCustomError(this.helper.execute(), 'TimelockUnexpectedOperationState', [ - this.proposal.timelockid, - Enums.OperationState.Ready, - ]); - }); + describe('cancel', function () { + it('cancel before queue prevents scheduling', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); - it('if too early', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); + 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.Queued); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Canceled, + proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ]); + }); - await expectRevertCustomError(this.helper.execute(), 'TimelockUnexpectedOperationState', [ - this.proposal.timelockid, - Enums.OperationState.Ready, - ]); - }); + it('cancel after queue prevents executing', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); - it('if already executed', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - await this.helper.execute(); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Executed, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); - }); + expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); - it('if already executed by another proposer', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Canceled, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); + }); - await this.timelock.executeBatch( - ...this.proposal.shortProposal.slice(0, 3), - '0x0', - this.proposal.shortProposal[3], - ); + it('cancel on timelock is reflected on governor', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Executed, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); - }); - }); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + + expectEvent(await this.timelock.cancel(this.proposal.timelockid, { from: owner }), 'Cancelled', { + id: this.proposal.timelockid, }); - describe('cancel', function () { - it('cancel before queue prevents scheduling', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + }); + }); - expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded]), - ]); - }); - - it('cancel after queue prevents executing', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - - expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ - this.proposal.id, - Enums.ProposalState.Canceled, - proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), - ]); - }); - - it('cancel on timelock is reflected on governor', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); - - expectEvent(await this.timelock.cancel(this.proposal.timelockid, { from: owner }), 'Cancelled', { - id: this.proposal.timelockid, - }); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - }); + describe('onlyGovernance', function () { + describe('relay', function () { + beforeEach(async function () { + await this.token.$_mint(this.mock.address, 1); }); - describe('onlyGovernance', function () { - describe('relay', function () { - beforeEach(async function () { - await this.token.$_mint(this.mock.address, 1); - }); - - it('is protected', async function () { - await expectRevertCustomError( - this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI(), { - from: owner, - }), - 'GovernorOnlyExecutor', - [owner], - ); - }); - - it('can be executed through governance', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods - .relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()) - .encodeABI(), - }, - ], - '', - ); - - expect(await this.token.balanceOf(this.mock.address), 1); - expect(await this.token.balanceOf(other), 0); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - expect(await this.token.balanceOf(this.mock.address), 0); - expect(await this.token.balanceOf(other), 1); - - await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { - from: this.mock.address, - to: other, - value: '1', - }); - }); - - it('is payable and can transfer eth to EOA', async function () { - const t2g = web3.utils.toBN(128); // timelock to governor - const g2o = web3.utils.toBN(100); // governor to eoa (other) - - this.helper.setProposal( - [ - { - target: this.mock.address, - value: t2g, - data: this.mock.contract.methods.relay(other, g2o, '0x').encodeABI(), - }, - ], - '', - ); - - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - const timelockBalance = await web3.eth.getBalance(this.timelock.address).then(web3.utils.toBN); - const otherBalance = await web3.eth.getBalance(other).then(web3.utils.toBN); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - await this.helper.execute(); - - expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal( - timelockBalance.sub(t2g), - ); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(t2g.sub(g2o)); - expect(await web3.eth.getBalance(other)).to.be.bignumber.equal(otherBalance.add(g2o)); - }); - - it('protected against other proposers', async function () { - const target = this.mock.address; - const value = web3.utils.toWei('0'); - const data = this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(); - const predecessor = constants.ZERO_BYTES32; - const salt = constants.ZERO_BYTES32; - const delay = 3600; - - await this.timelock.schedule(target, value, data, predecessor, salt, delay, { from: owner }); - - await time.increase(3600); - - await expectRevertCustomError( - this.timelock.execute(target, value, data, predecessor, salt, { from: owner }), - 'QueueEmpty', // Bubbled up from Governor - [], - ); - }); - }); - - describe('updateTimelock', function () { - beforeEach(async function () { - this.newTimelock = await Timelock.new( - 3600, - [this.mock.address], - [this.mock.address], - constants.ZERO_ADDRESS, - ); - }); - - it('is protected', async function () { - await expectRevertCustomError( - this.mock.updateTimelock(this.newTimelock.address, { from: owner }), - 'GovernorOnlyExecutor', - [owner], - ); - }); - - it('can be executed through governance to', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - expectEvent(txExecute, 'TimelockChange', { - oldTimelock: this.timelock.address, - newTimelock: this.newTimelock.address, - }); - - expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); - }); - }); + it('is protected', async function () { + await expectRevertCustomError( + this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI(), { + from: owner, + }), + 'GovernorOnlyExecutor', + [owner], + ); }); - it('clear queue of pending governor calls', async function () { + it('can be executed through governance', async function () { this.helper.setProposal( [ { target: this.mock.address, - data: this.mock.contract.methods.nonGovernanceFunction().encodeABI(), + data: this.mock.contract.methods + .relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()) + .encodeABI(), + }, + ], + '', + ); + + expect(await this.token.balanceOf(this.mock.address), 1); + expect(await this.token.balanceOf(other), 0); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + expect(await this.token.balanceOf(this.mock.address), 0); + expect(await this.token.balanceOf(other), 1); + + await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { + from: this.mock.address, + to: other, + value: '1', + }); + }); + + it('is payable and can transfer eth to EOA', async function () { + const t2g = web3.utils.toBN(128); // timelock to governor + const g2o = web3.utils.toBN(100); // governor to eoa (other) + + this.helper.setProposal( + [ + { + target: this.mock.address, + value: t2g, + data: this.mock.contract.methods.relay(other, g2o, '0x').encodeABI(), + }, + ], + '', + ); + + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); + const timelockBalance = await web3.eth.getBalance(this.timelock.address).then(web3.utils.toBN); + const otherBalance = await web3.eth.getBalance(other).then(web3.utils.toBN); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + await this.helper.execute(); + + expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(timelockBalance.sub(t2g)); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(t2g.sub(g2o)); + expect(await web3.eth.getBalance(other)).to.be.bignumber.equal(otherBalance.add(g2o)); + }); + + it('protected against other proposers', async function () { + const target = this.mock.address; + const value = web3.utils.toWei('0'); + const data = this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(); + const predecessor = constants.ZERO_BYTES32; + const salt = constants.ZERO_BYTES32; + const delay = 3600; + + await this.timelock.schedule(target, value, data, predecessor, salt, delay, { from: owner }); + + await time.increase(3600); + + await expectRevertCustomError( + this.timelock.execute(target, value, data, predecessor, salt, { from: owner }), + 'QueueEmpty', // Bubbled up from Governor + [], + ); + }); + }); + + describe('updateTimelock', function () { + beforeEach(async function () { + this.newTimelock = await Timelock.new( + 3600, + [this.mock.address], + [this.mock.address], + constants.ZERO_ADDRESS, + ); + }); + + it('is protected', async function () { + await expectRevertCustomError( + this.mock.updateTimelock(this.newTimelock.address, { from: owner }), + 'GovernorOnlyExecutor', + [owner], + ); + }); + + it('can be executed through governance to', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(), }, ], '', @@ -459,14 +402,41 @@ contract('GovernorTimelockControl', function (accounts) { await this.helper.waitForDeadline(); await this.helper.queue(); await this.helper.waitForEta(); - await this.helper.execute(); + const txExecute = await this.helper.execute(); - // This path clears _governanceCall as part of the afterExecute call, - // but we have not way to check that the cleanup actually happened other - // then coverage reports. + expectEvent(txExecute, 'TimelockChange', { + oldTimelock: this.timelock.address, + newTimelock: this.newTimelock.address, + }); + + expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); }); }); }); + + it('clear queue of pending governor calls', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.nonGovernanceFunction().encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + await this.helper.execute(); + + // This path clears _governanceCall as part of the afterExecute call, + // but we have not way to check that the cleanup actually happened other + // then coverage reports. + }); }); } }); From 5cc1ea0a390d18490d99922242d3633a6b43653a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 14 Jun 2023 13:01:33 -0600 Subject: [PATCH 170/197] Add `GUIDELINES.md` for marking `abstract` contracts (#4010) Co-authored-by: Hadrien Croubois Co-authored-by: Francisco Giordano --- GUIDELINES.md | 7 +++++++ contracts/token/ERC1155/ERC1155.sol | 2 +- contracts/token/ERC1155/utils/ERC1155Holder.sol | 2 +- contracts/token/ERC20/ERC20.sol | 2 +- contracts/token/ERC721/ERC721.sol | 2 +- contracts/token/ERC721/utils/ERC721Holder.sol | 2 +- test/token/ERC1155/utils/ERC1155Holder.test.js | 2 +- test/token/ERC721/utils/ERC721Holder.test.js | 2 +- 8 files changed, 14 insertions(+), 7 deletions(-) diff --git a/GUIDELINES.md b/GUIDELINES.md index 1dd606ddd..71f166405 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -114,6 +114,13 @@ In addition to the official Solidity Style Guide we have a number of other conve interface IERC777 { ``` +* Contracts not intended to be used standalone should be marked abstract + so they are required to be inherited to other contracts. + + ```solidity + abstract contract AccessControl is ..., { + ``` + * Unchecked arithmetic blocks should contain comments explaining why overflow is guaranteed not to happen. If the reason is immediately apparent from the line above the unchecked block, the comment may be omitted. * Custom errors should be declared following the [EIP-6093](https://eips.ethereum.org/EIPS/eip-6093) rationale whenever reasonable. Also, consider the following: diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol index 05bcc3fa1..610868489 100644 --- a/contracts/token/ERC1155/ERC1155.sol +++ b/contracts/token/ERC1155/ERC1155.sol @@ -17,7 +17,7 @@ import "../../interfaces/draft-IERC6093.sol"; * * _Available since v3.1._ */ -contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IERC1155Errors { +abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IERC1155Errors { // Mapping from token ID to account balances mapping(uint256 => mapping(address => uint256)) private _balances; diff --git a/contracts/token/ERC1155/utils/ERC1155Holder.sol b/contracts/token/ERC1155/utils/ERC1155Holder.sol index b06a37f18..d24a1a53a 100644 --- a/contracts/token/ERC1155/utils/ERC1155Holder.sol +++ b/contracts/token/ERC1155/utils/ERC1155Holder.sol @@ -13,7 +13,7 @@ import "./ERC1155Receiver.sol"; * * @dev _Available since v3.1._ */ -contract ERC1155Holder is ERC1155Receiver { +abstract contract ERC1155Holder is ERC1155Receiver { function onERC1155Received( address, address, diff --git a/contracts/token/ERC20/ERC20.sol b/contracts/token/ERC20/ERC20.sol index dd46c15dd..aacfe79b7 100644 --- a/contracts/token/ERC20/ERC20.sol +++ b/contracts/token/ERC20/ERC20.sol @@ -35,7 +35,7 @@ import "../../interfaces/draft-IERC6093.sol"; * functions have been added to mitigate the well-known issues around setting * allowances. See {IERC20-approve}. */ -contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { +abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { mapping(address => uint256) private _balances; mapping(address => mapping(address => uint256)) private _allowances; diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 08f660a29..a26255c7c 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -16,7 +16,7 @@ import "../../interfaces/draft-IERC6093.sol"; * the Metadata extension, but not including the Enumerable extension, which is available separately as * {ERC721Enumerable}. */ -contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Errors { +abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Errors { using Strings for uint256; // Token name diff --git a/contracts/token/ERC721/utils/ERC721Holder.sol b/contracts/token/ERC721/utils/ERC721Holder.sol index 9f5b2e9f9..a3ee8b5f0 100644 --- a/contracts/token/ERC721/utils/ERC721Holder.sol +++ b/contracts/token/ERC721/utils/ERC721Holder.sol @@ -11,7 +11,7 @@ import "../IERC721Receiver.sol"; * Accepts all token transfers. * Make sure the contract is able to use its token with {IERC721-safeTransferFrom}, {IERC721-approve} or {IERC721-setApprovalForAll}. */ -contract ERC721Holder is IERC721Receiver { +abstract contract ERC721Holder is IERC721Receiver { /** * @dev See {IERC721Receiver-onERC721Received}. * diff --git a/test/token/ERC1155/utils/ERC1155Holder.test.js b/test/token/ERC1155/utils/ERC1155Holder.test.js index 864e89b50..8d8541640 100644 --- a/test/token/ERC1155/utils/ERC1155Holder.test.js +++ b/test/token/ERC1155/utils/ERC1155Holder.test.js @@ -1,6 +1,6 @@ const { BN } = require('@openzeppelin/test-helpers'); -const ERC1155Holder = artifacts.require('ERC1155Holder'); +const ERC1155Holder = artifacts.require('$ERC1155Holder'); const ERC1155 = artifacts.require('$ERC1155'); const { expect } = require('chai'); diff --git a/test/token/ERC721/utils/ERC721Holder.test.js b/test/token/ERC721/utils/ERC721Holder.test.js index 0fd822280..4aa2b7948 100644 --- a/test/token/ERC721/utils/ERC721Holder.test.js +++ b/test/token/ERC721/utils/ERC721Holder.test.js @@ -1,6 +1,6 @@ const { expect } = require('chai'); -const ERC721Holder = artifacts.require('ERC721Holder'); +const ERC721Holder = artifacts.require('$ERC721Holder'); const ERC721 = artifacts.require('$ERC721'); contract('ERC721Holder', function (accounts) { From ef103f37e4eb18514f0d1d8511c38e9fffe2afce Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 14 Jun 2023 21:11:12 +0200 Subject: [PATCH 171/197] Replace some uses of abi.encodePacked with more explicit alternatives (#4296) Co-authored-by: Francisco --- .changeset/flat-bottles-wonder.md | 5 +++++ .../governance/compatibility/GovernorCompatibilityBravo.sol | 2 +- contracts/mocks/ReentrancyAttack.sol | 4 ++-- contracts/mocks/ReentrancyMock.sol | 3 +-- contracts/token/ERC1155/extensions/ERC1155URIStorage.sol | 4 ++-- contracts/token/ERC721/ERC721.sol | 2 +- contracts/token/ERC721/extensions/ERC721URIStorage.sol | 4 ++-- contracts/utils/Strings.sol | 2 +- 8 files changed, 15 insertions(+), 11 deletions(-) create mode 100644 .changeset/flat-bottles-wonder.md diff --git a/.changeset/flat-bottles-wonder.md b/.changeset/flat-bottles-wonder.md new file mode 100644 index 000000000..099ea8339 --- /dev/null +++ b/.changeset/flat-bottles-wonder.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +Replace some uses of `abi.encodePacked` with clearer alternatives (e.g. `bytes.concat`, `string.concat`). diff --git a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol index 670be9591..8d6d9ccb4 100644 --- a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol @@ -155,7 +155,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp for (uint256 i = 0; i < fullcalldatas.length; ++i) { fullcalldatas[i] = bytes(signatures[i]).length == 0 ? calldatas[i] - : abi.encodePacked(bytes4(keccak256(bytes(signatures[i]))), calldatas[i]); + : bytes.concat(abi.encodeWithSignature(signatures[i]), calldatas[i]); } return fullcalldatas; diff --git a/contracts/mocks/ReentrancyAttack.sol b/contracts/mocks/ReentrancyAttack.sol index 2da8b1f1a..df2924301 100644 --- a/contracts/mocks/ReentrancyAttack.sol +++ b/contracts/mocks/ReentrancyAttack.sol @@ -5,8 +5,8 @@ pragma solidity ^0.8.19; import "../utils/Context.sol"; contract ReentrancyAttack is Context { - function callSender(bytes4 data) public { - (bool success, ) = _msgSender().call(abi.encodeWithSelector(data)); + function callSender(bytes calldata data) public { + (bool success, ) = _msgSender().call(data); require(success, "ReentrancyAttack: failed call"); } } diff --git a/contracts/mocks/ReentrancyMock.sol b/contracts/mocks/ReentrancyMock.sol index b4819dd59..053e53d77 100644 --- a/contracts/mocks/ReentrancyMock.sol +++ b/contracts/mocks/ReentrancyMock.sol @@ -33,8 +33,7 @@ contract ReentrancyMock is ReentrancyGuard { function countAndCall(ReentrancyAttack attacker) public nonReentrant { _count(); - bytes4 func = bytes4(keccak256("callback()")); - attacker.callSender(func); + attacker.callSender(abi.encodeCall(this.callback, ())); } function _count() private { diff --git a/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol b/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol index 5b0da2b33..79782a42b 100644 --- a/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol +++ b/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol @@ -42,8 +42,8 @@ abstract contract ERC1155URIStorage is ERC1155 { function uri(uint256 tokenId) public view virtual override returns (string memory) { string memory tokenURI = _tokenURIs[tokenId]; - // If token URI is set, concatenate base URI and tokenURI (via abi.encodePacked). - return bytes(tokenURI).length > 0 ? string(abi.encodePacked(_baseURI, tokenURI)) : super.uri(tokenId); + // If token URI is set, concatenate base URI and tokenURI (via string.concat). + return bytes(tokenURI).length > 0 ? string.concat(_baseURI, tokenURI) : super.uri(tokenId); } /** diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index a26255c7c..21ed95813 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -97,7 +97,7 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er _requireMinted(tokenId); string memory baseURI = _baseURI(); - return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ""; + return bytes(baseURI).length > 0 ? string.concat(baseURI, tokenId.toString()) : ""; } /** diff --git a/contracts/token/ERC721/extensions/ERC721URIStorage.sol b/contracts/token/ERC721/extensions/ERC721URIStorage.sol index ae058122d..ae625fc28 100644 --- a/contracts/token/ERC721/extensions/ERC721URIStorage.sol +++ b/contracts/token/ERC721/extensions/ERC721URIStorage.sol @@ -35,9 +35,9 @@ abstract contract ERC721URIStorage is IERC4906, ERC721 { if (bytes(base).length == 0) { return _tokenURI; } - // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked). + // If both are set, concatenate the baseURI and tokenURI (via string.concat). if (bytes(_tokenURI).length > 0) { - return string(abi.encodePacked(base, _tokenURI)); + return string.concat(base, _tokenURI); } return super.tokenURI(tokenId); diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 65c8c8753..574717f86 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -47,7 +47,7 @@ library Strings { * @dev Converts a `int256` to its ASCII `string` decimal representation. */ function toString(int256 value) internal pure returns (string memory) { - return string(abi.encodePacked(value < 0 ? "-" : "", toString(SignedMath.abs(value)))); + return string.concat(value < 0 ? "-" : "", toString(SignedMath.abs(value))); } /** From ac5480e7caa8544da5ca629524bf11ea9af2751a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 14 Jun 2023 14:11:37 -0600 Subject: [PATCH 172/197] Merge release-v4.9 branch (#4352) --- CHANGELOG.md | 4 ++++ contracts/governance/Governor.sol | 2 +- contracts/package.json | 2 +- package.json | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d4e13046..abbb1b347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,10 @@ function supportsInterface(bytes4 interfaceId) public view virtual override retu } ``` +## 4.9.1 (2023-06-07) + +- `Governor`: Add a mechanism to restrict the address of the proposer using a suffix in the description. + ## 4.9.0 (2023-05-23) - `ReentrancyGuard`: Add a `_reentrancyGuardEntered` function to expose the guard status. ([#3714](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3714)) diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index b42fe1034..83bbfb083 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.9.0) (governance/Governor.sol) +// OpenZeppelin Contracts (last updated v4.9.1) (governance/Governor.sol) pragma solidity ^0.8.19; diff --git a/contracts/package.json b/contracts/package.json index 4d0f576bb..4711a6b3f 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,7 +1,7 @@ { "name": "@openzeppelin/contracts", "description": "Secure Smart Contract library for Solidity", - "version": "4.9.0", + "version": "4.9.1", "files": [ "**/*.sol", "/build/contracts/*.json", diff --git a/package.json b/package.json index c070915f9..c44154133 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openzeppelin-solidity", "description": "Secure Smart Contract library for Solidity", - "version": "4.9.0", + "version": "4.9.1", "files": [ "/contracts/**/*.sol", "/build/contracts/*.json", From 247753426034263e07ef5dc7f0cf3483fb6c53b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 14 Jun 2023 14:21:42 -0600 Subject: [PATCH 173/197] Change behavior of ceilDiv(0, 0) and improve test coverage (#4348) --- .changeset/blue-scissors-design.md | 5 ++ contracts/mocks/token/ERC20Reentrant.sol | 2 +- contracts/mocks/token/ERC4626LimitsMock.sol | 23 ++++++ contracts/utils/math/Math.sol | 5 ++ test/token/ERC1155/ERC1155.behavior.js | 8 ++ test/token/ERC20/extensions/ERC4626.test.js | 74 +++++++++++++++++++ .../ERC721/extensions/ERC721Burnable.test.js | 11 ++- .../extensions/ERC721Consecutive.test.js | 19 +++++ test/utils/math/Math.test.js | 18 +++++ test/utils/structs/Checkpoints.test.js | 15 ++++ 10 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 .changeset/blue-scissors-design.md create mode 100644 contracts/mocks/token/ERC4626LimitsMock.sol diff --git a/.changeset/blue-scissors-design.md b/.changeset/blue-scissors-design.md new file mode 100644 index 000000000..c2f815aae --- /dev/null +++ b/.changeset/blue-scissors-design.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Math`: Make `ceilDiv` to revert on 0 division even if the numerator is 0 diff --git a/contracts/mocks/token/ERC20Reentrant.sol b/contracts/mocks/token/ERC20Reentrant.sol index ee803b9e1..00ba74260 100644 --- a/contracts/mocks/token/ERC20Reentrant.sol +++ b/contracts/mocks/token/ERC20Reentrant.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import "../../token/ERC20/ERC20.sol"; -import "../../token/ERC20/extensions/ERC4626.sol"; +import "../../utils/Address.sol"; contract ERC20Reentrant is ERC20("TEST", "TST") { enum Type { diff --git a/contracts/mocks/token/ERC4626LimitsMock.sol b/contracts/mocks/token/ERC4626LimitsMock.sol new file mode 100644 index 000000000..a47826bb8 --- /dev/null +++ b/contracts/mocks/token/ERC4626LimitsMock.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../../token/ERC20/extensions/ERC4626.sol"; + +abstract contract ERC4626LimitsMock is ERC4626 { + uint256 _maxDeposit; + uint256 _maxMint; + + constructor() { + _maxDeposit = 100 ether; + _maxMint = 100 ether; + } + + function maxDeposit(address) public view override returns (uint256) { + return _maxDeposit; + } + + function maxMint(address) public view override returns (uint256) { + return _maxMint; + } +} diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index a2fc7ceb7..d372295d7 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -114,6 +114,11 @@ library Math { * of rounding down. */ function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + if (b == 0) { + // Guarantee the same behavior as in a regular Solidity division. + return a / b; + } + // (a + b - 1) / b can overflow on addition, so we distribute. return a == 0 ? 0 : (a - 1) / b + 1; } diff --git a/test/token/ERC1155/ERC1155.behavior.js b/test/token/ERC1155/ERC1155.behavior.js index 5e87f8d52..1693c4aa6 100644 --- a/test/token/ERC1155/ERC1155.behavior.js +++ b/test/token/ERC1155/ERC1155.behavior.js @@ -479,6 +479,14 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m ); }); + it('reverts when transferring from zero address', async function () { + await expectRevertCustomError( + this.token.$_safeBatchTransferFrom(ZERO_ADDRESS, multiTokenHolder, [firstTokenId], [firstAmount], '0x'), + 'ERC1155InvalidSender', + [ZERO_ADDRESS], + ); + }); + function batchTransferWasSuccessful({ operator, from, ids, values }) { it('debits transferred balances from sender', async function () { const newBalances = await this.token.balanceOfBatch(new Array(ids.length).fill(from), ids); diff --git a/test/token/ERC20/extensions/ERC4626.test.js b/test/token/ERC20/extensions/ERC4626.test.js index d67486a60..99d6009e4 100644 --- a/test/token/ERC20/extensions/ERC4626.test.js +++ b/test/token/ERC20/extensions/ERC4626.test.js @@ -6,6 +6,7 @@ const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC20Decimals = artifacts.require('$ERC20DecimalsMock'); const ERC4626 = artifacts.require('$ERC4626'); +const ERC4626LimitsMock = artifacts.require('$ERC4626LimitsMock'); const ERC4626OffsetMock = artifacts.require('$ERC4626OffsetMock'); const ERC4626FeesMock = artifacts.require('$ERC4626FeesMock'); const ERC20ExcessDecimalsMock = artifacts.require('ERC20ExcessDecimalsMock'); @@ -220,6 +221,49 @@ contract('ERC4626', function (accounts) { }); }); + describe('limits', async function () { + beforeEach(async function () { + this.token = await ERC20Decimals.new(name, symbol, decimals); + this.vault = await ERC4626LimitsMock.new(name + ' Vault', symbol + 'V', this.token.address); + }); + + it('reverts on deposit() above max deposit', async function () { + const maxDeposit = await this.vault.maxDeposit(holder); + await expectRevertCustomError(this.vault.deposit(maxDeposit.addn(1), recipient), 'ERC4626ExceededMaxDeposit', [ + recipient, + maxDeposit.addn(1), + maxDeposit, + ]); + }); + + it('reverts on mint() above max mint', async function () { + const maxMint = await this.vault.maxMint(holder); + await expectRevertCustomError(this.vault.mint(maxMint.addn(1), recipient), 'ERC4626ExceededMaxMint', [ + recipient, + maxMint.addn(1), + maxMint, + ]); + }); + + it('reverts on withdraw() above max withdraw', async function () { + const maxWithdraw = await this.vault.maxWithdraw(holder); + await expectRevertCustomError( + this.vault.withdraw(maxWithdraw.addn(1), recipient, holder), + 'ERC4626ExceededMaxWithdraw', + [holder, maxWithdraw.addn(1), maxWithdraw], + ); + }); + + it('reverts on redeem() above max redeem', async function () { + const maxRedeem = await this.vault.maxRedeem(holder); + await expectRevertCustomError( + this.vault.redeem(maxRedeem.addn(1), recipient, holder), + 'ERC4626ExceededMaxRedeem', + [holder, maxRedeem.addn(1), maxRedeem], + ); + }); + }); + for (const offset of [0, 6, 18].map(web3.utils.toBN)) { const parseToken = token => web3.utils.toBN(10).pow(decimals).muln(token); const parseShare = share => web3.utils.toBN(10).pow(decimals.add(offset)).muln(share); @@ -849,6 +893,9 @@ contract('ERC4626', function (accounts) { expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('0'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2000'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0'); + expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '2000', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('2000'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('2000'); } @@ -872,6 +919,9 @@ contract('ERC4626', function (accounts) { expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2000'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('4000'); + expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '6000', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('6000'); } @@ -883,6 +933,9 @@ contract('ERC4626', function (accounts) { expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2999'); // used to be 3000, but virtual assets/shares captures part of the yield expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('5999'); // used to be 6000, but virtual assets/shares captures part of the yield + expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '6000', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('9000'); @@ -904,6 +957,9 @@ contract('ERC4626', function (accounts) { expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('4999'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('6000'); + expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '7333', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('7333'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('11000'); } @@ -928,6 +984,9 @@ contract('ERC4626', function (accounts) { expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('4999'); // used to be 5000 expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('9000'); + expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '9333', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('14000'); // used to be 14001 } @@ -940,6 +999,9 @@ contract('ERC4626', function (accounts) { expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('6070'); // used to be 6071 expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10928'); // used to be 10929 + expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '9333', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('17000'); // used to be 17001 @@ -961,6 +1023,9 @@ contract('ERC4626', function (accounts) { expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3643'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10929'); + expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '8000', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('8000'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('14573'); } @@ -983,6 +1048,9 @@ contract('ERC4626', function (accounts) { expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3643'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8000'); + expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '6392', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('6392'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('11644'); } @@ -1006,6 +1074,9 @@ contract('ERC4626', function (accounts) { expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8000'); // used to be 8001 + expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '4392', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('4392'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('8001'); } @@ -1028,6 +1099,9 @@ contract('ERC4626', function (accounts) { expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('0'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0'); + expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '0', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('0'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('1'); // used to be 0 } diff --git a/test/token/ERC721/extensions/ERC721Burnable.test.js b/test/token/ERC721/extensions/ERC721Burnable.test.js index c6c076919..df059e090 100644 --- a/test/token/ERC721/extensions/ERC721Burnable.test.js +++ b/test/token/ERC721/extensions/ERC721Burnable.test.js @@ -6,7 +6,7 @@ const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC721Burnable = artifacts.require('$ERC721Burnable'); contract('ERC721Burnable', function (accounts) { - const [owner, approved] = accounts; + const [owner, approved, another] = accounts; const firstTokenId = new BN(1); const secondTokenId = new BN(2); @@ -61,6 +61,15 @@ contract('ERC721Burnable', function (accounts) { }); }); + describe('when there is no previous approval burned', function () { + it('reverts', async function () { + await expectRevertCustomError(this.token.burn(tokenId, { from: another }), 'ERC721InsufficientApproval', [ + another, + tokenId, + ]); + }); + }); + describe('when the given token ID was not tracked by this contract', function () { it('reverts', async function () { await expectRevertCustomError(this.token.burn(unknownTokenId, { from: owner }), 'ERC721NonexistentToken', [ diff --git a/test/token/ERC721/extensions/ERC721Consecutive.test.js b/test/token/ERC721/extensions/ERC721Consecutive.test.js index 172da86a6..55c26dffe 100644 --- a/test/token/ERC721/extensions/ERC721Consecutive.test.js +++ b/test/token/ERC721/extensions/ERC721Consecutive.test.js @@ -85,6 +85,14 @@ contract('ERC721Consecutive', function (accounts) { expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(balance)); } }); + + it('reverts on consecutive minting to the zero address', async function () { + await expectRevertCustomError( + ERC721ConsecutiveMock.new(name, symbol, offset, delegates, [ZERO_ADDRESS], [10]), + 'ERC721InvalidReceiver', + [ZERO_ADDRESS], + ); + }); }); describe('minting after construction', function () { @@ -172,6 +180,17 @@ contract('ERC721Consecutive', function (accounts) { expect(await this.token.$_exists(tokenId)).to.be.equal(true); expect(await this.token.ownerOf(tokenId), user2); }); + + it('reverts burning batches of size != 1', async function () { + const tokenId = batches[0].amount + offset; + const receiver = batches[0].receiver; + + await expectRevertCustomError( + this.token.$_afterTokenTransfer(receiver, ZERO_ADDRESS, tokenId, 2), + 'ERC721ForbiddenBatchBurn', + [], + ); + }); }); }); } diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index df459c5f8..afd822b17 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -2,6 +2,7 @@ const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { MAX_UINT256 } = constants; const { Rounding } = require('../../helpers/enums.js'); +const { expectRevertCustomError } = require('../../helpers/customError.js'); const Math = artifacts.require('$Math'); @@ -204,6 +205,19 @@ contract('Math', function () { }); describe('ceilDiv', function () { + it('reverts on zero division', async function () { + const a = new BN('2'); + const b = new BN('0'); + // It's unspecified because it's a low level 0 division error + await expectRevert.unspecified(this.math.$ceilDiv(a, b)); + }); + + it('does not round up a zero result', async function () { + const a = new BN('0'); + const b = new BN('2'); + expect(await this.math.$ceilDiv(a, b)).to.be.bignumber.equal('0'); + }); + it('does not round up on exact division', async function () { const a = new BN('10'); const b = new BN('5'); @@ -233,6 +247,10 @@ contract('Math', function () { await expectRevert.unspecified(this.math.$mulDiv(1, 1, 0, Rounding.Down)); }); + it('reverts with result higher than 2 ^ 256', async function () { + await expectRevertCustomError(this.math.$mulDiv(5, MAX_UINT256, 2, Rounding.Down), 'MathOverflowedMulDiv', []); + }); + describe('does round down', async function () { it('small values', async function () { expect(await this.math.$mulDiv('3', '4', '5', Rounding.Down)).to.be.bignumber.equal('2'); diff --git a/test/utils/structs/Checkpoints.test.js b/test/utils/structs/Checkpoints.test.js index d82180127..936ac565a 100644 --- a/test/utils/structs/Checkpoints.test.js +++ b/test/utils/structs/Checkpoints.test.js @@ -4,6 +4,7 @@ const { expect } = require('chai'); const { VALUE_SIZES } = require('../../../scripts/generate/templates/Checkpoints.opts.js'); const { expectRevertCustomError } = require('../../helpers/customError.js'); +const { expectRevert } = require('@openzeppelin/test-helpers'); const $Checkpoints = artifacts.require('$Checkpoints'); @@ -22,6 +23,7 @@ contract('Checkpoints', function () { describe(`Trace${length}`, function () { beforeEach(async function () { this.methods = { + at: (...args) => this.mock.methods[`$at_${libraryName}_Trace${length}(uint256,uint32)`](0, ...args), latest: (...args) => this.mock.methods[`$latest_${libraryName}_Trace${length}(uint256)`](0, ...args), latestCheckpoint: (...args) => this.mock.methods[`$latestCheckpoint_${libraryName}_Trace${length}(uint256)`](0, ...args), @@ -35,6 +37,11 @@ contract('Checkpoints', function () { }); describe('without checkpoints', function () { + it('at zero reverts', async function () { + // Reverts with array out of bound access, which is unspecified + await expectRevert.unspecified(this.methods.at(0)); + }); + it('returns zero as latest value', async function () { expect(await this.methods.latest()).to.be.bignumber.equal('0'); @@ -65,6 +72,14 @@ contract('Checkpoints', function () { } }); + it('at keys', async function () { + for (const [index, { key, value }] of this.checkpoints.entries()) { + const at = await this.methods.at(index); + expect(at._value).to.be.bignumber.equal(value); + expect(at._key).to.be.bignumber.equal(key); + } + }); + it('length', async function () { expect(await this.methods.length()).to.be.bignumber.equal(this.checkpoints.length.toString()); }); From 05ef6924ac7e5db711c9bd588479c559bd383aba Mon Sep 17 00:00:00 2001 From: Claudia Barcelo Date: Thu, 15 Jun 2023 04:39:34 +0200 Subject: [PATCH 174/197] Optimize array access in ERC1155 (#4300) Co-authored-by: Hadrien Croubois Co-authored-by: Francisco Giordano --- .changeset/hot-coins-judge.md | 5 +++++ .changeset/tough-drinks-hammer.md | 5 +++++ contracts/token/ERC1155/ERC1155.sol | 14 +++++++++----- contracts/utils/Arrays.sol | 22 ++++++++++++++++++++++ 4 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 .changeset/hot-coins-judge.md create mode 100644 .changeset/tough-drinks-hammer.md diff --git a/.changeset/hot-coins-judge.md b/.changeset/hot-coins-judge.md new file mode 100644 index 000000000..e544af467 --- /dev/null +++ b/.changeset/hot-coins-judge.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Arrays`: Add `unsafeMemoryAccess` helpers to read from a memory array without checking the length. diff --git a/.changeset/tough-drinks-hammer.md b/.changeset/tough-drinks-hammer.md new file mode 100644 index 000000000..51b3836e4 --- /dev/null +++ b/.changeset/tough-drinks-hammer.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ERC1155`: Optimize array accesses by skipping bounds checking when unnecessary. diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol index 610868489..5369d7732 100644 --- a/contracts/token/ERC1155/ERC1155.sol +++ b/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 "../../utils/Arrays.sol"; import "../../interfaces/draft-IERC6093.sol"; /** @@ -18,6 +19,9 @@ import "../../interfaces/draft-IERC6093.sol"; * _Available since v3.1._ */ abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IERC1155Errors { + using Arrays for uint256[]; + using Arrays for address[]; + // Mapping from token ID to account balances mapping(uint256 => mapping(address => uint256)) private _balances; @@ -87,7 +91,7 @@ abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IER uint256[] memory batchBalances = new uint256[](accounts.length); for (uint256 i = 0; i < accounts.length; ++i) { - batchBalances[i] = balanceOf(accounts[i], ids[i]); + batchBalances[i] = balanceOf(accounts.unsafeMemoryAccess(i), ids.unsafeMemoryAccess(i)); } return batchBalances; @@ -157,8 +161,8 @@ abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IER address operator = _msgSender(); for (uint256 i = 0; i < ids.length; ++i) { - uint256 id = ids[i]; - uint256 amount = amounts[i]; + uint256 id = ids.unsafeMemoryAccess(i); + uint256 amount = amounts.unsafeMemoryAccess(i); if (from != address(0)) { uint256 fromBalance = _balances[id][from]; @@ -176,8 +180,8 @@ abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IER } if (ids.length == 1) { - uint256 id = ids[0]; - uint256 amount = amounts[0]; + uint256 id = ids.unsafeMemoryAccess(0); + uint256 amount = amounts.unsafeMemoryAccess(0); emit TransferSingle(operator, from, to, id, amount); if (to != address(0)) { _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data); diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index 383a0820a..f4ef45645 100644 --- a/contracts/utils/Arrays.sol +++ b/contracts/utils/Arrays.sol @@ -102,4 +102,26 @@ library Arrays { } return slot.getUint256Slot(); } + + /** + * @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check. + * + * WARNING: Only use if you are certain `pos` is lower than the array length. + */ + function unsafeMemoryAccess(uint256[] memory arr, uint256 pos) internal pure returns (uint256 res) { + assembly { + res := mload(add(add(arr, 0x20), mul(pos, 0x20))) + } + } + + /** + * @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check. + * + * WARNING: Only use if you are certain `pos` is lower than the array length. + */ + function unsafeMemoryAccess(address[] memory arr, uint256 pos) internal pure returns (address res) { + assembly { + res := mload(add(add(arr, 0x20), mul(pos, 0x20))) + } + } } From ff85c7b0eb90c85f080a05b65e2b335104ff3923 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 15 Jun 2023 22:01:04 +0200 Subject: [PATCH 175/197] Make ERC1967Upgrades a library instead of an abstract contract (#4325) --- .changeset/grumpy-worms-tease.md | 5 ++ contracts/mocks/proxy/UUPSLegacy.sol | 15 ++-- contracts/mocks/proxy/UUPSUpgradeableMock.sol | 4 +- contracts/proxy/ERC1967/ERC1967Proxy.sol | 8 +- .../{ERC1967Upgrade.sol => ERC1967Utils.sol} | 80 ++++++++++++------- contracts/proxy/README.adoc | 4 +- contracts/proxy/beacon/BeaconProxy.sol | 8 +- .../TransparentUpgradeableProxy.sol | 10 +-- contracts/proxy/utils/UUPSUpgradeable.sol | 12 +-- hardhat.config.js | 2 +- scripts/upgradeable/transpile.sh | 2 +- 11 files changed, 89 insertions(+), 61 deletions(-) create mode 100644 .changeset/grumpy-worms-tease.md rename contracts/proxy/ERC1967/{ERC1967Upgrade.sol => ERC1967Utils.sol} (65%) diff --git a/.changeset/grumpy-worms-tease.md b/.changeset/grumpy-worms-tease.md new file mode 100644 index 000000000..910b996c6 --- /dev/null +++ b/.changeset/grumpy-worms-tease.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`ERC1967Utils`: Refactor the `ERC1967Upgrade` abstract contract as a library. diff --git a/contracts/mocks/proxy/UUPSLegacy.sol b/contracts/mocks/proxy/UUPSLegacy.sol index f8ea7214b..6812f39b7 100644 --- a/contracts/mocks/proxy/UUPSLegacy.sol +++ b/contracts/mocks/proxy/UUPSLegacy.sol @@ -7,18 +7,18 @@ import "./UUPSUpgradeableMock.sol"; // This contract implements the pre-4.5 UUPS upgrade function with a rollback test. // It's used to test that newer UUPS contracts are considered valid upgrades by older UUPS contracts. contract UUPSUpgradeableLegacyMock is UUPSUpgradeableMock { - // Inlined from ERC1967Upgrade + // Inlined from ERC1967Utils bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; - // ERC1967Upgrade._setImplementation is private so we reproduce it here. + // ERC1967Utils._setImplementation is private so we reproduce it here. // An extra underscore prevents a name clash error. function __setImplementation(address newImplementation) private { require(newImplementation.code.length > 0, "ERC1967: new implementation is not a contract"); - StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + StorageSlot.getAddressSlot(ERC1967Utils.IMPLEMENTATION_SLOT).value = newImplementation; } function _upgradeToAndCallSecureLegacyV1(address newImplementation, bytes memory data, bool forceCall) internal { - address oldImplementation = _getImplementation(); + address oldImplementation = ERC1967Utils.getImplementation(); // Initial upgrade and setup call __setImplementation(newImplementation); @@ -34,9 +34,12 @@ contract UUPSUpgradeableLegacyMock is UUPSUpgradeableMock { Address.functionDelegateCall(newImplementation, abi.encodeCall(this.upgradeTo, (oldImplementation))); rollbackTesting.value = false; // Check rollback was effective - require(oldImplementation == _getImplementation(), "ERC1967Upgrade: upgrade breaks further upgrades"); + require( + oldImplementation == ERC1967Utils.getImplementation(), + "ERC1967Utils: upgrade breaks further upgrades" + ); // Finally reset to the new implementation and log the upgrade - _upgradeTo(newImplementation); + ERC1967Utils.upgradeTo(newImplementation); } } diff --git a/contracts/mocks/proxy/UUPSUpgradeableMock.sol b/contracts/mocks/proxy/UUPSUpgradeableMock.sol index 60eed4c93..81af96490 100644 --- a/contracts/mocks/proxy/UUPSUpgradeableMock.sol +++ b/contracts/mocks/proxy/UUPSUpgradeableMock.sol @@ -23,10 +23,10 @@ contract UUPSUpgradeableMock is NonUpgradeableMock, UUPSUpgradeable { contract UUPSUpgradeableUnsafeMock is UUPSUpgradeableMock { function upgradeTo(address newImplementation) public override { - _upgradeToAndCall(newImplementation, bytes(""), false); + ERC1967Utils.upgradeToAndCall(newImplementation, bytes(""), false); } function upgradeToAndCall(address newImplementation, bytes memory data) public payable override { - _upgradeToAndCall(newImplementation, data, false); + ERC1967Utils.upgradeToAndCall(newImplementation, data, false); } } diff --git a/contracts/proxy/ERC1967/ERC1967Proxy.sol b/contracts/proxy/ERC1967/ERC1967Proxy.sol index ea5c204f8..5a752f13d 100644 --- a/contracts/proxy/ERC1967/ERC1967Proxy.sol +++ b/contracts/proxy/ERC1967/ERC1967Proxy.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.19; import "../Proxy.sol"; -import "./ERC1967Upgrade.sol"; +import "./ERC1967Utils.sol"; /** * @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an @@ -12,7 +12,7 @@ import "./ERC1967Upgrade.sol"; * https://eips.ethereum.org/EIPS/eip-1967[EIP1967], so that it doesn't conflict with the storage layout of the * implementation behind the proxy. */ -contract ERC1967Proxy is Proxy, ERC1967Upgrade { +contract ERC1967Proxy is Proxy { /** * @dev Initializes the upgradeable proxy with an initial implementation specified by `_logic`. * @@ -20,7 +20,7 @@ contract ERC1967Proxy is Proxy, ERC1967Upgrade { * function call, and allows initializing the storage of the proxy like a Solidity constructor. */ constructor(address _logic, bytes memory _data) payable { - _upgradeToAndCall(_logic, _data, false); + ERC1967Utils.upgradeToAndCall(_logic, _data, false); } /** @@ -31,6 +31,6 @@ contract ERC1967Proxy is Proxy, ERC1967Upgrade { * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` */ function _implementation() internal view virtual override returns (address impl) { - return _getImplementation(); + return ERC1967Utils.getImplementation(); } } diff --git a/contracts/proxy/ERC1967/ERC1967Upgrade.sol b/contracts/proxy/ERC1967/ERC1967Utils.sol similarity index 65% rename from contracts/proxy/ERC1967/ERC1967Upgrade.sol rename to contracts/proxy/ERC1967/ERC1967Utils.sol index ca6b92580..55f5c6aab 100644 --- a/contracts/proxy/ERC1967/ERC1967Upgrade.sol +++ b/contracts/proxy/ERC1967/ERC1967Utils.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.9.0) (proxy/ERC1967/ERC1967Upgrade.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/ERC1967/ERC1967Utils.sol) -pragma solidity ^0.8.19; +pragma solidity ^0.8.20; import "../beacon/IBeacon.sol"; import "../../interfaces/IERC1967.sol"; @@ -15,7 +15,24 @@ import "../../utils/StorageSlot.sol"; * * _Available since v4.1._ */ -abstract contract ERC1967Upgrade is IERC1967 { +library ERC1967Utils { + // We re-declare ERC-1967 events here because they can't be used directly from IERC1967. + // This will be fixed in Solidity 0.8.21. At that point we should remove these events. + /** + * @dev Emitted when the implementation is upgraded. + */ + event Upgraded(address indexed implementation); + + /** + * @dev Emitted when the admin account has changed. + */ + event AdminChanged(address previousAdmin, address newAdmin); + + /** + * @dev Emitted when the beacon is changed. + */ + event BeaconUpgraded(address indexed beacon); + // This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1 bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; @@ -24,7 +41,8 @@ abstract contract ERC1967Upgrade is IERC1967 { * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is * validated in the constructor. */ - bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + // solhint-disable-next-line private-vars-leading-underscore + bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; /** * @dev The `implementation` of the proxy is invalid. @@ -49,8 +67,8 @@ abstract contract ERC1967Upgrade is IERC1967 { /** * @dev Returns the current implementation address. */ - function _getImplementation() internal view returns (address) { - return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + function getImplementation() internal view returns (address) { + return StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value; } /** @@ -60,15 +78,15 @@ abstract contract ERC1967Upgrade is IERC1967 { if (newImplementation.code.length == 0) { revert ERC1967InvalidImplementation(newImplementation); } - StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = newImplementation; } /** * @dev Perform implementation upgrade * - * Emits an {Upgraded} event. + * Emits an {IERC1967-Upgraded} event. */ - function _upgradeTo(address newImplementation) internal { + function upgradeTo(address newImplementation) internal { _setImplementation(newImplementation); emit Upgraded(newImplementation); } @@ -76,10 +94,10 @@ abstract contract ERC1967Upgrade is IERC1967 { /** * @dev Perform implementation upgrade with additional setup call. * - * Emits an {Upgraded} event. + * Emits an {IERC1967-Upgraded} event. */ - function _upgradeToAndCall(address newImplementation, bytes memory data, bool forceCall) internal { - _upgradeTo(newImplementation); + function upgradeToAndCall(address newImplementation, bytes memory data, bool forceCall) internal { + upgradeTo(newImplementation); if (data.length > 0 || forceCall) { Address.functionDelegateCall(newImplementation, data); } @@ -88,9 +106,9 @@ abstract contract ERC1967Upgrade is IERC1967 { /** * @dev Perform implementation upgrade with security checks for UUPS proxies, and additional setup call. * - * Emits an {Upgraded} event. + * Emits an {IERC1967-Upgraded} event. */ - function _upgradeToAndCallUUPS(address newImplementation, bytes memory data, bool forceCall) internal { + function upgradeToAndCallUUPS(address newImplementation, bytes memory data, bool forceCall) internal { // Upgrades from old implementations will perform a rollback test. This test requires the new // implementation to upgrade back to the old, non-ERC1822 compliant, implementation. Removing // this special case will break upgrade paths from old UUPS implementation to new ones. @@ -98,14 +116,14 @@ abstract contract ERC1967Upgrade is IERC1967 { _setImplementation(newImplementation); } else { try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) { - if (slot != _IMPLEMENTATION_SLOT) { + if (slot != IMPLEMENTATION_SLOT) { revert ERC1967UnsupportedProxiableUUID(slot); } } catch { // The implementation is not UUPS revert ERC1967InvalidImplementation(newImplementation); } - _upgradeToAndCall(newImplementation, data, forceCall); + upgradeToAndCall(newImplementation, data, forceCall); } } @@ -114,7 +132,8 @@ abstract contract ERC1967Upgrade is IERC1967 { * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is * validated in the constructor. */ - bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + // solhint-disable-next-line private-vars-leading-underscore + bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; /** * @dev Returns the current admin. @@ -123,8 +142,8 @@ abstract contract ERC1967Upgrade is IERC1967 { * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` */ - function _getAdmin() internal view returns (address) { - return StorageSlot.getAddressSlot(_ADMIN_SLOT).value; + function getAdmin() internal view returns (address) { + return StorageSlot.getAddressSlot(ADMIN_SLOT).value; } /** @@ -134,30 +153,31 @@ abstract contract ERC1967Upgrade is IERC1967 { if (newAdmin == address(0)) { revert ERC1967InvalidAdmin(address(0)); } - StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin; + StorageSlot.getAddressSlot(ADMIN_SLOT).value = newAdmin; } /** * @dev Changes the admin of the proxy. * - * Emits an {AdminChanged} event. + * Emits an {IERC1967-AdminChanged} event. */ - function _changeAdmin(address newAdmin) internal { - emit AdminChanged(_getAdmin(), newAdmin); + function changeAdmin(address newAdmin) internal { + emit AdminChanged(getAdmin(), newAdmin); _setAdmin(newAdmin); } /** * @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy. - * This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)) and is validated in the constructor. + * This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1) and is validated in the constructor. */ - bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; + // solhint-disable-next-line private-vars-leading-underscore + bytes32 internal constant BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; /** * @dev Returns the current beacon. */ - function _getBeacon() internal view returns (address) { - return StorageSlot.getAddressSlot(_BEACON_SLOT).value; + function getBeacon() internal view returns (address) { + return StorageSlot.getAddressSlot(BEACON_SLOT).value; } /** @@ -173,16 +193,16 @@ abstract contract ERC1967Upgrade is IERC1967 { revert ERC1967InvalidImplementation(beaconImplementation); } - StorageSlot.getAddressSlot(_BEACON_SLOT).value = newBeacon; + StorageSlot.getAddressSlot(BEACON_SLOT).value = newBeacon; } /** * @dev Perform beacon upgrade with additional setup call. Note: This upgrades the address of the beacon, it does * not upgrade the implementation contained in the beacon (see {UpgradeableBeacon-_setImplementation} for that). * - * Emits a {BeaconUpgraded} event. + * Emits an {IERC1967-BeaconUpgraded} event. */ - function _upgradeBeaconToAndCall(address newBeacon, bytes memory data, bool forceCall) internal { + function upgradeBeaconToAndCall(address newBeacon, bytes memory data, bool forceCall) internal { _setBeacon(newBeacon); emit BeaconUpgraded(newBeacon); if (data.length > 0 || forceCall) { diff --git a/contracts/proxy/README.adoc b/contracts/proxy/README.adoc index 89717a7bf..43b7f65b1 100644 --- a/contracts/proxy/README.adoc +++ b/contracts/proxy/README.adoc @@ -11,7 +11,7 @@ Most of the proxies below are built on an abstract base contract. In order to avoid clashes with the storage variables of the implementation contract behind a proxy, we use https://eips.ethereum.org/EIPS/eip-1967[EIP1967] storage slots. -- {ERC1967Upgrade}: Internal functions to get and set the storage slots defined in EIP1967. +- {ERC1967Utils}: Internal functions to get and set the storage slots defined in EIP1967. - {ERC1967Proxy}: A proxy using EIP1967 storage slots. Not upgradeable by default. There are two alternative ways to add upgradeability to an ERC1967 proxy. Their differences are explained below in <>. @@ -60,7 +60,7 @@ The current implementation of this security mechanism uses https://eips.ethereum {{ERC1967Proxy}} -{{ERC1967Upgrade}} +{{ERC1967Utils}} == Transparent Proxy diff --git a/contracts/proxy/beacon/BeaconProxy.sol b/contracts/proxy/beacon/BeaconProxy.sol index d603e38a7..dc6b5c90b 100644 --- a/contracts/proxy/beacon/BeaconProxy.sol +++ b/contracts/proxy/beacon/BeaconProxy.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.19; import "./IBeacon.sol"; import "../Proxy.sol"; -import "../ERC1967/ERC1967Upgrade.sol"; +import "../ERC1967/ERC1967Utils.sol"; /** * @dev This contract implements a proxy that gets the implementation address for each call from an {UpgradeableBeacon}. @@ -15,7 +15,7 @@ import "../ERC1967/ERC1967Upgrade.sol"; * * _Available since v3.4._ */ -contract BeaconProxy is Proxy, ERC1967Upgrade { +contract BeaconProxy is Proxy { /** * @dev Initializes the proxy with `beacon`. * @@ -28,13 +28,13 @@ contract BeaconProxy is Proxy, ERC1967Upgrade { * - `beacon` must be a contract with the interface {IBeacon}. */ constructor(address beacon, bytes memory data) payable { - _upgradeBeaconToAndCall(beacon, data, false); + ERC1967Utils.upgradeBeaconToAndCall(beacon, data, false); } /** * @dev Returns the current implementation address of the associated beacon. */ function _implementation() internal view virtual override returns (address) { - return IBeacon(_getBeacon()).implementation(); + return IBeacon(ERC1967Utils.getBeacon()).implementation(); } } diff --git a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol index 4536e28c8..2b022ccad 100644 --- a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol @@ -67,14 +67,14 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { * optionally initialized with `_data` as explained in {ERC1967Proxy-constructor}. */ constructor(address _logic, address admin_, bytes memory _data) payable ERC1967Proxy(_logic, _data) { - _changeAdmin(admin_); + ERC1967Utils.changeAdmin(admin_); } /** * @dev If caller is the admin process the call internally, otherwise transparently fallback to the proxy behavior */ function _fallback() internal virtual override { - if (msg.sender == _getAdmin()) { + if (msg.sender == ERC1967Utils.getAdmin()) { bytes memory ret; bytes4 selector = msg.sig; if (selector == ITransparentUpgradeableProxy.upgradeTo.selector) { @@ -103,7 +103,7 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { _requireZeroValue(); address newAdmin = abi.decode(msg.data[4:], (address)); - _changeAdmin(newAdmin); + ERC1967Utils.changeAdmin(newAdmin); return ""; } @@ -115,7 +115,7 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { _requireZeroValue(); address newImplementation = abi.decode(msg.data[4:], (address)); - _upgradeToAndCall(newImplementation, bytes(""), false); + ERC1967Utils.upgradeToAndCall(newImplementation, bytes(""), false); return ""; } @@ -127,7 +127,7 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { */ function _dispatchUpgradeToAndCall() private returns (bytes memory) { (address newImplementation, bytes memory data) = abi.decode(msg.data[4:], (address, bytes)); - _upgradeToAndCall(newImplementation, data, true); + ERC1967Utils.upgradeToAndCall(newImplementation, data, true); return ""; } diff --git a/contracts/proxy/utils/UUPSUpgradeable.sol b/contracts/proxy/utils/UUPSUpgradeable.sol index 41a72ef9c..d213f2d61 100644 --- a/contracts/proxy/utils/UUPSUpgradeable.sol +++ b/contracts/proxy/utils/UUPSUpgradeable.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.19; import "../../interfaces/draft-IERC1822.sol"; -import "../ERC1967/ERC1967Upgrade.sol"; +import "../ERC1967/ERC1967Utils.sol"; /** * @dev An upgradeability mechanism designed for UUPS proxies. The functions included here can perform an upgrade of an @@ -18,7 +18,7 @@ import "../ERC1967/ERC1967Upgrade.sol"; * * _Available since v4.1._ */ -abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { +abstract contract UUPSUpgradeable is IERC1822Proxiable { /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment address private immutable __self = address(this); @@ -39,7 +39,7 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { // Must be called through delegatecall revert UUPSUnauthorizedCallContext(); } - if (_getImplementation() != __self) { + if (ERC1967Utils.getImplementation() != __self) { // Must be called through an active proxy revert UUPSUnauthorizedCallContext(); } @@ -67,7 +67,7 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier. */ function proxiableUUID() external view virtual notDelegated returns (bytes32) { - return _IMPLEMENTATION_SLOT; + return ERC1967Utils.IMPLEMENTATION_SLOT; } /** @@ -81,7 +81,7 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { */ function upgradeTo(address newImplementation) public virtual onlyProxy { _authorizeUpgrade(newImplementation); - _upgradeToAndCallUUPS(newImplementation, new bytes(0), false); + ERC1967Utils.upgradeToAndCallUUPS(newImplementation, new bytes(0), false); } /** @@ -96,7 +96,7 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { */ function upgradeToAndCall(address newImplementation, bytes memory data) public payable virtual onlyProxy { _authorizeUpgrade(newImplementation); - _upgradeToAndCallUUPS(newImplementation, data, true); + ERC1967Utils.upgradeToAndCallUUPS(newImplementation, data, true); } /** diff --git a/hardhat.config.js b/hardhat.config.js index 66367e96e..6cb8b9144 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -40,7 +40,7 @@ const argv = require('yargs/yargs')() compiler: { alias: 'compileVersion', type: 'string', - default: '0.8.19', + default: '0.8.20', }, coinmarketcap: { alias: 'coinmarketcapApiKey', diff --git a/scripts/upgradeable/transpile.sh b/scripts/upgradeable/transpile.sh index e3aa31cc0..fbffe844e 100644 --- a/scripts/upgradeable/transpile.sh +++ b/scripts/upgradeable/transpile.sh @@ -29,7 +29,7 @@ npx @openzeppelin/upgrade-safe-transpiler@latest -D \ -x 'contracts/proxy/**/*' \ -x '!contracts/proxy/Clones.sol' \ -x '!contracts/proxy/ERC1967/ERC1967Storage.sol' \ - -x '!contracts/proxy/ERC1967/ERC1967Upgrade.sol' \ + -x '!contracts/proxy/ERC1967/ERC1967Utils.sol' \ -x '!contracts/proxy/utils/UUPSUpgradeable.sol' \ -x '!contracts/proxy/beacon/IBeacon.sol' \ -p 'contracts/**/presets/**/*' From c014c8f1484b2fcbac5fae9c05b6d63771f0d716 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 15 Jun 2023 23:27:36 +0200 Subject: [PATCH 176/197] Use ERC721Holder & ERC1155Holder in the TimelockController (#4284) Co-authored-by: Francisco --- contracts/governance/TimelockController.sol | 39 ++++----------------- scripts/checks/inheritance-ordering.js | 2 +- 2 files changed, 8 insertions(+), 33 deletions(-) diff --git a/contracts/governance/TimelockController.sol b/contracts/governance/TimelockController.sol index 4772a252f..a25cd7b4a 100644 --- a/contracts/governance/TimelockController.sol +++ b/contracts/governance/TimelockController.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.19; import "../access/AccessControl.sol"; -import "../token/ERC721/IERC721Receiver.sol"; -import "../token/ERC1155/IERC1155Receiver.sol"; +import "../token/ERC721/utils/ERC721Holder.sol"; +import "../token/ERC1155/utils/ERC1155Holder.sol"; import "../utils/Address.sol"; /** @@ -23,7 +23,7 @@ import "../utils/Address.sol"; * * _Available since v3.3._ */ -contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver { +contract TimelockController is AccessControl, ERC721Holder, ERC1155Holder { bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE"); bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE"); bytes32 public constant CANCELLER_ROLE = keccak256("CANCELLER_ROLE"); @@ -155,8 +155,10 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver /** * @dev See {IERC165-supportsInterface}. */ - function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, AccessControl) returns (bool) { - return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(AccessControl, ERC1155Receiver) returns (bool) { + return super.supportsInterface(interfaceId); } /** @@ -430,31 +432,4 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver emit MinDelayChange(_minDelay, newDelay); _minDelay = newDelay; } - - /** - * @dev See {IERC721Receiver-onERC721Received}. - */ - function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) { - return this.onERC721Received.selector; - } - - /** - * @dev See {IERC1155Receiver-onERC1155Received}. - */ - function onERC1155Received(address, address, uint256, uint256, bytes memory) public virtual returns (bytes4) { - return this.onERC1155Received.selector; - } - - /** - * @dev See {IERC1155Receiver-onERC1155BatchReceived}. - */ - function onERC1155BatchReceived( - address, - address, - uint256[] memory, - uint256[] memory, - bytes memory - ) public virtual returns (bytes4) { - return this.onERC1155BatchReceived.selector; - } } diff --git a/scripts/checks/inheritance-ordering.js b/scripts/checks/inheritance-ordering.js index 45c707e6f..72aa37ef7 100755 --- a/scripts/checks/inheritance-ordering.js +++ b/scripts/checks/inheritance-ordering.js @@ -13,7 +13,7 @@ for (const artifact of artifacts) { const linearized = []; for (const source in solcOutput.contracts) { - if (source.includes('/mocks/')) { + if (['contracts-exposed/', 'contracts/mocks/'].some(pattern => source.startsWith(pattern))) { continue; } From 67248738953d4fd8601744386cd92262024c576d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 15 Jun 2023 23:54:35 +0200 Subject: [PATCH 177/197] Output contract name where storage inconsistency was found (#4357) --- scripts/checks/compare-layout.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/checks/compare-layout.js b/scripts/checks/compare-layout.js index 7b7df9928..4368b77fb 100644 --- a/scripts/checks/compare-layout.js +++ b/scripts/checks/compare-layout.js @@ -10,6 +10,7 @@ for (const name in oldLayout) { if (name in newLayout) { const report = getStorageUpgradeReport(oldLayout[name], newLayout[name], {}); if (!report.ok) { + console.log(`Storage layout incompatilibity found in ${name}:`); console.log(report.explain()); process.exitCode = 1; } From cd48b3eab380254b08d7893a5a7bf568a33c5259 Mon Sep 17 00:00:00 2001 From: Claudia Barcelo Date: Fri, 16 Jun 2023 02:43:17 +0200 Subject: [PATCH 178/197] Add validation in Governor on ERC-721 or ERC-1155 received (#4314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hadrien Croubois Co-authored-by: Francisco Co-authored-by: Ernesto García --- .changeset/mighty-donuts-smile.md | 6 + .changeset/thin-camels-matter.md | 5 + contracts/governance/Governor.sol | 14 +- contracts/mocks/token/ERC1155ReceiverMock.sol | 33 ++++- contracts/token/ERC1155/ERC1155.sol | 30 +++-- .../GovernorTimelockCompound.test.js | 66 ++++++++++ .../GovernorTimelockControl.test.js | 66 ++++++++++ test/token/ERC1155/ERC1155.behavior.js | 123 +++++++++++++++--- 8 files changed, 306 insertions(+), 37 deletions(-) create mode 100644 .changeset/mighty-donuts-smile.md create mode 100644 .changeset/thin-camels-matter.md diff --git a/.changeset/mighty-donuts-smile.md b/.changeset/mighty-donuts-smile.md new file mode 100644 index 000000000..5885a7370 --- /dev/null +++ b/.changeset/mighty-donuts-smile.md @@ -0,0 +1,6 @@ +--- +'openzeppelin-solidity': patch +--- + +`Governor`: Add validation in ERC1155 and ERC721 receiver hooks to ensure Governor is the executor. + diff --git a/.changeset/thin-camels-matter.md b/.changeset/thin-camels-matter.md new file mode 100644 index 000000000..c832b1163 --- /dev/null +++ b/.changeset/thin-camels-matter.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC1155`: Bubble errors triggered in the `onERC1155Received` and `onERC1155BatchReceived` hooks. diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index 83bbfb083..a8654ac35 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -70,7 +70,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive * governance protocol (since v4.6). */ modifier onlyGovernance() { - if (_msgSender() != _executor()) { + if (_executor() != _msgSender()) { revert GovernorOnlyExecutor(_msgSender()); } if (_executor() != address(this)) { @@ -631,20 +631,29 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive /** * @dev See {IERC721Receiver-onERC721Received}. + * Receiving tokens is disabled if the governance executor is other than the governor itself (eg. when using with a timelock). */ function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) { + if (_executor() != address(this)) { + revert GovernorDisabledDeposit(); + } return this.onERC721Received.selector; } /** * @dev See {IERC1155Receiver-onERC1155Received}. + * Receiving tokens is disabled if the governance executor is other than the governor itself (eg. when using with a timelock). */ function onERC1155Received(address, address, uint256, uint256, bytes memory) public virtual returns (bytes4) { + if (_executor() != address(this)) { + revert GovernorDisabledDeposit(); + } return this.onERC1155Received.selector; } /** * @dev See {IERC1155Receiver-onERC1155BatchReceived}. + * Receiving tokens is disabled if the governance executor is other than the governor itself (eg. when using with a timelock). */ function onERC1155BatchReceived( address, @@ -653,6 +662,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive uint256[] memory, bytes memory ) public virtual returns (bytes4) { + if (_executor() != address(this)) { + revert GovernorDisabledDeposit(); + } return this.onERC1155BatchReceived.selector; } diff --git a/contracts/mocks/token/ERC1155ReceiverMock.sol b/contracts/mocks/token/ERC1155ReceiverMock.sol index 2b591c058..c4cdbf20e 100644 --- a/contracts/mocks/token/ERC1155ReceiverMock.sol +++ b/contracts/mocks/token/ERC1155ReceiverMock.sol @@ -6,15 +6,24 @@ import "../../token/ERC1155/IERC1155Receiver.sol"; import "../../utils/introspection/ERC165.sol"; contract ERC1155ReceiverMock is ERC165, IERC1155Receiver { + enum RevertType { + None, + Empty, + String, + Custom + } + bytes4 private _recRetval; - bool private _recReverts; + RevertType private _recReverts; bytes4 private _batRetval; - bool private _batReverts; + RevertType private _batReverts; event Received(address operator, address from, uint256 id, uint256 value, bytes data, uint256 gas); event BatchReceived(address operator, address from, uint256[] ids, uint256[] values, bytes data, uint256 gas); - constructor(bytes4 recRetval, bool recReverts, bytes4 batRetval, bool batReverts) { + error ERC1155ReceiverMockError(); + + constructor(bytes4 recRetval, RevertType recReverts, bytes4 batRetval, RevertType batReverts) { _recRetval = recRetval; _recReverts = recReverts; _batRetval = batRetval; @@ -28,7 +37,14 @@ contract ERC1155ReceiverMock is ERC165, IERC1155Receiver { uint256 value, bytes calldata data ) external returns (bytes4) { - require(!_recReverts, "ERC1155ReceiverMock: reverting on receive"); + if (_recReverts == RevertType.Empty) { + revert(); + } else if (_recReverts == RevertType.String) { + revert("ERC1155ReceiverMock: reverting on receive"); + } else if (_recReverts == RevertType.Custom) { + revert ERC1155ReceiverMockError(); + } + emit Received(operator, from, id, value, data, gasleft()); return _recRetval; } @@ -40,7 +56,14 @@ contract ERC1155ReceiverMock is ERC165, IERC1155Receiver { uint256[] calldata values, bytes calldata data ) external returns (bytes4) { - require(!_batReverts, "ERC1155ReceiverMock: reverting on batch receive"); + if (_batReverts == RevertType.Empty) { + revert(); + } else if (_batReverts == RevertType.String) { + revert("ERC1155ReceiverMock: reverting on batch receive"); + } else if (_batReverts == RevertType.Custom) { + revert ERC1155ReceiverMockError(); + } + emit BatchReceived(operator, from, ids, values, data, gasleft()); return _batRetval; } diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol index 5369d7732..9129992f4 100644 --- a/contracts/token/ERC1155/ERC1155.sol +++ b/contracts/token/ERC1155/ERC1155.sol @@ -364,11 +364,16 @@ abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IER // Tokens rejected revert ERC1155InvalidReceiver(to); } - } catch Error(string memory reason) { - revert(reason); - } catch { - // non-ERC1155Receiver implementer - revert ERC1155InvalidReceiver(to); + } catch (bytes memory reason) { + if (reason.length == 0) { + // non-ERC1155Receiver implementer + revert ERC1155InvalidReceiver(to); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } } } } @@ -389,11 +394,16 @@ abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IER // Tokens rejected revert ERC1155InvalidReceiver(to); } - } catch Error(string memory reason) { - revert(reason); - } catch { - // non-ERC1155Receiver implementer - revert ERC1155InvalidReceiver(to); + } catch (bytes memory reason) { + if (reason.length == 0) { + // non-ERC1155Receiver implementer + revert ERC1155InvalidReceiver(to); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } } } } diff --git a/test/governance/extensions/GovernorTimelockCompound.test.js b/test/governance/extensions/GovernorTimelockCompound.test.js index 8c6680aa8..c406baf8d 100644 --- a/test/governance/extensions/GovernorTimelockCompound.test.js +++ b/test/governance/extensions/GovernorTimelockCompound.test.js @@ -11,6 +11,8 @@ const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsI const Timelock = artifacts.require('CompTimelock'); const Governor = artifacts.require('$GovernorTimelockCompoundMock'); const CallReceiver = artifacts.require('CallReceiverMock'); +const ERC721 = artifacts.require('$ERC721'); +const ERC1155 = artifacts.require('$ERC1155'); function makeContractAddress(creator, nonce) { return web3.utils.toChecksumAddress( @@ -212,6 +214,70 @@ contract('GovernorTimelockCompound', function (accounts) { ]); }); }); + + describe('on safe receive', function () { + describe('ERC721', function () { + const name = 'Non Fungible Token'; + const symbol = 'NFT'; + const tokenId = web3.utils.toBN(1); + + beforeEach(async function () { + this.token = await ERC721.new(name, symbol); + await this.token.$_mint(owner, tokenId); + }); + + it("can't receive an ERC721 safeTransfer", async function () { + await expectRevertCustomError( + this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner }), + 'GovernorDisabledDeposit', + [], + ); + }); + }); + + describe('ERC1155', function () { + const uri = 'https://token-cdn-domain/{id}.json'; + const tokenIds = { + 1: web3.utils.toBN(1000), + 2: web3.utils.toBN(2000), + 3: web3.utils.toBN(3000), + }; + + beforeEach(async function () { + this.token = await ERC1155.new(uri); + await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); + }); + + it("can't receive ERC1155 safeTransfer", async function () { + await expectRevertCustomError( + this.token.safeTransferFrom( + owner, + this.mock.address, + ...Object.entries(tokenIds)[0], // id + amount + '0x', + { from: owner }, + ), + 'GovernorDisabledDeposit', + [], + ); + }); + + it("can't receive ERC1155 safeBatchTransfer", async function () { + await expectRevertCustomError( + this.token.safeBatchTransferFrom( + owner, + this.mock.address, + Object.keys(tokenIds), + Object.values(tokenIds), + '0x', + { from: owner }, + ), + 'GovernorDisabledDeposit', + [], + ); + }); + }); + }); }); describe('cancel', function () { diff --git a/test/governance/extensions/GovernorTimelockControl.test.js b/test/governance/extensions/GovernorTimelockControl.test.js index 10a7f05b2..3265dfa68 100644 --- a/test/governance/extensions/GovernorTimelockControl.test.js +++ b/test/governance/extensions/GovernorTimelockControl.test.js @@ -10,6 +10,8 @@ const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsI const Timelock = artifacts.require('TimelockController'); const Governor = artifacts.require('$GovernorTimelockControlMock'); const CallReceiver = artifacts.require('CallReceiverMock'); +const ERC721 = artifacts.require('$ERC721'); +const ERC1155 = artifacts.require('$ERC1155'); const TOKENS = [ { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, @@ -412,6 +414,70 @@ contract('GovernorTimelockControl', function (accounts) { expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); }); }); + + describe('on safe receive', function () { + describe('ERC721', function () { + const name = 'Non Fungible Token'; + const symbol = 'NFT'; + const tokenId = web3.utils.toBN(1); + + beforeEach(async function () { + this.token = await ERC721.new(name, symbol); + await this.token.$_mint(owner, tokenId); + }); + + it("can't receive an ERC721 safeTransfer", async function () { + await expectRevertCustomError( + this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner }), + 'GovernorDisabledDeposit', + [], + ); + }); + }); + + describe('ERC1155', function () { + const uri = 'https://token-cdn-domain/{id}.json'; + const tokenIds = { + 1: web3.utils.toBN(1000), + 2: web3.utils.toBN(2000), + 3: web3.utils.toBN(3000), + }; + + beforeEach(async function () { + this.token = await ERC1155.new(uri); + await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); + }); + + it("can't receive ERC1155 safeTransfer", async function () { + await expectRevertCustomError( + this.token.safeTransferFrom( + owner, + this.mock.address, + ...Object.entries(tokenIds)[0], // id + amount + '0x', + { from: owner }, + ), + 'GovernorDisabledDeposit', + [], + ); + }); + + it("can't receive ERC1155 safeBatchTransfer", async function () { + await expectRevertCustomError( + this.token.safeBatchTransferFrom( + owner, + this.mock.address, + Object.keys(tokenIds), + Object.values(tokenIds), + '0x', + { from: owner }, + ), + 'GovernorDisabledDeposit', + [], + ); + }); + }); + }); }); it('clear queue of pending governor calls', async function () { diff --git a/test/token/ERC1155/ERC1155.behavior.js b/test/token/ERC1155/ERC1155.behavior.js index 1693c4aa6..36f67d321 100644 --- a/test/token/ERC1155/ERC1155.behavior.js +++ b/test/token/ERC1155/ERC1155.behavior.js @@ -5,8 +5,10 @@ const { expect } = require('chai'); const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); const { expectRevertCustomError } = require('../../helpers/customError'); +const { Enum } = require('../../helpers/enums'); const ERC1155ReceiverMock = artifacts.require('ERC1155ReceiverMock'); +const RevertType = Enum('None', 'Empty', 'String', 'Custom'); function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, multiTokenHolder, recipient, proxy]) { const firstTokenId = new BN(1); @@ -296,9 +298,9 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m beforeEach(async function () { this.receiver = await ERC1155ReceiverMock.new( RECEIVER_SINGLE_MAGIC_VALUE, - false, + RevertType.None, RECEIVER_BATCH_MAGIC_VALUE, - false, + RevertType.None, ); }); @@ -370,7 +372,12 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m context('to a receiver contract returning unexpected value', function () { beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new('0x00c0ffee', false, RECEIVER_BATCH_MAGIC_VALUE, false); + this.receiver = await ERC1155ReceiverMock.new( + '0x00c0ffee', + RevertType.None, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.None, + ); }); it('reverts', async function () { @@ -385,23 +392,55 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); context('to a receiver contract that reverts', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( + it('with empty reason', async function () { + const receiver = await ERC1155ReceiverMock.new( RECEIVER_SINGLE_MAGIC_VALUE, - true, + RevertType.Empty, RECEIVER_BATCH_MAGIC_VALUE, - false, + RevertType.None, + ); + + await expectRevertCustomError( + this.token.safeTransferFrom(multiTokenHolder, receiver.address, firstTokenId, firstAmount, '0x', { + from: multiTokenHolder, + }), + 'ERC1155InvalidReceiver', + [receiver.address], ); }); - it('reverts', async function () { + it('with reason string', async function () { + const receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RevertType.String, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.None, + ); + await expectRevert( - this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { + this.token.safeTransferFrom(multiTokenHolder, receiver.address, firstTokenId, firstAmount, '0x', { from: multiTokenHolder, }), 'ERC1155ReceiverMock: reverting on receive', ); }); + + it('with custom error', async function () { + const receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RevertType.Custom, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.None, + ); + + await expectRevertCustomError( + this.token.safeTransferFrom(multiTokenHolder, receiver.address, firstTokenId, firstAmount, '0x', { + from: multiTokenHolder, + }), + 'ERC1155ReceiverMockError', + [], + ); + }); }); context('to a contract that does not implement the required function', function () { @@ -590,9 +629,9 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m beforeEach(async function () { this.receiver = await ERC1155ReceiverMock.new( RECEIVER_SINGLE_MAGIC_VALUE, - false, + RevertType.None, RECEIVER_BATCH_MAGIC_VALUE, - false, + RevertType.None, ); }); @@ -666,9 +705,9 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m beforeEach(async function () { this.receiver = await ERC1155ReceiverMock.new( RECEIVER_SINGLE_MAGIC_VALUE, - false, + RevertType.None, RECEIVER_SINGLE_MAGIC_VALUE, - false, + RevertType.None, ); }); @@ -689,20 +728,40 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); context('to a receiver contract that reverts', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( + it('with empty reason', async function () { + const receiver = await ERC1155ReceiverMock.new( RECEIVER_SINGLE_MAGIC_VALUE, - false, + RevertType.None, RECEIVER_BATCH_MAGIC_VALUE, - true, + RevertType.Empty, + ); + + await expectRevertCustomError( + this.token.safeBatchTransferFrom( + multiTokenHolder, + receiver.address, + [firstTokenId, secondTokenId], + [firstAmount, secondAmount], + '0x', + { from: multiTokenHolder }, + ), + 'ERC1155InvalidReceiver', + [receiver.address], ); }); - it('reverts', async function () { + it('with reason string', async function () { + const receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RevertType.None, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.String, + ); + await expectRevert( this.token.safeBatchTransferFrom( multiTokenHolder, - this.receiver.address, + receiver.address, [firstTokenId, secondTokenId], [firstAmount, secondAmount], '0x', @@ -711,15 +770,37 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m 'ERC1155ReceiverMock: reverting on batch receive', ); }); + + it('with custom error', async function () { + const receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RevertType.None, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.Custom, + ); + + await expectRevertCustomError( + this.token.safeBatchTransferFrom( + multiTokenHolder, + receiver.address, + [firstTokenId, secondTokenId], + [firstAmount, secondAmount], + '0x', + { from: multiTokenHolder }, + ), + 'ERC1155ReceiverMockError', + [], + ); + }); }); context('to a receiver contract that reverts only on single transfers', function () { beforeEach(async function () { this.receiver = await ERC1155ReceiverMock.new( RECEIVER_SINGLE_MAGIC_VALUE, - true, + RevertType.String, RECEIVER_BATCH_MAGIC_VALUE, - false, + RevertType.None, ); this.toWhom = this.receiver.address; From 002a7c8812e73c282b91e14541ce9b93a6de1172 Mon Sep 17 00:00:00 2001 From: Francisco Date: Fri, 16 Jun 2023 15:42:19 -0300 Subject: [PATCH 179/197] Remove automatic conflict resolution for merge from release branch (#4362) --- .github/workflows/release-cycle.yml | 8 ++++-- .../release/workflow/prepare-release-merge.sh | 27 ------------------- 2 files changed, 6 insertions(+), 29 deletions(-) delete mode 100644 scripts/release/workflow/prepare-release-merge.sh diff --git a/.github/workflows/release-cycle.yml b/.github/workflows/release-cycle.yml index 2fd66458d..9d35022dc 100644 --- a/.github/workflows/release-cycle.yml +++ b/.github/workflows/release-cycle.yml @@ -192,6 +192,8 @@ jobs: pull-requests: write if: needs.state.outputs.merge == 'true' runs-on: ubuntu-latest + env: + MERGE_BRANCH: merge/${{ github.ref_name }} steps: - uses: actions/checkout@v3 with: @@ -200,7 +202,9 @@ jobs: uses: ./.github/actions/setup - run: bash scripts/git-user-config.sh - name: Create branch to merge - run: bash scripts/release/workflow/prepare-release-merge.sh + run: | + git checkout -B "$MERGE_BRANCH" "$GITHUB_REF_NAME" + git push -f origin "$MERGE_BRANCH" - name: Create PR back to master uses: actions/github-script@v6 with: @@ -208,7 +212,7 @@ jobs: await github.rest.pulls.create({ owner: context.repo.owner, repo: context.repo.repo, - head: 'merge/${{ github.ref_name }}', + head: process.env.MERGE_BRANCH, base: 'master', title: '${{ format('Merge {0} branch', github.ref_name) }}' }); diff --git a/scripts/release/workflow/prepare-release-merge.sh b/scripts/release/workflow/prepare-release-merge.sh deleted file mode 100644 index 4e6da5145..000000000 --- a/scripts/release/workflow/prepare-release-merge.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# Define merge branch name -MERGE_BRANCH=merge/$GITHUB_REF_NAME - -# Create the branch and force to start from ref -git checkout -B "$MERGE_BRANCH" "$GITHUB_REF_NAME" - -# Get deleted changesets in this branch that might conflict with master -# --diff-filter=D - Only deleted files -readarray -t DELETED_CHANGESETS < <(git diff origin/master --diff-filter=D --name-only -- '.changeset/*.md') - -# Merge master, which will take those files cherry-picked. Auto-resolve conflicts favoring master. -# Ignore conflicts that can't be resolved. -git merge origin/master -m "Merge master to $GITHUB_REF_NAME" -X theirs || true - -# Remove the originally deleted changesets to correctly sync with master -rm -f "${DELETED_CHANGESETS[@]}" - -# Only git add deleted files -git ls-files --deleted .changeset/ | xargs git add - -# Allow empty here since there may be no changes if `rm -f` failed for all changesets -git commit --allow-empty -m "Sync changesets with master" -git push -f origin "$MERGE_BRANCH" From 1a77a508f93e2df058cb082def4753a060aefa8f Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Sat, 17 Jun 2023 04:25:58 +0200 Subject: [PATCH 180/197] Move upgradeToAndCallUUPS to UUPSUpgradeable (#4356) Co-authored-by: ernestognw --- .changeset/hip-beds-provide.md | 5 ++ contracts/mocks/proxy/UUPSLegacy.sol | 54 ------------- contracts/mocks/proxy/UUPSUpgradeableMock.sol | 6 ++ contracts/proxy/ERC1967/ERC1967Utils.sol | 34 -------- .../TransparentUpgradeableProxy.sol | 1 + contracts/proxy/utils/UUPSUpgradeable.sol | 36 +++++++-- test/proxy/utils/UUPSUpgradeable.test.js | 80 +++++++++++++------ 7 files changed, 97 insertions(+), 119 deletions(-) create mode 100644 .changeset/hip-beds-provide.md delete mode 100644 contracts/mocks/proxy/UUPSLegacy.sol diff --git a/.changeset/hip-beds-provide.md b/.changeset/hip-beds-provide.md new file mode 100644 index 000000000..c67283813 --- /dev/null +++ b/.changeset/hip-beds-provide.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Move the logic to validate ERC-1822 during an upgrade from `ERC1967Utils` to `UUPSUpgradeable`. diff --git a/contracts/mocks/proxy/UUPSLegacy.sol b/contracts/mocks/proxy/UUPSLegacy.sol deleted file mode 100644 index 6812f39b7..000000000 --- a/contracts/mocks/proxy/UUPSLegacy.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.19; - -import "./UUPSUpgradeableMock.sol"; - -// This contract implements the pre-4.5 UUPS upgrade function with a rollback test. -// It's used to test that newer UUPS contracts are considered valid upgrades by older UUPS contracts. -contract UUPSUpgradeableLegacyMock is UUPSUpgradeableMock { - // Inlined from ERC1967Utils - bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; - - // ERC1967Utils._setImplementation is private so we reproduce it here. - // An extra underscore prevents a name clash error. - function __setImplementation(address newImplementation) private { - require(newImplementation.code.length > 0, "ERC1967: new implementation is not a contract"); - StorageSlot.getAddressSlot(ERC1967Utils.IMPLEMENTATION_SLOT).value = newImplementation; - } - - function _upgradeToAndCallSecureLegacyV1(address newImplementation, bytes memory data, bool forceCall) internal { - address oldImplementation = ERC1967Utils.getImplementation(); - - // Initial upgrade and setup call - __setImplementation(newImplementation); - if (data.length > 0 || forceCall) { - Address.functionDelegateCall(newImplementation, data); - } - - // Perform rollback test if not already in progress - StorageSlot.BooleanSlot storage rollbackTesting = StorageSlot.getBooleanSlot(_ROLLBACK_SLOT); - if (!rollbackTesting.value) { - // Trigger rollback using upgradeTo from the new implementation - rollbackTesting.value = true; - Address.functionDelegateCall(newImplementation, abi.encodeCall(this.upgradeTo, (oldImplementation))); - rollbackTesting.value = false; - // Check rollback was effective - require( - oldImplementation == ERC1967Utils.getImplementation(), - "ERC1967Utils: upgrade breaks further upgrades" - ); - // Finally reset to the new implementation and log the upgrade - ERC1967Utils.upgradeTo(newImplementation); - } - } - - // hooking into the old mechanism - function upgradeTo(address newImplementation) public override { - _upgradeToAndCallSecureLegacyV1(newImplementation, bytes(""), false); - } - - function upgradeToAndCall(address newImplementation, bytes memory data) public payable override { - _upgradeToAndCallSecureLegacyV1(newImplementation, data, false); - } -} diff --git a/contracts/mocks/proxy/UUPSUpgradeableMock.sol b/contracts/mocks/proxy/UUPSUpgradeableMock.sol index 81af96490..77744998b 100644 --- a/contracts/mocks/proxy/UUPSUpgradeableMock.sol +++ b/contracts/mocks/proxy/UUPSUpgradeableMock.sol @@ -30,3 +30,9 @@ contract UUPSUpgradeableUnsafeMock is UUPSUpgradeableMock { ERC1967Utils.upgradeToAndCall(newImplementation, data, false); } } + +contract UUPSUnsupportedProxiableUUID is UUPSUpgradeableMock { + function proxiableUUID() external pure override returns (bytes32) { + return keccak256("invalid UUID"); + } +} diff --git a/contracts/proxy/ERC1967/ERC1967Utils.sol b/contracts/proxy/ERC1967/ERC1967Utils.sol index 55f5c6aab..c244982bf 100644 --- a/contracts/proxy/ERC1967/ERC1967Utils.sol +++ b/contracts/proxy/ERC1967/ERC1967Utils.sol @@ -4,8 +4,6 @@ pragma solidity ^0.8.20; import "../beacon/IBeacon.sol"; -import "../../interfaces/IERC1967.sol"; -import "../../interfaces/draft-IERC1822.sol"; import "../../utils/Address.sol"; import "../../utils/StorageSlot.sol"; @@ -33,9 +31,6 @@ library ERC1967Utils { */ event BeaconUpgraded(address indexed beacon); - // This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1 - bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; - /** * @dev Storage slot with the address of the current implementation. * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is @@ -59,11 +54,6 @@ library ERC1967Utils { */ error ERC1967InvalidBeacon(address beacon); - /** - * @dev The storage `slot` is unsupported as a UUID. - */ - error ERC1967UnsupportedProxiableUUID(bytes32 slot); - /** * @dev Returns the current implementation address. */ @@ -103,30 +93,6 @@ library ERC1967Utils { } } - /** - * @dev Perform implementation upgrade with security checks for UUPS proxies, and additional setup call. - * - * Emits an {IERC1967-Upgraded} event. - */ - function upgradeToAndCallUUPS(address newImplementation, bytes memory data, bool forceCall) internal { - // Upgrades from old implementations will perform a rollback test. This test requires the new - // implementation to upgrade back to the old, non-ERC1822 compliant, implementation. Removing - // this special case will break upgrade paths from old UUPS implementation to new ones. - if (StorageSlot.getBooleanSlot(_ROLLBACK_SLOT).value) { - _setImplementation(newImplementation); - } else { - try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) { - if (slot != IMPLEMENTATION_SLOT) { - revert ERC1967UnsupportedProxiableUUID(slot); - } - } catch { - // The implementation is not UUPS - revert ERC1967InvalidImplementation(newImplementation); - } - upgradeToAndCall(newImplementation, data, forceCall); - } - } - /** * @dev Storage slot with the admin of the contract. * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is diff --git a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol index 2b022ccad..4f7ae9666 100644 --- a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.19; import "../ERC1967/ERC1967Proxy.sol"; +import "../../interfaces/IERC1967.sol"; /** * @dev Interface for {TransparentUpgradeableProxy}. In order to implement transparency, {TransparentUpgradeableProxy} diff --git a/contracts/proxy/utils/UUPSUpgradeable.sol b/contracts/proxy/utils/UUPSUpgradeable.sol index d213f2d61..07de178bc 100644 --- a/contracts/proxy/utils/UUPSUpgradeable.sol +++ b/contracts/proxy/utils/UUPSUpgradeable.sol @@ -27,6 +27,11 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable { */ error UUPSUnauthorizedCallContext(); + /** + * @dev The storage `slot` is unsupported as a UUID. + */ + error UUPSUnsupportedProxiableUUID(bytes32 slot); + /** * @dev Check that the execution is being performed through a delegatecall call and that the execution context is * a proxy contract with an implementation (as defined in ERC1967) pointing to self. This should only be the case @@ -35,12 +40,10 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable { * fail. */ modifier onlyProxy() { - if (address(this) == __self) { - // Must be called through delegatecall - revert UUPSUnauthorizedCallContext(); - } - if (ERC1967Utils.getImplementation() != __self) { - // Must be called through an active proxy + if ( + address(this) == __self || // Must be called through delegatecall + ERC1967Utils.getImplementation() != __self // Must be called through an active proxy + ) { revert UUPSUnauthorizedCallContext(); } _; @@ -81,7 +84,7 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable { */ function upgradeTo(address newImplementation) public virtual onlyProxy { _authorizeUpgrade(newImplementation); - ERC1967Utils.upgradeToAndCallUUPS(newImplementation, new bytes(0), false); + _upgradeToAndCallUUPS(newImplementation, new bytes(0), false); } /** @@ -96,7 +99,7 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable { */ function upgradeToAndCall(address newImplementation, bytes memory data) public payable virtual onlyProxy { _authorizeUpgrade(newImplementation); - ERC1967Utils.upgradeToAndCallUUPS(newImplementation, data, true); + _upgradeToAndCallUUPS(newImplementation, data, true); } /** @@ -110,4 +113,21 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable { * ``` */ function _authorizeUpgrade(address newImplementation) internal virtual; + + /** + * @dev Perform implementation upgrade with security checks for UUPS proxies, and additional setup call. + * + * Emits an {IERC1967-Upgraded} event. + */ + function _upgradeToAndCallUUPS(address newImplementation, bytes memory data, bool forceCall) private { + try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) { + if (slot != ERC1967Utils.IMPLEMENTATION_SLOT) { + revert UUPSUnsupportedProxiableUUID(slot); + } + ERC1967Utils.upgradeToAndCall(newImplementation, data, forceCall); + } catch { + // The implementation is not UUPS + revert ERC1967Utils.ERC1967InvalidImplementation(newImplementation); + } + } } diff --git a/test/proxy/utils/UUPSUpgradeable.test.js b/test/proxy/utils/UUPSUpgradeable.test.js index ea1b1d51f..95319c305 100644 --- a/test/proxy/utils/UUPSUpgradeable.test.js +++ b/test/proxy/utils/UUPSUpgradeable.test.js @@ -1,13 +1,13 @@ const { expectEvent } = require('@openzeppelin/test-helpers'); -const { web3 } = require('@openzeppelin/test-helpers/src/setup'); -const { getSlot, ImplementationSlot } = require('../../helpers/erc1967'); +const { getAddressInSlot, ImplementationSlot } = require('../../helpers/erc1967'); const { expectRevertCustomError } = require('../../helpers/customError'); const ERC1967Proxy = artifacts.require('ERC1967Proxy'); const UUPSUpgradeableMock = artifacts.require('UUPSUpgradeableMock'); const UUPSUpgradeableUnsafeMock = artifacts.require('UUPSUpgradeableUnsafeMock'); -const UUPSUpgradeableLegacyMock = artifacts.require('UUPSUpgradeableLegacyMock'); const NonUpgradeableMock = artifacts.require('NonUpgradeableMock'); +const UUPSUnsupportedProxiableUUID = artifacts.require('UUPSUnsupportedProxiableUUID'); +const Address = artifacts.require('$Address'); contract('UUPSUpgradeable', function () { before(async function () { @@ -15,6 +15,8 @@ contract('UUPSUpgradeable', function () { this.implUpgradeOk = await UUPSUpgradeableMock.new(); this.implUpgradeUnsafe = await UUPSUpgradeableUnsafeMock.new(); this.implUpgradeNonUUPS = await NonUpgradeableMock.new(); + this.implUnsupportedUUID = await UUPSUnsupportedProxiableUUID.new(); + this.helper = await Address.new(); }); beforeEach(async function () { @@ -26,6 +28,7 @@ contract('UUPSUpgradeable', function () { const { receipt } = await this.instance.upgradeTo(this.implUpgradeOk.address); expect(receipt.logs.filter(({ event }) => event === 'Upgraded').length).to.be.equal(1); expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeOk.address }); + expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.be.equal(this.implUpgradeOk.address); }); it('upgrade to upgradeable implementation with call', async function () { @@ -37,13 +40,64 @@ contract('UUPSUpgradeable', function () { ); expect(receipt.logs.filter(({ event }) => event === 'Upgraded').length).to.be.equal(1); expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeOk.address }); + expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.be.equal(this.implUpgradeOk.address); expect(await this.instance.current()).to.be.bignumber.equal('1'); }); + it('calling upgradeTo on the implementation reverts', async function () { + await expectRevertCustomError( + this.implInitial.upgradeTo(this.implUpgradeOk.address), + 'UUPSUnauthorizedCallContext', + [], + ); + }); + + it('calling upgradeToAndCall on the implementation reverts', async function () { + await expectRevertCustomError( + this.implInitial.upgradeToAndCall( + this.implUpgradeOk.address, + this.implUpgradeOk.contract.methods.increment().encodeABI(), + ), + 'UUPSUnauthorizedCallContext', + [], + ); + }); + + it('calling upgradeTo from a contract that is not an ERC1967 proxy (with the right implementation) reverts', async function () { + await expectRevertCustomError( + this.helper.$functionDelegateCall( + this.implUpgradeOk.address, + this.implUpgradeOk.contract.methods.upgradeTo(this.implUpgradeUnsafe.address).encodeABI(), + ), + 'UUPSUnauthorizedCallContext', + [], + ); + }); + + it('calling upgradeToAndCall from a contract that is not an ERC1967 proxy (with the right implementation) reverts', async function () { + await expectRevertCustomError( + this.helper.$functionDelegateCall( + this.implUpgradeOk.address, + this.implUpgradeOk.contract.methods.upgradeToAndCall(this.implUpgradeUnsafe.address, '0x').encodeABI(), + ), + 'UUPSUnauthorizedCallContext', + [], + ); + }); + + it('rejects upgrading to an unsupported UUID', async function () { + await expectRevertCustomError( + this.instance.upgradeTo(this.implUnsupportedUUID.address), + 'UUPSUnsupportedProxiableUUID', + [web3.utils.keccak256('invalid UUID')], + ); + }); + it('upgrade to and unsafe upgradeable implementation', async function () { const { receipt } = await this.instance.upgradeTo(this.implUpgradeUnsafe.address); expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeUnsafe.address }); + expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.be.equal(this.implUpgradeUnsafe.address); }); // delegate to a non existing upgradeTo function causes a low level revert @@ -63,24 +117,4 @@ contract('UUPSUpgradeable', function () { otherInstance.address, ]); }); - - it('can upgrade from legacy implementations', async function () { - const legacyImpl = await UUPSUpgradeableLegacyMock.new(); - const legacyInstance = await ERC1967Proxy.new(legacyImpl.address, '0x').then(({ address }) => - UUPSUpgradeableLegacyMock.at(address), - ); - - const receipt = await legacyInstance.upgradeTo(this.implInitial.address); - - const UpgradedEvents = receipt.logs.filter( - ({ address, event }) => address === legacyInstance.address && event === 'Upgraded', - ); - expect(UpgradedEvents.length).to.be.equal(1); - - expectEvent(receipt, 'Upgraded', { implementation: this.implInitial.address }); - - const implementationSlot = await getSlot(legacyInstance, ImplementationSlot); - const implementationAddress = web3.utils.toChecksumAddress(implementationSlot.substr(-40)); - expect(implementationAddress).to.be.equal(this.implInitial.address); - }); }); From c95a44513011e019bdc9757f9f35ae2359ea76c0 Mon Sep 17 00:00:00 2001 From: Francisco Date: Mon, 19 Jun 2023 15:55:35 -0300 Subject: [PATCH 181/197] Detect MerkleProof multiproof invariant violation (#4367) Co-authored-by: Hadrien Croubois --- .changeset/shy-crews-teach.md | 5 ++++ contracts/utils/cryptography/MerkleProof.sol | 12 ++++++++-- test/utils/cryptography/MerkleProof.test.js | 25 ++++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 .changeset/shy-crews-teach.md diff --git a/.changeset/shy-crews-teach.md b/.changeset/shy-crews-teach.md new file mode 100644 index 000000000..8ab929bf8 --- /dev/null +++ b/.changeset/shy-crews-teach.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`MerkleProof`: Fix a bug in `processMultiProof` and `processMultiProofCalldata` that allows proving arbitrary leaves if the tree contains a node with value 0 at depth 1. diff --git a/contracts/utils/cryptography/MerkleProof.sol b/contracts/utils/cryptography/MerkleProof.sol index ed88ea1db..39826d8c6 100644 --- a/contracts/utils/cryptography/MerkleProof.sol +++ b/contracts/utils/cryptography/MerkleProof.sol @@ -126,10 +126,11 @@ library MerkleProof { // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of // the merkle tree. uint256 leavesLen = leaves.length; + uint256 proofLen = proof.length; uint256 totalHashes = proofFlags.length; // Check proof validity. - if (leavesLen + proof.length - 1 != totalHashes) { + if (leavesLen + proofLen - 1 != totalHashes) { revert MerkleProofInvalidMultiproof(); } @@ -153,6 +154,9 @@ library MerkleProof { } if (totalHashes > 0) { + if (proofPos != proofLen) { + revert MerkleProofInvalidMultiproof(); + } unchecked { return hashes[totalHashes - 1]; } @@ -180,10 +184,11 @@ library MerkleProof { // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of // the merkle tree. uint256 leavesLen = leaves.length; + uint256 proofLen = proof.length; uint256 totalHashes = proofFlags.length; // Check proof validity. - if (leavesLen + proof.length - 1 != totalHashes) { + if (leavesLen + proofLen - 1 != totalHashes) { revert MerkleProofInvalidMultiproof(); } @@ -207,6 +212,9 @@ library MerkleProof { } if (totalHashes > 0) { + if (proofPos != proofLen) { + revert MerkleProofInvalidMultiproof(); + } unchecked { return hashes[totalHashes - 1]; } diff --git a/test/utils/cryptography/MerkleProof.test.js b/test/utils/cryptography/MerkleProof.test.js index 43ef76bfa..5b87bc525 100644 --- a/test/utils/cryptography/MerkleProof.test.js +++ b/test/utils/cryptography/MerkleProof.test.js @@ -178,5 +178,30 @@ contract('MerkleProof', function () { expect(await this.merkleProof.$multiProofVerify([root], [], root, [])).to.equal(true); expect(await this.merkleProof.$multiProofVerifyCalldata([root], [], root, [])).to.equal(true); }); + + it('reverts processing manipulated proofs with a zero-value node at depth 1', async function () { + // Create a merkle tree that contains a zero leaf at depth 1 + const leaves = [keccak256('real leaf'), Buffer.alloc(32, 0)]; + const merkleTree = new MerkleTree(leaves, keccak256, { sortPairs: true }); + + const root = merkleTree.getRoot(); + + // Now we can pass any **malicious** fake leaves as valid! + const maliciousLeaves = ['malicious', 'leaves'].map(keccak256).sort(Buffer.compare); + const maliciousProof = [leaves[0], leaves[0]]; + const maliciousProofFlags = [true, true, false]; + + await expectRevertCustomError( + this.merkleProof.$multiProofVerify(maliciousProof, maliciousProofFlags, root, maliciousLeaves), + 'MerkleProofInvalidMultiproof', + [], + ); + + await expectRevertCustomError( + this.merkleProof.$multiProofVerifyCalldata(maliciousProof, maliciousProofFlags, root, maliciousLeaves), + 'MerkleProofInvalidMultiproof', + [], + ); + }); }); }); From fc19a7947c316d5047148762298dc650b5c0ed42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Mon, 19 Jun 2023 16:33:21 -0600 Subject: [PATCH 182/197] Change release cycle `prBackExists` definition (#4365) --- scripts/release/workflow/state.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/release/workflow/state.js b/scripts/release/workflow/state.js index 5cfaafb86..914e8de02 100644 --- a/scripts/release/workflow/state.js +++ b/scripts/release/workflow/state.js @@ -47,7 +47,7 @@ function shouldRunMerge({ hasPendingChangesets, prBackExists, }) { - return isReleaseBranch && isPush && !prerelease && isCurrentFinalVersion && !hasPendingChangesets && prBackExists; + return isReleaseBranch && isPush && !prerelease && isCurrentFinalVersion && !hasPendingChangesets && !prBackExists; } async function getState({ github, context, core }) { @@ -79,7 +79,7 @@ async function getState({ github, context, core }) { state: 'open', }); - state.prBackExists = prs.length === 0; + state.prBackExists = prs.length !== 0; state.isPublishedOnNpm = await isPublishedOnNpm(packageName, version); From 1f4e33fb725ad49e28a82447cad1d1ce752a4b0c Mon Sep 17 00:00:00 2001 From: Balaji Shetty Pachai <32358081+balajipachai@users.noreply.github.com> Date: Tue, 20 Jun 2023 05:26:00 +0530 Subject: [PATCH 183/197] Add toStringSigned to Strings.sol (#4330) Co-authored-by: Francisco Co-authored-by: Hadrien Croubois --- .changeset/tasty-tomatoes-turn.md | 5 +++++ contracts/utils/Strings.sol | 2 +- test/utils/Strings.test.js | 8 ++++---- 3 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/tasty-tomatoes-turn.md diff --git a/.changeset/tasty-tomatoes-turn.md b/.changeset/tasty-tomatoes-turn.md new file mode 100644 index 000000000..3fe46a9b1 --- /dev/null +++ b/.changeset/tasty-tomatoes-turn.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`Strings`: Rename `toString(int256)` to `toStringSigned(int256)`. diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 574717f86..afbf463d1 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -46,7 +46,7 @@ library Strings { /** * @dev Converts a `int256` to its ASCII `string` decimal representation. */ - function toString(int256 value) internal pure returns (string memory) { + function toStringSigned(int256 value) internal pure returns (string memory) { return string.concat(value < 0 ? "-" : "", toString(SignedMath.abs(value))); } diff --git a/test/utils/Strings.test.js b/test/utils/Strings.test.js index 09b958a61..2435fc71c 100644 --- a/test/utils/Strings.test.js +++ b/test/utils/Strings.test.js @@ -48,22 +48,22 @@ contract('Strings', function () { describe('int256', function () { it('converts MAX_INT256', async function () { const value = constants.MAX_INT256; - expect(await this.strings.methods['$toString(int256)'](value)).to.equal(value.toString(10)); + expect(await this.strings.methods['$toStringSigned(int256)'](value)).to.equal(value.toString(10)); }); it('converts MIN_INT256', async function () { const value = constants.MIN_INT256; - expect(await this.strings.methods['$toString(int256)'](value)).to.equal(value.toString(10)); + expect(await this.strings.methods['$toStringSigned(int256)'](value)).to.equal(value.toString(10)); }); for (const value of values) { it(`convert ${value}`, async function () { - expect(await this.strings.methods['$toString(int256)'](value)).to.equal(value); + expect(await this.strings.methods['$toStringSigned(int256)'](value)).to.equal(value); }); it(`convert negative ${value}`, async function () { const negated = new BN(value).neg(); - expect(await this.strings.methods['$toString(int256)'](negated)).to.equal(negated.toString(10)); + expect(await this.strings.methods['$toStringSigned(int256)'](negated)).to.equal(negated.toString(10)); }); } }); From 1d0dbcf9ab4dc1588c0c41c1b836d20854350c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Mon, 19 Jun 2023 20:57:30 -0600 Subject: [PATCH 184/197] Make `TransparentUpgradeableProxy` admin immutable (#4354) Co-authored-by: Hadrien Croubois Co-authored-by: Francisco --- .changeset/happy-falcons-walk.md | 5 + contracts/mocks/DummyImplementation.sol | 7 ++ .../mocks/proxy/ClashingImplementation.sol | 2 +- contracts/proxy/README.adoc | 2 +- contracts/proxy/transparent/ProxyAdmin.sol | 11 --- .../TransparentUpgradeableProxy.sol | 48 +++++---- test/proxy/transparent/ProxyAdmin.test.js | 21 +--- .../TransparentUpgradeableProxy.behaviour.js | 99 +++++++++---------- 8 files changed, 84 insertions(+), 111 deletions(-) create mode 100644 .changeset/happy-falcons-walk.md diff --git a/.changeset/happy-falcons-walk.md b/.changeset/happy-falcons-walk.md new file mode 100644 index 000000000..bba9642aa --- /dev/null +++ b/.changeset/happy-falcons-walk.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`TransparentUpgradeableProxy`: Admin is now stored in an immutable variable (set during construction) to avoid unnecessary storage reads on every proxy call. This removed the ability to ever change the admin. Transfer of the upgrade capability is exclusively handled through the ownership of the `ProxyAdmin`. diff --git a/contracts/mocks/DummyImplementation.sol b/contracts/mocks/DummyImplementation.sol index 85503c36e..71761a755 100644 --- a/contracts/mocks/DummyImplementation.sol +++ b/contracts/mocks/DummyImplementation.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.19; +import "../proxy/ERC1967/ERC1967Utils.sol"; + abstract contract Impl { function version() public pure virtual returns (string memory); } @@ -44,6 +46,11 @@ contract DummyImplementation { function reverts() public pure { require(false, "DummyImplementation reverted"); } + + // Use for forcing an unsafe TransparentUpgradeableProxy admin override + function unsafeOverrideAdmin(address newAdmin) public { + StorageSlot.getAddressSlot(ERC1967Utils.ADMIN_SLOT).value = newAdmin; + } } contract DummyImplementationV2 is DummyImplementation { diff --git a/contracts/mocks/proxy/ClashingImplementation.sol b/contracts/mocks/proxy/ClashingImplementation.sol index 957bc34be..89904b91f 100644 --- a/contracts/mocks/proxy/ClashingImplementation.sol +++ b/contracts/mocks/proxy/ClashingImplementation.sol @@ -9,7 +9,7 @@ pragma solidity ^0.8.19; contract ClashingImplementation { event ClashingImplementationCall(); - function changeAdmin(address) external payable { + function upgradeTo(address) external payable { emit ClashingImplementationCall(); } diff --git a/contracts/proxy/README.adoc b/contracts/proxy/README.adoc index 43b7f65b1..3c4a78d19 100644 --- a/contracts/proxy/README.adoc +++ b/contracts/proxy/README.adoc @@ -16,7 +16,7 @@ In order to avoid clashes with the storage variables of the implementation contr There are two alternative ways to add upgradeability to an ERC1967 proxy. Their differences are explained below in <>. -- {TransparentUpgradeableProxy}: A proxy with a built in admin and upgrade interface. +- {TransparentUpgradeableProxy}: A proxy with a built-in immutable admin and upgrade interface. - {UUPSUpgradeable}: An upgradeability mechanism to be included in the implementation contract. CAUTION: Using upgradeable proxies correctly and securely is a difficult task that requires deep knowledge of the proxy pattern, Solidity, and the EVM. Unless you want a lot of low level control, we recommend using the xref:upgrades-plugins::index.adoc[OpenZeppelin Upgrades Plugins] for Truffle and Hardhat. diff --git a/contracts/proxy/transparent/ProxyAdmin.sol b/contracts/proxy/transparent/ProxyAdmin.sol index 490f552f8..e8578a585 100644 --- a/contracts/proxy/transparent/ProxyAdmin.sol +++ b/contracts/proxy/transparent/ProxyAdmin.sol @@ -16,17 +16,6 @@ contract ProxyAdmin is Ownable { */ constructor(address initialOwner) Ownable(initialOwner) {} - /** - * @dev Changes the admin of `proxy` to `newAdmin`. - * - * Requirements: - * - * - This contract must be the current admin of `proxy`. - */ - function changeProxyAdmin(ITransparentUpgradeableProxy proxy, address newAdmin) public virtual onlyOwner { - proxy.changeAdmin(newAdmin); - } - /** * @dev Upgrades `proxy` to `implementation`. See {TransparentUpgradeableProxy-upgradeTo}. * diff --git a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol index 4f7ae9666..71ce665d0 100644 --- a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol @@ -13,15 +13,13 @@ import "../../interfaces/IERC1967.sol"; * include them in the ABI so this interface must be used to interact with it. */ interface ITransparentUpgradeableProxy is IERC1967 { - function changeAdmin(address) external; - function upgradeTo(address) external; function upgradeToAndCall(address, bytes memory) external payable; } /** - * @dev This contract implements a proxy that is upgradeable by an admin. + * @dev This contract implements a proxy that is upgradeable by an immutable admin. * * To avoid https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357[proxy selector * clashing], which can potentially be used in an attack, this contract uses the @@ -31,15 +29,16 @@ interface ITransparentUpgradeableProxy is IERC1967 { * 1. If any account other than the admin calls the proxy, the call will be forwarded to the implementation, even if * that call matches one of the admin functions exposed by the proxy itself. * 2. If the admin calls the proxy, it can access the admin functions, but its calls will never be forwarded to the - * implementation. If the admin tries to call a function on the implementation it will fail with an error that says - * "admin cannot fallback to proxy target". + * implementation. If the admin tries to call a function on the implementation it will fail with an error indicating the + * proxy admin cannot fallback to the target implementation. * - * These properties mean that the admin account can only be used for admin actions like upgrading the proxy or changing - * the admin, so it's best if it's a dedicated account that is not used for anything else. This will avoid headaches due - * to sudden errors when trying to call a function from the proxy implementation. + * These properties mean that the admin account can only be used for upgrading the proxy, so it's best if it's a dedicated + * account that is not used for anything else. This will avoid headaches due to sudden errors when trying to call a function + * from the proxy implementation. * * Our recommendation is for the dedicated account to be an instance of the {ProxyAdmin} contract. If set up this way, - * you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy. + * you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy, which extends from the + * {Ownable} contract to allow for changing the proxy's admin owner. * * NOTE: The real interface of this proxy is that defined in `ITransparentUpgradeableProxy`. This contract does not * inherit from that interface, and instead the admin functions are implicitly implemented using a custom dispatch @@ -47,12 +46,23 @@ interface ITransparentUpgradeableProxy is IERC1967 { * fully implement transparency without decoding reverts caused by selector clashes between the proxy and the * implementation. * + * IMPORTANT: This contract avoids unnecessary storage reads by setting the admin only during construction as an immutable variable, + * preventing any changes thereafter. However, the admin slot defined in ERC-1967 can still be overwritten by the implementation + * logic pointed to by this proxy. In such cases, the contract may end up in an undesirable state where the admin slot is different + * from the actual admin. + * * WARNING: It is not recommended to extend this contract to add additional external functions. If you do so, the compiler * will not check that there are no selector conflicts, due to the note above. A selector clash between any new function * and the functions declared in {ITransparentUpgradeableProxy} will be resolved in favor of the new one. This could * render the admin operations inaccessible, which could prevent upgradeability. Transparency may also be compromised. */ contract TransparentUpgradeableProxy is ERC1967Proxy { + // An immutable address for the admin avoid unnecessary SLOADs before each call + // at the expense of removing the ability to change the admin once it's set. + // This is acceptable if the admin is always a ProxyAdmin instance or similar contract + // with its own ability to transfer the permissions to another account. + address private immutable _admin; + /** * @dev The proxy caller is the current admin, and can't fallback to the proxy target. */ @@ -68,6 +78,8 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { * optionally initialized with `_data` as explained in {ERC1967Proxy-constructor}. */ constructor(address _logic, address admin_, bytes memory _data) payable ERC1967Proxy(_logic, _data) { + _admin = admin_; + // Set the storage value and emit an event for ERC-1967 compatibility ERC1967Utils.changeAdmin(admin_); } @@ -75,15 +87,13 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { * @dev If caller is the admin process the call internally, otherwise transparently fallback to the proxy behavior */ function _fallback() internal virtual override { - if (msg.sender == ERC1967Utils.getAdmin()) { + if (msg.sender == _admin) { bytes memory ret; bytes4 selector = msg.sig; if (selector == ITransparentUpgradeableProxy.upgradeTo.selector) { ret = _dispatchUpgradeTo(); } else if (selector == ITransparentUpgradeableProxy.upgradeToAndCall.selector) { ret = _dispatchUpgradeToAndCall(); - } else if (selector == ITransparentUpgradeableProxy.changeAdmin.selector) { - ret = _dispatchChangeAdmin(); } else { revert ProxyDeniedAdminAccess(); } @@ -95,20 +105,6 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { } } - /** - * @dev Changes the admin of the proxy. - * - * Emits an {AdminChanged} event. - */ - function _dispatchChangeAdmin() private returns (bytes memory) { - _requireZeroValue(); - - address newAdmin = abi.decode(msg.data[4:], (address)); - ERC1967Utils.changeAdmin(newAdmin); - - return ""; - } - /** * @dev Upgrade the implementation of the proxy. */ diff --git a/test/proxy/transparent/ProxyAdmin.test.js b/test/proxy/transparent/ProxyAdmin.test.js index d660ffc56..23f7ce9b2 100644 --- a/test/proxy/transparent/ProxyAdmin.test.js +++ b/test/proxy/transparent/ProxyAdmin.test.js @@ -6,11 +6,11 @@ const ProxyAdmin = artifacts.require('ProxyAdmin'); const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy'); const ITransparentUpgradeableProxy = artifacts.require('ITransparentUpgradeableProxy'); -const { getAddressInSlot, ImplementationSlot, AdminSlot } = require('../../helpers/erc1967'); +const { getAddressInSlot, ImplementationSlot } = require('../../helpers/erc1967'); const { expectRevertCustomError } = require('../../helpers/customError'); contract('ProxyAdmin', function (accounts) { - const [proxyAdminOwner, newAdmin, anotherAccount] = accounts; + const [proxyAdminOwner, anotherAccount] = accounts; before('set implementations', async function () { this.implementationV1 = await ImplV1.new(); @@ -32,23 +32,6 @@ contract('ProxyAdmin', function (accounts) { expect(await this.proxyAdmin.owner()).to.equal(proxyAdminOwner); }); - describe('#changeProxyAdmin', function () { - it('fails to change proxy admin if its not the proxy owner', async function () { - await expectRevertCustomError( - this.proxyAdmin.changeProxyAdmin(this.proxy.address, newAdmin, { from: anotherAccount }), - 'OwnableUnauthorizedAccount', - [anotherAccount], - ); - }); - - it('changes proxy admin', async function () { - await this.proxyAdmin.changeProxyAdmin(this.proxy.address, newAdmin, { from: proxyAdminOwner }); - - const newProxyAdmin = await getAddressInSlot(this.proxy, AdminSlot); - expect(newProxyAdmin).to.be.equal(newAdmin); - }); - }); - describe('#upgrade', function () { context('with unauthorized account', function () { it('fails to upgrade', async function () { diff --git a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js index c6e949156..1a03b84db 100644 --- a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js +++ b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js @@ -47,6 +47,32 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx }); }); + describe('proxy admin', function () { + it('emits AdminChanged event during construction', async function () { + expectEvent.inConstruction(this.proxy, 'AdminChanged', { + previousAdmin: ZERO_ADDRESS, + newAdmin: proxyAdminAddress, + }); + }); + + it('sets the admin in the storage', async function () { + expect(await getAddressInSlot(this.proxy, AdminSlot)).to.be.equal(proxyAdminAddress); + }); + + it('can overwrite the admin by the implementation', async function () { + const dummy = new DummyImplementation(this.proxyAddress); + await dummy.unsafeOverrideAdmin(anotherAccount); + const ERC1967AdminSlotValue = await getAddressInSlot(this.proxy, AdminSlot); + expect(ERC1967AdminSlotValue).to.be.equal(anotherAccount); + + // Still allows previous admin to execute admin operations + expect(ERC1967AdminSlotValue).to.not.equal(proxyAdminAddress); + expectEvent(await this.proxy.upgradeTo(this.implementationV1, { from: proxyAdminAddress }), 'Upgraded', { + implementation: this.implementationV1, + }); + }); + }); + describe('upgradeTo', function () { describe('when the sender is the admin', function () { const from = proxyAdminAddress; @@ -258,51 +284,14 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx }); }); - describe('changeAdmin', function () { - describe('when the new proposed admin is not the zero address', function () { - const newAdmin = anotherAccount; - - describe('when the sender is the admin', function () { - beforeEach('transferring', async function () { - this.receipt = await this.proxy.changeAdmin(newAdmin, { from: proxyAdminAddress }); - }); - - it('assigns new proxy admin', async function () { - const newProxyAdmin = await getAddressInSlot(this.proxy, AdminSlot); - expect(newProxyAdmin).to.be.equal(anotherAccount); - }); - - it('emits an event', function () { - expectEvent(this.receipt, 'AdminChanged', { - previousAdmin: proxyAdminAddress, - newAdmin: newAdmin, - }); - }); - }); - - describe('when the sender is not the admin', function () { - it('reverts', async function () { - await expectRevert.unspecified(this.proxy.changeAdmin(newAdmin, { from: anotherAccount })); - }); - }); - }); - - describe('when the new proposed admin is the zero address', function () { - it('reverts', async function () { - await expectRevertCustomError( - this.proxy.changeAdmin(ZERO_ADDRESS, { from: proxyAdminAddress }), - 'ERC1967InvalidAdmin', - [ZERO_ADDRESS], - ); - }); - }); - }); - describe('transparent proxy', function () { beforeEach('creating proxy', async function () { const initializeData = Buffer.from(''); - this.impl = await ClashingImplementation.new(); - this.proxy = await createProxy(this.impl.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner }); + this.clashingImplV0 = (await ClashingImplementation.new()).address; + this.clashingImplV1 = (await ClashingImplementation.new()).address; + this.proxy = await createProxy(this.clashingImplV0, proxyAdminAddress, initializeData, { + from: proxyAdminOwner, + }); this.clashing = new ClashingImplementation(this.proxy.address); }); @@ -315,24 +304,28 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx }); describe('when function names clash', function () { - it('when sender is proxy admin should run the proxy function', async function () { - const receipt = await this.proxy.changeAdmin(anotherAccount, { from: proxyAdminAddress, value: 0 }); - expectEvent(receipt, 'AdminChanged'); + it('executes the proxy function if the sender is the admin', async function () { + const receipt = await this.proxy.upgradeTo(this.clashingImplV1, { from: proxyAdminAddress, value: 0 }); + expectEvent(receipt, 'Upgraded', { implementation: this.clashingImplV1 }); }); - it('when sender is other should delegate to implementation', async function () { - const receipt = await this.proxy.changeAdmin(anotherAccount, { from: anotherAccount, value: 0 }); - expectEvent.notEmitted(receipt, 'AdminChanged'); + it('delegates the call to implementation when sender is not the admin', async function () { + const receipt = await this.proxy.upgradeTo(this.clashingImplV1, { from: anotherAccount, value: 0 }); + expectEvent.notEmitted(receipt, 'Upgraded'); expectEvent.inTransaction(receipt.tx, this.clashing, 'ClashingImplementationCall'); }); - it('when sender is proxy admin value should not be accepted', async function () { - await expectRevert.unspecified(this.proxy.changeAdmin(anotherAccount, { from: proxyAdminAddress, value: 1 })); + it('requires 0 value calling upgradeTo by proxy admin', async function () { + await expectRevertCustomError( + this.proxy.upgradeTo(this.clashingImplV1, { from: proxyAdminAddress, value: 1 }), + 'ProxyNonPayableFunction', + [], + ); }); - it('when sender is other value should be accepted', async function () { - const receipt = await this.proxy.changeAdmin(anotherAccount, { from: anotherAccount, value: 1 }); - expectEvent.notEmitted(receipt, 'AdminChanged'); + it('allows calling with value if sender is not the admin', async function () { + const receipt = await this.proxy.upgradeTo(this.clashingImplV1, { from: anotherAccount, value: 1 }); + expectEvent.notEmitted(receipt, 'Upgraded'); expectEvent.inTransaction(receipt.tx, this.clashing, 'ClashingImplementationCall'); }); }); From 365aca6d60bbb99f725e7a5086ae5219c6c6bd8b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 20 Jun 2023 05:20:02 +0200 Subject: [PATCH 185/197] Improve tests for ERC1155 and ERC721 error handling (#3781) Co-authored-by: Francisco --- contracts/mocks/token/ERC1155ReceiverMock.sol | 44 +-- contracts/mocks/token/ERC721ReceiverMock.sol | 21 +- test/token/ERC1155/ERC1155.behavior.js | 299 +++++++++--------- test/token/ERC721/ERC721.behavior.js | 60 +++- 4 files changed, 237 insertions(+), 187 deletions(-) diff --git a/contracts/mocks/token/ERC1155ReceiverMock.sol b/contracts/mocks/token/ERC1155ReceiverMock.sol index c4cdbf20e..a5d7233cf 100644 --- a/contracts/mocks/token/ERC1155ReceiverMock.sol +++ b/contracts/mocks/token/ERC1155ReceiverMock.sol @@ -8,26 +8,24 @@ import "../../utils/introspection/ERC165.sol"; contract ERC1155ReceiverMock is ERC165, IERC1155Receiver { enum RevertType { None, - Empty, - String, - Custom + RevertWithoutMessage, + RevertWithMessage, + RevertWithCustomError, + Panic } - bytes4 private _recRetval; - RevertType private _recReverts; - bytes4 private _batRetval; - RevertType private _batReverts; + bytes4 private immutable _recRetval; + bytes4 private immutable _batRetval; + RevertType private immutable _error; event Received(address operator, address from, uint256 id, uint256 value, bytes data, uint256 gas); event BatchReceived(address operator, address from, uint256[] ids, uint256[] values, bytes data, uint256 gas); + error CustomError(bytes4); - error ERC1155ReceiverMockError(); - - constructor(bytes4 recRetval, RevertType recReverts, bytes4 batRetval, RevertType batReverts) { + constructor(bytes4 recRetval, bytes4 batRetval, RevertType error) { _recRetval = recRetval; - _recReverts = recReverts; _batRetval = batRetval; - _batReverts = batReverts; + _error = error; } function onERC1155Received( @@ -37,12 +35,15 @@ contract ERC1155ReceiverMock is ERC165, IERC1155Receiver { uint256 value, bytes calldata data ) external returns (bytes4) { - if (_recReverts == RevertType.Empty) { + if (_error == RevertType.RevertWithoutMessage) { revert(); - } else if (_recReverts == RevertType.String) { + } else if (_error == RevertType.RevertWithMessage) { revert("ERC1155ReceiverMock: reverting on receive"); - } else if (_recReverts == RevertType.Custom) { - revert ERC1155ReceiverMockError(); + } else if (_error == RevertType.RevertWithCustomError) { + revert CustomError(_recRetval); + } else if (_error == RevertType.Panic) { + uint256 a = uint256(0) / uint256(0); + a; } emit Received(operator, from, id, value, data, gasleft()); @@ -56,12 +57,15 @@ contract ERC1155ReceiverMock is ERC165, IERC1155Receiver { uint256[] calldata values, bytes calldata data ) external returns (bytes4) { - if (_batReverts == RevertType.Empty) { + if (_error == RevertType.RevertWithoutMessage) { revert(); - } else if (_batReverts == RevertType.String) { + } else if (_error == RevertType.RevertWithMessage) { revert("ERC1155ReceiverMock: reverting on batch receive"); - } else if (_batReverts == RevertType.Custom) { - revert ERC1155ReceiverMockError(); + } else if (_error == RevertType.RevertWithCustomError) { + revert CustomError(_recRetval); + } else if (_error == RevertType.Panic) { + uint256 a = uint256(0) / uint256(0); + a; } emit BatchReceived(operator, from, ids, values, data, gasleft()); diff --git a/contracts/mocks/token/ERC721ReceiverMock.sol b/contracts/mocks/token/ERC721ReceiverMock.sol index e97ceacf3..416613174 100644 --- a/contracts/mocks/token/ERC721ReceiverMock.sol +++ b/contracts/mocks/token/ERC721ReceiverMock.sol @@ -5,19 +5,21 @@ pragma solidity ^0.8.19; import "../../token/ERC721/IERC721Receiver.sol"; contract ERC721ReceiverMock is IERC721Receiver { - enum Error { + enum RevertType { None, - RevertWithMessage, RevertWithoutMessage, + RevertWithMessage, + RevertWithCustomError, Panic } bytes4 private immutable _retval; - Error private immutable _error; + RevertType private immutable _error; event Received(address operator, address from, uint256 tokenId, bytes data, uint256 gas); + error CustomError(bytes4); - constructor(bytes4 retval, Error error) { + constructor(bytes4 retval, RevertType error) { _retval = retval; _error = error; } @@ -28,14 +30,17 @@ contract ERC721ReceiverMock is IERC721Receiver { uint256 tokenId, bytes memory data ) public returns (bytes4) { - if (_error == Error.RevertWithMessage) { - revert("ERC721ReceiverMock: reverting"); - } else if (_error == Error.RevertWithoutMessage) { + if (_error == RevertType.RevertWithoutMessage) { revert(); - } else if (_error == Error.Panic) { + } else if (_error == RevertType.RevertWithMessage) { + revert("ERC721ReceiverMock: reverting"); + } else if (_error == RevertType.RevertWithCustomError) { + revert CustomError(_retval); + } else if (_error == RevertType.Panic) { uint256 a = uint256(0) / uint256(0); a; } + emit Received(operator, from, tokenId, data, gasleft()); return _retval; } diff --git a/test/token/ERC1155/ERC1155.behavior.js b/test/token/ERC1155/ERC1155.behavior.js index 36f67d321..3391be4c8 100644 --- a/test/token/ERC1155/ERC1155.behavior.js +++ b/test/token/ERC1155/ERC1155.behavior.js @@ -1,14 +1,13 @@ const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS } = constants; - const { expect } = require('chai'); +const { ZERO_ADDRESS } = constants; const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); const { expectRevertCustomError } = require('../../helpers/customError'); const { Enum } = require('../../helpers/enums'); const ERC1155ReceiverMock = artifacts.require('ERC1155ReceiverMock'); -const RevertType = Enum('None', 'Empty', 'String', 'Custom'); +const RevertType = Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'); function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, multiTokenHolder, recipient, proxy]) { const firstTokenId = new BN(1); @@ -298,7 +297,6 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m beforeEach(async function () { this.receiver = await ERC1155ReceiverMock.new( RECEIVER_SINGLE_MAGIC_VALUE, - RevertType.None, RECEIVER_BATCH_MAGIC_VALUE, RevertType.None, ); @@ -372,12 +370,7 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m context('to a receiver contract returning unexpected value', function () { beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( - '0x00c0ffee', - RevertType.None, - RECEIVER_BATCH_MAGIC_VALUE, - RevertType.None, - ); + this.receiver = await ERC1155ReceiverMock.new('0x00c0ffee', RECEIVER_BATCH_MAGIC_VALUE, RevertType.None); }); it('reverts', async function () { @@ -392,54 +385,81 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); context('to a receiver contract that reverts', function () { - it('with empty reason', async function () { - const receiver = await ERC1155ReceiverMock.new( - RECEIVER_SINGLE_MAGIC_VALUE, - RevertType.Empty, - RECEIVER_BATCH_MAGIC_VALUE, - RevertType.None, - ); + context('with a revert string', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.RevertWithMessage, + ); + }); - await expectRevertCustomError( - this.token.safeTransferFrom(multiTokenHolder, receiver.address, firstTokenId, firstAmount, '0x', { - from: multiTokenHolder, - }), - 'ERC1155InvalidReceiver', - [receiver.address], - ); + it('reverts', async function () { + await expectRevert( + this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { + from: multiTokenHolder, + }), + 'ERC1155ReceiverMock: reverting on receive', + ); + }); }); - it('with reason string', async function () { - const receiver = await ERC1155ReceiverMock.new( - RECEIVER_SINGLE_MAGIC_VALUE, - RevertType.String, - RECEIVER_BATCH_MAGIC_VALUE, - RevertType.None, - ); + context('without a revert string', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.RevertWithoutMessage, + ); + }); - await expectRevert( - this.token.safeTransferFrom(multiTokenHolder, receiver.address, firstTokenId, firstAmount, '0x', { - from: multiTokenHolder, - }), - 'ERC1155ReceiverMock: reverting on receive', - ); + it('reverts', async function () { + await expectRevertCustomError( + this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { + from: multiTokenHolder, + }), + 'ERC1155InvalidReceiver', + [this.receiver.address], + ); + }); }); - it('with custom error', async function () { - const receiver = await ERC1155ReceiverMock.new( - RECEIVER_SINGLE_MAGIC_VALUE, - RevertType.Custom, - RECEIVER_BATCH_MAGIC_VALUE, - RevertType.None, - ); + context('with a custom error', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.RevertWithCustomError, + ); + }); - await expectRevertCustomError( - this.token.safeTransferFrom(multiTokenHolder, receiver.address, firstTokenId, firstAmount, '0x', { - from: multiTokenHolder, - }), - 'ERC1155ReceiverMockError', - [], - ); + it('reverts', async function () { + await expectRevertCustomError( + this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { + from: multiTokenHolder, + }), + 'CustomError', + [RECEIVER_SINGLE_MAGIC_VALUE], + ); + }); + }); + + context('with a panic', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.Panic, + ); + }); + + it('reverts', async function () { + await expectRevert.unspecified( + this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { + from: multiTokenHolder, + }), + ); + }); }); }); @@ -629,7 +649,6 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m beforeEach(async function () { this.receiver = await ERC1155ReceiverMock.new( RECEIVER_SINGLE_MAGIC_VALUE, - RevertType.None, RECEIVER_BATCH_MAGIC_VALUE, RevertType.None, ); @@ -705,7 +724,6 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m beforeEach(async function () { this.receiver = await ERC1155ReceiverMock.new( RECEIVER_SINGLE_MAGIC_VALUE, - RevertType.None, RECEIVER_SINGLE_MAGIC_VALUE, RevertType.None, ); @@ -728,107 +746,100 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); context('to a receiver contract that reverts', function () { - it('with empty reason', async function () { - const receiver = await ERC1155ReceiverMock.new( - RECEIVER_SINGLE_MAGIC_VALUE, - RevertType.None, - RECEIVER_BATCH_MAGIC_VALUE, - RevertType.Empty, - ); + context('with a revert string', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.RevertWithMessage, + ); + }); - await expectRevertCustomError( - this.token.safeBatchTransferFrom( - multiTokenHolder, - receiver.address, - [firstTokenId, secondTokenId], - [firstAmount, secondAmount], - '0x', - { from: multiTokenHolder }, - ), - 'ERC1155InvalidReceiver', - [receiver.address], - ); + it('reverts', async function () { + await expectRevert( + this.token.safeBatchTransferFrom( + multiTokenHolder, + this.receiver.address, + [firstTokenId, secondTokenId], + [firstAmount, secondAmount], + '0x', + { from: multiTokenHolder }, + ), + 'ERC1155ReceiverMock: reverting on batch receive', + ); + }); }); - it('with reason string', async function () { - const receiver = await ERC1155ReceiverMock.new( - RECEIVER_SINGLE_MAGIC_VALUE, - RevertType.None, - RECEIVER_BATCH_MAGIC_VALUE, - RevertType.String, - ); + context('without a revert string', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.RevertWithoutMessage, + ); + }); - await expectRevert( - this.token.safeBatchTransferFrom( - multiTokenHolder, - receiver.address, - [firstTokenId, secondTokenId], - [firstAmount, secondAmount], - '0x', - { from: multiTokenHolder }, - ), - 'ERC1155ReceiverMock: reverting on batch receive', - ); + it('reverts', async function () { + await expectRevertCustomError( + this.token.safeBatchTransferFrom( + multiTokenHolder, + this.receiver.address, + [firstTokenId, secondTokenId], + [firstAmount, secondAmount], + '0x', + { from: multiTokenHolder }, + ), + 'ERC1155InvalidReceiver', + [this.receiver.address], + ); + }); }); - it('with custom error', async function () { - const receiver = await ERC1155ReceiverMock.new( - RECEIVER_SINGLE_MAGIC_VALUE, - RevertType.None, - RECEIVER_BATCH_MAGIC_VALUE, - RevertType.Custom, - ); + context('with a custom error', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.RevertWithCustomError, + ); + }); - await expectRevertCustomError( - this.token.safeBatchTransferFrom( - multiTokenHolder, - receiver.address, - [firstTokenId, secondTokenId], - [firstAmount, secondAmount], - '0x', - { from: multiTokenHolder }, - ), - 'ERC1155ReceiverMockError', - [], - ); - }); - }); - - context('to a receiver contract that reverts only on single transfers', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( - RECEIVER_SINGLE_MAGIC_VALUE, - RevertType.String, - RECEIVER_BATCH_MAGIC_VALUE, - RevertType.None, - ); - - this.toWhom = this.receiver.address; - this.transferReceipt = await this.token.safeBatchTransferFrom( - multiTokenHolder, - this.receiver.address, - [firstTokenId, secondTokenId], - [firstAmount, secondAmount], - '0x', - { from: multiTokenHolder }, - ); - this.transferLogs = this.transferReceipt; + it('reverts', async function () { + await expectRevertCustomError( + this.token.safeBatchTransferFrom( + multiTokenHolder, + this.receiver.address, + [firstTokenId, secondTokenId], + [firstAmount, secondAmount], + '0x', + { from: multiTokenHolder }, + ), + 'CustomError', + [RECEIVER_SINGLE_MAGIC_VALUE], + ); + }); }); - batchTransferWasSuccessful.call(this, { - operator: multiTokenHolder, - from: multiTokenHolder, - ids: [firstTokenId, secondTokenId], - values: [firstAmount, secondAmount], - }); + context('with a panic', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.Panic, + ); + }); - it('calls onERC1155BatchReceived', async function () { - await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'BatchReceived', { - operator: multiTokenHolder, - from: multiTokenHolder, - // ids: [firstTokenId, secondTokenId], - // values: [firstAmount, secondAmount], - data: null, + it('reverts', async function () { + await expectRevert.unspecified( + this.token.safeBatchTransferFrom( + multiTokenHolder, + this.receiver.address, + [firstTokenId, secondTokenId], + [firstAmount, secondAmount], + '0x', + { from: multiTokenHolder }, + ), + ); }); }); }); diff --git a/test/token/ERC721/ERC721.behavior.js b/test/token/ERC721/ERC721.behavior.js index 7df429202..75700f6ab 100644 --- a/test/token/ERC721/ERC721.behavior.js +++ b/test/token/ERC721/ERC721.behavior.js @@ -4,14 +4,12 @@ const { ZERO_ADDRESS } = constants; const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); const { expectRevertCustomError } = require('../../helpers/customError'); +const { Enum } = require('../../helpers/enums'); const ERC721ReceiverMock = artifacts.require('ERC721ReceiverMock'); const NonERC721ReceiverMock = artifacts.require('CallReceiverMock'); -const Error = ['None', 'RevertWithMessage', 'RevertWithoutMessage', 'Panic'].reduce( - (acc, entry, idx) => Object.assign({ [entry]: idx }, acc), - {}, -); +const RevertType = Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'); const firstTokenId = new BN('5042'); const secondTokenId = new BN('79217'); @@ -234,7 +232,7 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper describe('to a valid receiver contract', function () { beforeEach(async function () { - this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.None); + this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.None); this.toWhom = this.receiver.address; }); @@ -284,7 +282,7 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper describe('to a receiver contract returning unexpected value', function () { it('reverts', async function () { - const invalidReceiver = await ERC721ReceiverMock.new('0x42', Error.None); + const invalidReceiver = await ERC721ReceiverMock.new('0x42', RevertType.None); await expectRevertCustomError( this.token.safeTransferFrom(owner, invalidReceiver.address, tokenId, { from: owner }), 'ERC721InvalidReceiver', @@ -295,7 +293,7 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper describe('to a receiver contract that reverts with message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithMessage); + const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithMessage); await expectRevert( this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }), 'ERC721ReceiverMock: reverting', @@ -305,7 +303,10 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper describe('to a receiver contract that reverts without message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithoutMessage); + const revertingReceiver = await ERC721ReceiverMock.new( + RECEIVER_MAGIC_VALUE, + RevertType.RevertWithoutMessage, + ); await expectRevertCustomError( this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }), 'ERC721InvalidReceiver', @@ -314,9 +315,23 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper }); }); + describe('to a receiver contract that reverts with custom error', function () { + it('reverts', async function () { + const revertingReceiver = await ERC721ReceiverMock.new( + RECEIVER_MAGIC_VALUE, + RevertType.RevertWithCustomError, + ); + await expectRevertCustomError( + this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }), + 'CustomError', + [RECEIVER_MAGIC_VALUE], + ); + }); + }); + describe('to a receiver contract that panics', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.Panic); + const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.Panic); await expectRevert.unspecified( this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }), ); @@ -343,7 +358,7 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper describe('via safeMint', function () { // regular minting is tested in ERC721Mintable.test.js and others it('calls onERC721Received — with data', async function () { - this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.None); + this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.None); const receipt = await this.token.$_safeMint(this.receiver.address, tokenId, data); await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', { @@ -354,7 +369,7 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper }); it('calls onERC721Received — without data', async function () { - this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.None); + this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.None); const receipt = await this.token.$_safeMint(this.receiver.address, tokenId); await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', { @@ -365,7 +380,7 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper context('to a receiver contract returning unexpected value', function () { it('reverts', async function () { - const invalidReceiver = await ERC721ReceiverMock.new('0x42', Error.None); + const invalidReceiver = await ERC721ReceiverMock.new('0x42', RevertType.None); await expectRevertCustomError( this.token.$_safeMint(invalidReceiver.address, tokenId), 'ERC721InvalidReceiver', @@ -376,7 +391,7 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper context('to a receiver contract that reverts with message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithMessage); + const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithMessage); await expectRevert( this.token.$_safeMint(revertingReceiver.address, tokenId), 'ERC721ReceiverMock: reverting', @@ -386,7 +401,10 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper context('to a receiver contract that reverts without message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithoutMessage); + const revertingReceiver = await ERC721ReceiverMock.new( + RECEIVER_MAGIC_VALUE, + RevertType.RevertWithoutMessage, + ); await expectRevertCustomError( this.token.$_safeMint(revertingReceiver.address, tokenId), 'ERC721InvalidReceiver', @@ -395,9 +413,21 @@ function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, oper }); }); + context('to a receiver contract that reverts with custom error', function () { + it('reverts', async function () { + const revertingReceiver = await ERC721ReceiverMock.new( + RECEIVER_MAGIC_VALUE, + RevertType.RevertWithCustomError, + ); + await expectRevertCustomError(this.token.$_safeMint(revertingReceiver.address, tokenId), 'CustomError', [ + RECEIVER_MAGIC_VALUE, + ]); + }); + }); + context('to a receiver contract that panics', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.Panic); + const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.Panic); await expectRevert.unspecified(this.token.$_safeMint(revertingReceiver.address, tokenId)); }); }); From 2271e2c58d007894c5fe23c4f03a95f645ac9175 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 20 Jun 2023 10:01:30 +0200 Subject: [PATCH 186/197] Use clones for testing non ERC1967 proxies (#4371) --- test/proxy/utils/UUPSUpgradeable.test.js | 25 ++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/test/proxy/utils/UUPSUpgradeable.test.js b/test/proxy/utils/UUPSUpgradeable.test.js index 95319c305..6a8104248 100644 --- a/test/proxy/utils/UUPSUpgradeable.test.js +++ b/test/proxy/utils/UUPSUpgradeable.test.js @@ -7,7 +7,7 @@ const UUPSUpgradeableMock = artifacts.require('UUPSUpgradeableMock'); const UUPSUpgradeableUnsafeMock = artifacts.require('UUPSUpgradeableUnsafeMock'); const NonUpgradeableMock = artifacts.require('NonUpgradeableMock'); const UUPSUnsupportedProxiableUUID = artifacts.require('UUPSUnsupportedProxiableUUID'); -const Address = artifacts.require('$Address'); +const Clones = artifacts.require('$Clones'); contract('UUPSUpgradeable', function () { before(async function () { @@ -16,7 +16,8 @@ contract('UUPSUpgradeable', function () { this.implUpgradeUnsafe = await UUPSUpgradeableUnsafeMock.new(); this.implUpgradeNonUUPS = await NonUpgradeableMock.new(); this.implUnsupportedUUID = await UUPSUnsupportedProxiableUUID.new(); - this.helper = await Address.new(); + // Used for testing non ERC1967 compliant proxies (clones are proxies that don't use the ERC1967 implementation slot) + this.cloneFactory = await Clones.new(); }); beforeEach(async function () { @@ -65,22 +66,26 @@ contract('UUPSUpgradeable', function () { }); it('calling upgradeTo from a contract that is not an ERC1967 proxy (with the right implementation) reverts', async function () { + const receipt = await this.cloneFactory.$clone(this.implUpgradeOk.address); + const instance = await UUPSUpgradeableMock.at( + receipt.logs.find(({ event }) => event === 'return$clone').args.instance, + ); + await expectRevertCustomError( - this.helper.$functionDelegateCall( - this.implUpgradeOk.address, - this.implUpgradeOk.contract.methods.upgradeTo(this.implUpgradeUnsafe.address).encodeABI(), - ), + instance.upgradeTo(this.implUpgradeUnsafe.address), 'UUPSUnauthorizedCallContext', [], ); }); it('calling upgradeToAndCall from a contract that is not an ERC1967 proxy (with the right implementation) reverts', async function () { + const receipt = await this.cloneFactory.$clone(this.implUpgradeOk.address); + const instance = await UUPSUpgradeableMock.at( + receipt.logs.find(({ event }) => event === 'return$clone').args.instance, + ); + await expectRevertCustomError( - this.helper.$functionDelegateCall( - this.implUpgradeOk.address, - this.implUpgradeOk.contract.methods.upgradeToAndCall(this.implUpgradeUnsafe.address, '0x').encodeABI(), - ), + instance.upgradeToAndCall(this.implUpgradeUnsafe.address, '0x'), 'UUPSUnauthorizedCallContext', [], ); From 9fa550c62f8f086f2d874f4113819e1eaf33f686 Mon Sep 17 00:00:00 2001 From: Francisco Date: Tue, 20 Jun 2023 13:06:39 -0300 Subject: [PATCH 187/197] Fix attempt to delete nonexistent npm tag (#4374) --- scripts/release/workflow/publish.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/release/workflow/publish.sh b/scripts/release/workflow/publish.sh index 41a9975cb..e490e5d00 100644 --- a/scripts/release/workflow/publish.sh +++ b/scripts/release/workflow/publish.sh @@ -2,19 +2,25 @@ set -euo pipefail +PACKAGE_JSON_NAME="$(tar xfO "$TARBALL" package/package.json | jq -r .name)" +PACKAGE_JSON_VERSION="$(tar xfO "$TARBALL" package/package.json | jq -r .version)" + # Intentionally escape $ to avoid interpolation and writing the token to disk echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc # Actual publish npm publish "$TARBALL" --tag "$TAG" +# Clean up tags delete_tag() { - PACKAGE_JSON_NAME="$(tar xfO "$TARBALL" package/package.json | jq -r .name)" npm dist-tag rm "$PACKAGE_JSON_NAME" "$1" } if [ "$TAG" = tmp ]; then delete_tag "$TAG" elif [ "$TAG" = latest ]; then - delete_tag next + # Delete the next tag if it exists and is a prerelease for what is currently being published + if npm dist-tag ls "$PACKAGE_JSON_NAME" | grep -q "next: $PACKAGE_JSON_VERSION"; then + delete_tag next + fi fi From b66c77a1fc7bb43fa07ee0371df07ac3364214bb Mon Sep 17 00:00:00 2001 From: Francisco Date: Tue, 20 Jun 2023 13:06:56 -0300 Subject: [PATCH 188/197] Merge branch 'release-v4.9' into master (#4373) --- .changeset/shy-crews-teach.md | 5 ----- CHANGELOG.md | 4 ++++ contracts/package.json | 2 +- contracts/utils/cryptography/MerkleProof.sol | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 9 insertions(+), 10 deletions(-) delete mode 100644 .changeset/shy-crews-teach.md diff --git a/.changeset/shy-crews-teach.md b/.changeset/shy-crews-teach.md deleted file mode 100644 index 8ab929bf8..000000000 --- a/.changeset/shy-crews-teach.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`MerkleProof`: Fix a bug in `processMultiProof` and `processMultiProofCalldata` that allows proving arbitrary leaves if the tree contains a node with value 0 at depth 1. diff --git a/CHANGELOG.md b/CHANGELOG.md index abbb1b347..bbe7502e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,10 @@ function supportsInterface(bytes4 interfaceId) public view virtual override retu } ``` +## 4.9.2 (2023-06-16) + +- `MerkleProof`: Fix a bug in `processMultiProof` and `processMultiProofCalldata` that allows proving arbitrary leaves if the tree contains a node with value 0 at depth 1. + ## 4.9.1 (2023-06-07) - `Governor`: Add a mechanism to restrict the address of the proposer using a suffix in the description. diff --git a/contracts/package.json b/contracts/package.json index 4711a6b3f..df141192d 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,7 +1,7 @@ { "name": "@openzeppelin/contracts", "description": "Secure Smart Contract library for Solidity", - "version": "4.9.1", + "version": "4.9.2", "files": [ "**/*.sol", "/build/contracts/*.json", diff --git a/contracts/utils/cryptography/MerkleProof.sol b/contracts/utils/cryptography/MerkleProof.sol index 39826d8c6..94586ff7b 100644 --- a/contracts/utils/cryptography/MerkleProof.sol +++ b/contracts/utils/cryptography/MerkleProof.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/MerkleProof.sol) +// OpenZeppelin Contracts (last updated v4.9.2) (utils/cryptography/MerkleProof.sol) pragma solidity ^0.8.19; diff --git a/package-lock.json b/package-lock.json index edf1e9284..d4cb52694 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openzeppelin-solidity", - "version": "4.9.0", + "version": "4.9.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "openzeppelin-solidity", - "version": "4.9.0", + "version": "4.9.2", "license": "MIT", "devDependencies": { "@changesets/changelog-github": "^0.4.8", diff --git a/package.json b/package.json index c44154133..37e8f8710 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openzeppelin-solidity", "description": "Secure Smart Contract library for Solidity", - "version": "4.9.1", + "version": "4.9.2", "files": [ "/contracts/**/*.sol", "/build/contracts/*.json", From dac2457a800aaa3d75b7fc999d0c7c323ec2faff Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 20 Jun 2023 18:34:10 +0200 Subject: [PATCH 189/197] Improve customError testing (#4376) --- test/helpers/customError.js | 71 +++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/test/helpers/customError.js b/test/helpers/customError.js index a193ab0cf..e38170b78 100644 --- a/test/helpers/customError.js +++ b/test/helpers/customError.js @@ -2,42 +2,43 @@ const { expect } = require('chai'); /** Revert handler that supports custom errors. */ async function expectRevertCustomError(promise, expectedErrorName, args) { - try { - await promise; - expect.fail("Expected promise to throw but it didn't"); - } catch (revert) { - 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(/(?\w+)\((?.*)\)/); - if (!match) { - expect.fail(`Couldn't parse "${error}" as a custom error`); - } - // Extract the error name and parameters - const errorName = match.groups.name; - const argMatches = [...match.groups.args.matchAll(/-?\w+/g)]; - - // Assert error name - expect(errorName).to.be.equal( - expectedErrorName, - `Unexpected custom error name (with found args: [${argMatches.map(([a]) => a)}])`, - ); - - // Coerce to string for comparison since `arg` can be either a number or hex. - const sanitizedExpected = args.map(arg => arg.toString().toLowerCase()); - const sanitizedActual = argMatches.map(([arg]) => arg.toString().toLowerCase()); - - // Assert argument equality - expect(sanitizedActual).to.have.members(sanitizedExpected, `Unexpected ${errorName} arguments`); + if (!Array.isArray(args)) { + expect.fail('Expected 3rd array parameter for error arguments'); } + + await promise.then( + () => expect.fail("Expected promise to throw but it didn't"), + ({ message }) => { + // The revert message for custom errors looks like: + // VM Exception while processing transaction: + // reverted with custom error 'InvalidAccountNonce("0x70997970C51812dc3A010C7d01b50e0d17dc79C8", 0)' + + // We trim out anything inside the single quotes as comma-separated values + const [, error] = message.match(/'(.*)'/); + + // Attempt to parse as an error + const match = error.match(/(?\w+)\((?.*)\)/); + if (!match) { + expect.fail(`Couldn't parse "${error}" as a custom error`); + } + // Extract the error name and parameters + const errorName = match.groups.name; + const argMatches = [...match.groups.args.matchAll(/-?\w+/g)]; + + // Assert error name + expect(errorName).to.be.equal( + expectedErrorName, + `Unexpected custom error name (with found args: [${argMatches.map(([a]) => a)}])`, + ); + + // Coerce to string for comparison since `arg` can be either a number or hex. + const sanitizedExpected = args.map(arg => arg.toString().toLowerCase()); + const sanitizedActual = argMatches.map(([arg]) => arg.toString().toLowerCase()); + + // Assert argument equality + expect(sanitizedActual).to.have.members(sanitizedExpected, `Unexpected ${errorName} arguments`); + }, + ); } module.exports = { From 6ddacdbde856e203e222e3adc461dccce0c2930b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 20 Jun 2023 21:38:03 +0200 Subject: [PATCH 190/197] Cleanup timelockId on execution for gas refund (#4118) Co-authored-by: Francisco --- .changeset/hot-plums-approve.md | 5 +++++ .../extensions/GovernorTimelockControl.sol | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 .changeset/hot-plums-approve.md diff --git a/.changeset/hot-plums-approve.md b/.changeset/hot-plums-approve.md new file mode 100644 index 000000000..131559027 --- /dev/null +++ b/.changeset/hot-plums-approve.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`GovernorTimelockControl`: Clean up timelock id on execution for gas refund. diff --git a/contracts/governance/extensions/GovernorTimelockControl.sol b/contracts/governance/extensions/GovernorTimelockControl.sol index 888406ba7..fadbcc701 100644 --- a/contracts/governance/extensions/GovernorTimelockControl.sol +++ b/contracts/governance/extensions/GovernorTimelockControl.sol @@ -60,11 +60,13 @@ abstract contract GovernorTimelockControl is IGovernorTimelock, Governor { bytes32 queueid = _timelockIds[proposalId]; if (queueid == bytes32(0)) { return currentState; - } else if (_timelock.isOperationDone(queueid)) { - return ProposalState.Executed; } else if (_timelock.isOperationPending(queueid)) { return ProposalState.Queued; + } else if (_timelock.isOperationDone(queueid)) { + // This can happen if the proposal is executed directly on the timelock. + return ProposalState.Executed; } else { + // This can happen if the proposal is canceled directly on the timelock. return ProposalState.Canceled; } } @@ -117,13 +119,16 @@ abstract contract GovernorTimelockControl is IGovernorTimelock, Governor { * @dev Overridden execute function that run the already queued proposal through the timelock. */ function _execute( - uint256 /* proposalId */, + uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash ) internal virtual override { + // execute _timelock.executeBatch{value: msg.value}(targets, values, calldatas, 0, descriptionHash); + // cleanup for refund + delete _timelockIds[proposalId]; } /** @@ -140,9 +145,12 @@ abstract contract GovernorTimelockControl is IGovernorTimelock, Governor { bytes32 descriptionHash ) internal virtual override returns (uint256) { uint256 proposalId = super._cancel(targets, values, calldatas, descriptionHash); + bytes32 timelockId = _timelockIds[proposalId]; - if (_timelockIds[proposalId] != 0) { - _timelock.cancel(_timelockIds[proposalId]); + if (timelockId != 0) { + // cancel + _timelock.cancel(timelockId); + // cleanup delete _timelockIds[proposalId]; } From 1e0e4e20bb3a73436d3b02a9a9d3e607ea89a931 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 22 Jun 2023 18:41:56 +0200 Subject: [PATCH 191/197] Do not emit Approval event when calling transferFrom (#4370) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García Co-authored-by: Francisco --- .changeset/heavy-drinks-fail.md | 5 + contracts/mocks/token/ERC20ApprovalMock.sol | 10 + contracts/token/ERC20/ERC20.sol | 27 +- test/token/ERC20/ERC20.behavior.js | 26 +- test/token/ERC20/ERC20.test.js | 576 +++++++++--------- .../ERC20/extensions/ERC20Wrapper.test.js | 20 +- 6 files changed, 357 insertions(+), 307 deletions(-) create mode 100644 .changeset/heavy-drinks-fail.md create mode 100644 contracts/mocks/token/ERC20ApprovalMock.sol diff --git a/.changeset/heavy-drinks-fail.md b/.changeset/heavy-drinks-fail.md new file mode 100644 index 000000000..bbe93ca90 --- /dev/null +++ b/.changeset/heavy-drinks-fail.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`ERC20`: Remove `Approval` event previously emitted in `transferFrom` to indicate that part of the allowance was consumed. With this change, allowances are no longer reconstructible from events. See the code for guidelines on how to re-enable this event if needed. diff --git a/contracts/mocks/token/ERC20ApprovalMock.sol b/contracts/mocks/token/ERC20ApprovalMock.sol new file mode 100644 index 000000000..da8f51e1e --- /dev/null +++ b/contracts/mocks/token/ERC20ApprovalMock.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../../token/ERC20/ERC20.sol"; + +abstract contract ERC20ApprovalMock is ERC20 { + function _approve(address owner, address spender, uint256 amount, bool) internal virtual override { + super._approve(owner, spender, amount, true); + } +} diff --git a/contracts/token/ERC20/ERC20.sol b/contracts/token/ERC20/ERC20.sol index aacfe79b7..5ddebf22b 100644 --- a/contracts/token/ERC20/ERC20.sol +++ b/contracts/token/ERC20/ERC20.sol @@ -311,6 +311,27 @@ abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { * - `spender` cannot be the zero address. */ function _approve(address owner, address spender, uint256 amount) internal virtual { + _approve(owner, spender, amount, true); + } + + /** + * @dev Alternative version of {_approve} with an optional flag that can enable or disable the Approval event. + * + * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by + * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any + * `Approval` event during `transferFrom` operations. + * + * Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to true + * using the following override: + * ``` + * function _approve(address owner, address spender, uint256 amount, bool) internal virtual override { + * super._approve(owner, spender, amount, true); + * } + * ``` + * + * Requirements are the same as {_approve}. + */ + function _approve(address owner, address spender, uint256 amount, bool emitEvent) internal virtual { if (owner == address(0)) { revert ERC20InvalidApprover(address(0)); } @@ -318,7 +339,9 @@ abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { revert ERC20InvalidSpender(address(0)); } _allowances[owner][spender] = amount; - emit Approval(owner, spender, amount); + if (emitEvent) { + emit Approval(owner, spender, amount); + } } /** @@ -336,7 +359,7 @@ abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { revert ERC20InsufficientAllowance(spender, currentAllowance, amount); } unchecked { - _approve(owner, spender, currentAllowance - amount); + _approve(owner, spender, currentAllowance - amount, false); } } } diff --git a/test/token/ERC20/ERC20.behavior.js b/test/token/ERC20/ERC20.behavior.js index 7f547b112..bb2efda89 100644 --- a/test/token/ERC20/ERC20.behavior.js +++ b/test/token/ERC20/ERC20.behavior.js @@ -4,7 +4,10 @@ const { ZERO_ADDRESS, MAX_UINT256 } = constants; const { expectRevertCustomError } = require('../../helpers/customError'); -function shouldBehaveLikeERC20(initialSupply, initialHolder, recipient, anotherAccount) { +function shouldBehaveLikeERC20(initialSupply, accounts, opts = {}) { + const [initialHolder, recipient, anotherAccount] = accounts; + const { forcedApproval } = opts; + describe('total supply', function () { it('returns the total amount of tokens', async function () { expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply); @@ -70,13 +73,22 @@ function shouldBehaveLikeERC20(initialSupply, initialHolder, recipient, anotherA }); }); - it('emits an approval event', async function () { - expectEvent(await this.token.transferFrom(tokenOwner, to, amount, { from: spender }), 'Approval', { - owner: tokenOwner, - spender: spender, - value: await this.token.allowance(tokenOwner, spender), + if (forcedApproval) { + it('emits an approval event', async function () { + expectEvent(await this.token.transferFrom(tokenOwner, to, amount, { from: spender }), 'Approval', { + owner: tokenOwner, + spender: spender, + value: await this.token.allowance(tokenOwner, spender), + }); }); - }); + } else { + it('does not emit an approval event', async function () { + expectEvent.notEmitted( + await this.token.transferFrom(tokenOwner, to, amount, { from: spender }), + 'Approval', + ); + }); + } }); describe('when the token owner does not have enough balance', function () { diff --git a/test/token/ERC20/ERC20.test.js b/test/token/ERC20/ERC20.test.js index 7b97c56f1..ef6d82f2b 100644 --- a/test/token/ERC20/ERC20.test.js +++ b/test/token/ERC20/ERC20.test.js @@ -9,357 +9,357 @@ const { } = require('./ERC20.behavior'); const { expectRevertCustomError } = require('../../helpers/customError'); -const ERC20 = artifacts.require('$ERC20'); -const ERC20Decimals = artifacts.require('$ERC20DecimalsMock'); +const TOKENS = [ + { Token: artifacts.require('$ERC20') }, + { Token: artifacts.require('$ERC20ApprovalMock'), forcedApproval: true }, +]; contract('ERC20', function (accounts) { - const [initialHolder, recipient, anotherAccount] = accounts; + const [initialHolder, recipient] = accounts; const name = 'My Token'; const symbol = 'MTKN'; - const initialSupply = new BN(100); - beforeEach(async function () { - this.token = await ERC20.new(name, symbol); - await this.token.$_mint(initialHolder, initialSupply); - }); + for (const { Token, forcedApproval } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + this.token = await Token.new(name, symbol); + await this.token.$_mint(initialHolder, initialSupply); + }); - it('has a name', async function () { - expect(await this.token.name()).to.equal(name); - }); + shouldBehaveLikeERC20(initialSupply, accounts, { forcedApproval }); - it('has a symbol', async function () { - expect(await this.token.symbol()).to.equal(symbol); - }); + it('has a name', async function () { + expect(await this.token.name()).to.equal(name); + }); - it('has 18 decimals', async function () { - expect(await this.token.decimals()).to.be.bignumber.equal('18'); - }); + it('has a symbol', async function () { + expect(await this.token.symbol()).to.equal(symbol); + }); - describe('set decimals', function () { - const decimals = new BN(6); + it('has 18 decimals', async function () { + expect(await this.token.decimals()).to.be.bignumber.equal('18'); + }); - it('can set decimals during construction', async function () { - const token = await ERC20Decimals.new(name, symbol, decimals); - expect(await token.decimals()).to.be.bignumber.equal(decimals); - }); - }); + describe('decrease allowance', function () { + describe('when the spender is not the zero address', function () { + const spender = recipient; - shouldBehaveLikeERC20(initialSupply, initialHolder, recipient, anotherAccount); + function shouldDecreaseApproval(amount) { + describe('when there was no approved amount before', function () { + it('reverts', async function () { + const allowance = await this.token.allowance(initialHolder, spender); + await expectRevertCustomError( + this.token.decreaseAllowance(spender, amount, { from: initialHolder }), + 'ERC20FailedDecreaseAllowance', + [spender, allowance, amount], + ); + }); + }); - describe('decrease allowance', function () { - describe('when the spender is not the zero address', function () { - const spender = recipient; + describe('when the spender had an approved amount', function () { + const approvedAmount = amount; + + beforeEach(async function () { + await this.token.approve(spender, approvedAmount, { from: initialHolder }); + }); + + it('emits an approval event', async function () { + expectEvent( + await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder }), + 'Approval', + { owner: initialHolder, spender: spender, value: new BN(0) }, + ); + }); + + it('decreases the spender allowance subtracting the requested amount', async function () { + await this.token.decreaseAllowance(spender, approvedAmount.subn(1), { from: initialHolder }); + + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('1'); + }); + + it('sets the allowance to zero when all allowance is removed', async function () { + await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder }); + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('0'); + }); + + it('reverts when more than the full allowance is removed', async function () { + await expectRevertCustomError( + this.token.decreaseAllowance(spender, approvedAmount.addn(1), { from: initialHolder }), + 'ERC20FailedDecreaseAllowance', + [spender, approvedAmount, approvedAmount.addn(1)], + ); + }); + }); + } + + describe('when the sender has enough balance', function () { + const amount = initialSupply; + + shouldDecreaseApproval(amount); + }); + + describe('when the sender does not have enough balance', function () { + const amount = initialSupply.addn(1); + + shouldDecreaseApproval(amount); + }); + }); + + describe('when the spender is the zero address', function () { + const amount = initialSupply; + const spender = ZERO_ADDRESS; - function shouldDecreaseApproval(amount) { - describe('when there was no approved amount before', function () { it('reverts', async function () { - const allowance = await this.token.allowance(initialHolder, spender); await expectRevertCustomError( this.token.decreaseAllowance(spender, amount, { from: initialHolder }), 'ERC20FailedDecreaseAllowance', - [spender, allowance, amount], + [spender, 0, amount], ); }); }); + }); - describe('when the spender had an approved amount', function () { - const approvedAmount = amount; - - beforeEach(async function () { - await this.token.approve(spender, approvedAmount, { from: initialHolder }); - }); - - it('emits an approval event', async function () { - expectEvent( - await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder }), - 'Approval', - { owner: initialHolder, spender: spender, value: new BN(0) }, - ); - }); - - it('decreases the spender allowance subtracting the requested amount', async function () { - await this.token.decreaseAllowance(spender, approvedAmount.subn(1), { from: initialHolder }); - - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('1'); - }); - - it('sets the allowance to zero when all allowance is removed', async function () { - await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder }); - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('0'); - }); - - it('reverts when more than the full allowance is removed', async function () { - await expectRevertCustomError( - this.token.decreaseAllowance(spender, approvedAmount.addn(1), { from: initialHolder }), - 'ERC20FailedDecreaseAllowance', - [spender, approvedAmount, approvedAmount.addn(1)], - ); - }); - }); - } - - describe('when the sender has enough balance', function () { + describe('increase allowance', function () { const amount = initialSupply; - shouldDecreaseApproval(amount); - }); + describe('when the spender is not the zero address', function () { + const spender = recipient; - describe('when the sender does not have enough balance', function () { - const amount = initialSupply.addn(1); + describe('when the sender has enough balance', function () { + it('emits an approval event', async function () { + expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', { + owner: initialHolder, + spender: spender, + value: amount, + }); + }); - shouldDecreaseApproval(amount); - }); - }); + describe('when there was no approved amount before', function () { + it('approves the requested amount', async function () { + await this.token.increaseAllowance(spender, amount, { from: initialHolder }); - describe('when the spender is the zero address', function () { - const amount = initialSupply; - const spender = ZERO_ADDRESS; + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount); + }); + }); - it('reverts', async function () { - await expectRevertCustomError( - this.token.decreaseAllowance(spender, amount, { from: initialHolder }), - 'ERC20FailedDecreaseAllowance', - [spender, 0, amount], - ); - }); - }); - }); + describe('when the spender had an approved amount', function () { + beforeEach(async function () { + await this.token.approve(spender, new BN(1), { from: initialHolder }); + }); - describe('increase allowance', function () { - const amount = initialSupply; + it('increases the spender allowance adding the requested amount', async function () { + await this.token.increaseAllowance(spender, amount, { from: initialHolder }); - describe('when the spender is not the zero address', function () { - const spender = recipient; + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1)); + }); + }); + }); - describe('when the sender has enough balance', function () { - it('emits an approval event', async function () { - expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', { - owner: initialHolder, - spender: spender, - value: amount, + describe('when the sender does not have enough balance', function () { + const amount = initialSupply.addn(1); + + it('emits an approval event', async function () { + expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', { + owner: initialHolder, + spender: spender, + value: amount, + }); + }); + + describe('when there was no approved amount before', function () { + it('approves the requested amount', async function () { + await this.token.increaseAllowance(spender, amount, { from: initialHolder }); + + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount); + }); + }); + + describe('when the spender had an approved amount', function () { + beforeEach(async function () { + await this.token.approve(spender, new BN(1), { from: initialHolder }); + }); + + it('increases the spender allowance adding the requested amount', async function () { + await this.token.increaseAllowance(spender, amount, { from: initialHolder }); + + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1)); + }); + }); }); }); - describe('when there was no approved amount before', function () { - it('approves the requested amount', async function () { - await this.token.increaseAllowance(spender, amount, { from: initialHolder }); + describe('when the spender is the zero address', function () { + const spender = ZERO_ADDRESS; - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount); - }); - }); - - describe('when the spender had an approved amount', function () { - beforeEach(async function () { - await this.token.approve(spender, new BN(1), { from: initialHolder }); - }); - - it('increases the spender allowance adding the requested amount', async function () { - await this.token.increaseAllowance(spender, amount, { from: initialHolder }); - - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1)); + it('reverts', async function () { + await expectRevertCustomError( + this.token.increaseAllowance(spender, amount, { from: initialHolder }), + 'ERC20InvalidSpender', + [ZERO_ADDRESS], + ); }); }); }); - describe('when the sender does not have enough balance', function () { - const amount = initialSupply.addn(1); - - it('emits an approval event', async function () { - expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', { - owner: initialHolder, - spender: spender, - value: amount, - }); + describe('_mint', function () { + const amount = new BN(50); + it('rejects a null account', async function () { + await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, amount), 'ERC20InvalidReceiver', [ + ZERO_ADDRESS, + ]); }); - describe('when there was no approved amount before', function () { - it('approves the requested amount', async function () { - await this.token.increaseAllowance(spender, amount, { from: initialHolder }); - - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount); - }); + it('rejects overflow', async function () { + const maxUint256 = new BN('2').pow(new BN(256)).subn(1); + await expectRevert( + this.token.$_mint(recipient, maxUint256), + 'reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)', + ); }); - describe('when the spender had an approved amount', function () { - beforeEach(async function () { - await this.token.approve(spender, new BN(1), { from: initialHolder }); + describe('for a non zero account', function () { + beforeEach('minting', async function () { + this.receipt = await this.token.$_mint(recipient, amount); }); - it('increases the spender allowance adding the requested amount', async function () { - await this.token.increaseAllowance(spender, amount, { from: initialHolder }); - - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1)); - }); - }); - }); - }); - - describe('when the spender is the zero address', function () { - const spender = ZERO_ADDRESS; - - it('reverts', async function () { - await expectRevertCustomError( - this.token.increaseAllowance(spender, amount, { from: initialHolder }), - 'ERC20InvalidSpender', - [ZERO_ADDRESS], - ); - }); - }); - }); - - describe('_mint', function () { - const amount = new BN(50); - it('rejects a null account', async function () { - await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, amount), 'ERC20InvalidReceiver', [ZERO_ADDRESS]); - }); - - it('rejects overflow', async function () { - const maxUint256 = new BN('2').pow(new BN(256)).subn(1); - await expectRevert( - this.token.$_mint(recipient, maxUint256), - 'reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)', - ); - }); - - describe('for a non zero account', function () { - beforeEach('minting', async function () { - this.receipt = await this.token.$_mint(recipient, amount); - }); - - it('increments totalSupply', async function () { - const expectedSupply = initialSupply.add(amount); - expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply); - }); - - it('increments recipient balance', async function () { - expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(amount); - }); - - it('emits Transfer event', async function () { - const event = expectEvent(this.receipt, 'Transfer', { from: ZERO_ADDRESS, to: recipient }); - - expect(event.args.value).to.be.bignumber.equal(amount); - }); - }); - }); - - describe('_burn', function () { - it('rejects a null account', async function () { - await expectRevertCustomError(this.token.$_burn(ZERO_ADDRESS, new BN(1)), 'ERC20InvalidSender', [ZERO_ADDRESS]); - }); - - describe('for a non zero account', function () { - it('rejects burning more than balance', async function () { - await expectRevertCustomError( - this.token.$_burn(initialHolder, initialSupply.addn(1)), - 'ERC20InsufficientBalance', - [initialHolder, initialSupply, initialSupply.addn(1)], - ); - }); - - const describeBurn = function (description, amount) { - describe(description, function () { - beforeEach('burning', async function () { - this.receipt = await this.token.$_burn(initialHolder, amount); - }); - - it('decrements totalSupply', async function () { - const expectedSupply = initialSupply.sub(amount); + it('increments totalSupply', async function () { + const expectedSupply = initialSupply.add(amount); expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply); }); - it('decrements initialHolder balance', async function () { - const expectedBalance = initialSupply.sub(amount); - expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(expectedBalance); + it('increments recipient balance', async function () { + expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(amount); }); it('emits Transfer event', async function () { - const event = expectEvent(this.receipt, 'Transfer', { from: initialHolder, to: ZERO_ADDRESS }); + const event = expectEvent(this.receipt, 'Transfer', { from: ZERO_ADDRESS, to: recipient }); expect(event.args.value).to.be.bignumber.equal(amount); }); }); - }; - - describeBurn('for entire balance', initialSupply); - describeBurn('for less amount than balance', initialSupply.subn(1)); - }); - }); - - describe('_update', function () { - const amount = new BN(1); - - it('from is the zero address', async function () { - const balanceBefore = await this.token.balanceOf(initialHolder); - const totalSupply = await this.token.totalSupply(); - - expectEvent(await this.token.$_update(ZERO_ADDRESS, initialHolder, amount), 'Transfer', { - from: ZERO_ADDRESS, - to: initialHolder, - value: amount, }); - expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.add(amount)); - expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.add(amount)); - }); - it('to is the zero address', async function () { - const balanceBefore = await this.token.balanceOf(initialHolder); - const totalSupply = await this.token.totalSupply(); + describe('_burn', function () { + it('rejects a null account', async function () { + await expectRevertCustomError(this.token.$_burn(ZERO_ADDRESS, new BN(1)), 'ERC20InvalidSender', [ + ZERO_ADDRESS, + ]); + }); - expectEvent(await this.token.$_update(initialHolder, ZERO_ADDRESS, amount), 'Transfer', { - from: initialHolder, - to: ZERO_ADDRESS, - value: amount, + describe('for a non zero account', function () { + it('rejects burning more than balance', async function () { + await expectRevertCustomError( + this.token.$_burn(initialHolder, initialSupply.addn(1)), + 'ERC20InsufficientBalance', + [initialHolder, initialSupply, initialSupply.addn(1)], + ); + }); + + const describeBurn = function (description, amount) { + describe(description, function () { + beforeEach('burning', async function () { + this.receipt = await this.token.$_burn(initialHolder, amount); + }); + + it('decrements totalSupply', async function () { + const expectedSupply = initialSupply.sub(amount); + expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply); + }); + + it('decrements initialHolder balance', async function () { + const expectedBalance = initialSupply.sub(amount); + expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(expectedBalance); + }); + + it('emits Transfer event', async function () { + const event = expectEvent(this.receipt, 'Transfer', { from: initialHolder, to: ZERO_ADDRESS }); + + expect(event.args.value).to.be.bignumber.equal(amount); + }); + }); + }; + + describeBurn('for entire balance', initialSupply); + describeBurn('for less amount than balance', initialSupply.subn(1)); + }); }); - expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.sub(amount)); - expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.sub(amount)); - }); - it('from and to are the zero address', async function () { - const totalSupply = await this.token.totalSupply(); + describe('_update', function () { + const amount = new BN(1); - await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, amount); + it('from is the zero address', async function () { + const balanceBefore = await this.token.balanceOf(initialHolder); + const totalSupply = await this.token.totalSupply(); - expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply); - expectEvent(await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, amount), 'Transfer', { - from: ZERO_ADDRESS, - to: ZERO_ADDRESS, - value: amount, + expectEvent(await this.token.$_update(ZERO_ADDRESS, initialHolder, amount), 'Transfer', { + from: ZERO_ADDRESS, + to: initialHolder, + value: amount, + }); + expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.add(amount)); + expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.add(amount)); + }); + + it('to is the zero address', async function () { + const balanceBefore = await this.token.balanceOf(initialHolder); + const totalSupply = await this.token.totalSupply(); + + expectEvent(await this.token.$_update(initialHolder, ZERO_ADDRESS, amount), 'Transfer', { + from: initialHolder, + to: ZERO_ADDRESS, + value: amount, + }); + expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.sub(amount)); + expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.sub(amount)); + }); + + it('from and to are the zero address', async function () { + const totalSupply = await this.token.totalSupply(); + + await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, amount); + + expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply); + expectEvent(await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, amount), 'Transfer', { + from: ZERO_ADDRESS, + to: ZERO_ADDRESS, + value: amount, + }); + }); + }); + + describe('_transfer', function () { + shouldBehaveLikeERC20Transfer(initialHolder, recipient, initialSupply, function (from, to, amount) { + return this.token.$_transfer(from, to, amount); + }); + + describe('when the sender is the zero address', function () { + it('reverts', async function () { + await expectRevertCustomError( + this.token.$_transfer(ZERO_ADDRESS, recipient, initialSupply), + 'ERC20InvalidSender', + [ZERO_ADDRESS], + ); + }); + }); + }); + + describe('_approve', function () { + shouldBehaveLikeERC20Approve(initialHolder, recipient, initialSupply, function (owner, spender, amount) { + return this.token.$_approve(owner, spender, amount); + }); + + describe('when the owner is the zero address', function () { + it('reverts', async function () { + await expectRevertCustomError( + this.token.$_approve(ZERO_ADDRESS, recipient, initialSupply), + 'ERC20InvalidApprover', + [ZERO_ADDRESS], + ); + }); + }); }); }); - }); - - describe('_transfer', function () { - shouldBehaveLikeERC20Transfer(initialHolder, recipient, initialSupply, function (from, to, amount) { - return this.token.$_transfer(from, to, amount); - }); - - describe('when the sender is the zero address', function () { - it('reverts', async function () { - await expectRevertCustomError( - this.token.$_transfer(ZERO_ADDRESS, recipient, initialSupply), - 'ERC20InvalidSender', - [ZERO_ADDRESS], - ); - }); - }); - }); - - describe('_approve', function () { - shouldBehaveLikeERC20Approve(initialHolder, recipient, initialSupply, function (owner, spender, amount) { - return this.token.$_approve(owner, spender, amount); - }); - - describe('when the owner is the zero address', function () { - it('reverts', async function () { - await expectRevertCustomError( - this.token.$_approve(ZERO_ADDRESS, recipient, initialSupply), - 'ERC20InvalidApprover', - [ZERO_ADDRESS], - ); - }); - }); - }); + } }); diff --git a/test/token/ERC20/extensions/ERC20Wrapper.test.js b/test/token/ERC20/extensions/ERC20Wrapper.test.js index ffb97e8a2..94415d088 100644 --- a/test/token/ERC20/extensions/ERC20Wrapper.test.js +++ b/test/token/ERC20/extensions/ERC20Wrapper.test.js @@ -10,7 +10,7 @@ const ERC20Decimals = artifacts.require('$ERC20DecimalsMock'); const ERC20Wrapper = artifacts.require('$ERC20Wrapper'); contract('ERC20Wrapper', function (accounts) { - const [initialHolder, recipient, anotherAccount] = accounts; + const [initialHolder, receiver] = accounts; const name = 'My Token'; const symbol = 'MTKN'; @@ -85,7 +85,7 @@ contract('ERC20Wrapper', function (accounts) { it('to other account', async function () { await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder }); - const { tx } = await this.token.depositFor(anotherAccount, initialSupply, { from: initialHolder }); + const { tx } = await this.token.depositFor(receiver, initialSupply, { from: initialHolder }); await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { from: initialHolder, to: this.token.address, @@ -93,7 +93,7 @@ contract('ERC20Wrapper', function (accounts) { }); await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: ZERO_ADDRESS, - to: anotherAccount, + to: receiver, value: initialSupply, }); }); @@ -144,10 +144,10 @@ contract('ERC20Wrapper', function (accounts) { }); it('to other account', async function () { - const { tx } = await this.token.withdrawTo(anotherAccount, initialSupply, { from: initialHolder }); + const { tx } = await this.token.withdrawTo(receiver, initialSupply, { from: initialHolder }); await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { from: this.token.address, - to: anotherAccount, + to: receiver, value: initialSupply, }); await expectEvent.inTransaction(tx, this.token, 'Transfer', { @@ -163,10 +163,10 @@ contract('ERC20Wrapper', function (accounts) { await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder }); await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }); - const { tx } = await this.token.$_recover(anotherAccount); + const { tx } = await this.token.$_recover(receiver); await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: ZERO_ADDRESS, - to: anotherAccount, + to: receiver, value: '0', }); }); @@ -174,10 +174,10 @@ contract('ERC20Wrapper', function (accounts) { it('something to recover', async function () { await this.underlying.transfer(this.token.address, initialSupply, { from: initialHolder }); - const { tx } = await this.token.$_recover(anotherAccount); + const { tx } = await this.token.$_recover(receiver); await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: ZERO_ADDRESS, - to: anotherAccount, + to: receiver, value: initialSupply, }); }); @@ -189,6 +189,6 @@ contract('ERC20Wrapper', function (accounts) { await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }); }); - shouldBehaveLikeERC20(initialSupply, initialHolder, recipient, anotherAccount); + shouldBehaveLikeERC20(initialSupply, accounts); }); }); From a7a94c77463acea95d979aae1580fb0ddc3b6a1e Mon Sep 17 00:00:00 2001 From: Sergei Tikhomirov Date: Fri, 23 Jun 2023 00:34:53 +0200 Subject: [PATCH 192/197] Update comment to reflect code logic in Ownable.sol (#4369) --- contracts/access/Ownable.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/access/Ownable.sol b/contracts/access/Ownable.sol index f4394378b..627226cf7 100644 --- a/contracts/access/Ownable.sol +++ b/contracts/access/Ownable.sol @@ -10,8 +10,8 @@ import "../utils/Context.sol"; * there is an account (an owner) that can be granted exclusive access to * specific functions. * - * By default, the owner account will be the one that deploys the contract. This - * can later be changed with {transferOwnership}. + * The initial owner is set to the address provided by the deployer. This can + * later be changed with {transferOwnership}. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be applied to your functions to restrict their use to @@ -33,7 +33,7 @@ abstract contract Ownable is Context { event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** - * @dev Initializes the contract setting the deployer as the initial owner. + * @dev Initializes the contract setting the address provided by the deployer as the initial owner. */ constructor(address initialOwner) { _transferOwnership(initialOwner); From da89c438f1fced499308713e4ad155f8bfae932a Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 23 Jun 2023 18:05:22 +0200 Subject: [PATCH 193/197] Pack Governor's ProposalCore into a single slot (#4268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García Co-authored-by: Francisco --- .changeset/grumpy-bulldogs-call.md | 5 ++++ contracts/governance/Governor.sol | 30 ++++++++----------- contracts/governance/IGovernor.sol | 7 +++++ .../extensions/GovernorPreventLateQuorum.sol | 14 ++++----- .../extensions/GovernorSettings.sol | 17 ++++++----- .../extensions/GovernorTimelockCompound.sol | 4 +-- 6 files changed, 43 insertions(+), 34 deletions(-) create mode 100644 .changeset/grumpy-bulldogs-call.md diff --git a/.changeset/grumpy-bulldogs-call.md b/.changeset/grumpy-bulldogs-call.md new file mode 100644 index 000000000..c034587f3 --- /dev/null +++ b/.changeset/grumpy-bulldogs-call.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`Governor`: Optimized use of storage for proposal data diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index a8654ac35..bae5d62e6 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -34,14 +34,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive // solhint-disable var-name-mixedcase struct ProposalCore { - // --- start retyped from Timers.BlockNumber at offset 0x00 --- - uint64 voteStart; address proposer; - bytes4 __gap_unused0; - // --- start retyped from Timers.BlockNumber at offset 0x20 --- - uint64 voteEnd; - bytes24 __gap_unused1; - // --- Remaining fields starting at offset 0x40 --------------- + uint48 voteStart; + uint32 voteDuration; bool executed; bool canceled; } @@ -166,7 +161,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive * @dev See {IGovernor-state}. */ function state(uint256 proposalId) public view virtual override returns (ProposalState) { - ProposalCore storage proposal = _proposals[proposalId]; + // ProposalCore is just one slot. We can load it from storage to memory with a single sload and use memory + // object as a cache. This avoid duplicating expensive sloads. + ProposalCore memory proposal = _proposals[proposalId]; if (proposal.executed) { return ProposalState.Executed; @@ -219,7 +216,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive * @dev See {IGovernor-proposalDeadline}. */ function proposalDeadline(uint256 proposalId) public view virtual override returns (uint256) { - return _proposals[proposalId].voteEnd; + return _proposals[proposalId].voteStart + _proposals[proposalId].voteDuration; } /** @@ -300,16 +297,14 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive } uint256 snapshot = currentTimepoint + votingDelay(); - uint256 deadline = snapshot + votingPeriod(); + uint256 duration = votingPeriod(); _proposals[proposalId] = ProposalCore({ proposer: proposer, - voteStart: SafeCast.toUint64(snapshot), - voteEnd: SafeCast.toUint64(deadline), + voteStart: SafeCast.toUint48(snapshot), + voteDuration: SafeCast.toUint32(duration), executed: false, - canceled: false, - __gap_unused0: 0, - __gap_unused1: 0 + canceled: false }); emit ProposalCreated( @@ -320,7 +315,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive new string[](targets.length), calldatas, snapshot, - deadline, + snapshot + duration, description ); @@ -592,13 +587,12 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive string memory reason, bytes memory params ) internal virtual returns (uint256) { - ProposalCore storage proposal = _proposals[proposalId]; ProposalState currentState = state(proposalId); if (currentState != ProposalState.Active) { revert GovernorUnexpectedProposalState(proposalId, currentState, _encodeStateBitmap(ProposalState.Active)); } - uint256 weight = _getVotes(account, proposal.voteStart, params); + uint256 weight = _getVotes(account, proposalSnapshot(proposalId), params); _countVote(proposalId, account, support, weight, params); if (params.length == 0) { diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 5b51f088a..0adde6795 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -222,6 +222,9 @@ abstract contract IGovernor is IERC165, IERC6372 { * * This can be increased to leave time for users to buy voting power, or delegate it, before the voting of a * proposal starts. + * + * NOTE: While this interface returns a uint256, timepoints are stored as uint48 following the ERC-6372 clock type. + * Consequently this value must fit in a uint48 (when added to the current clock). See {IERC6372-clock}. */ function votingDelay() public view virtual returns (uint256); @@ -232,6 +235,10 @@ abstract contract IGovernor is IERC165, IERC6372 { * * NOTE: The {votingDelay} can delay the start of the vote. This must be considered when setting the voting * duration compared to the voting delay. + * + * NOTE: This value is stored when the proposal is submitted so that possible changes to the value do not affect + * proposals that have already been submitted. The type used to save it is a uint32. Consequently, while this + * interface returns a uint256, the value it returns should fit in a uint32. */ function votingPeriod() public view virtual returns (uint256); diff --git a/contracts/governance/extensions/GovernorPreventLateQuorum.sol b/contracts/governance/extensions/GovernorPreventLateQuorum.sol index abb81128e..3e730174e 100644 --- a/contracts/governance/extensions/GovernorPreventLateQuorum.sol +++ b/contracts/governance/extensions/GovernorPreventLateQuorum.sol @@ -18,10 +18,10 @@ import "../../utils/math/Math.sol"; * _Available since v4.5._ */ abstract contract GovernorPreventLateQuorum is Governor { - uint64 private _voteExtension; + uint48 private _voteExtension; /// @custom:oz-retyped-from mapping(uint256 => Timers.BlockNumber) - mapping(uint256 => uint64) private _extendedDeadlines; + mapping(uint256 => uint48) private _extendedDeadlines; /// @dev Emitted when a proposal deadline is pushed back due to reaching quorum late in its voting period. event ProposalExtended(uint256 indexed proposalId, uint64 extendedDeadline); @@ -34,7 +34,7 @@ abstract contract GovernorPreventLateQuorum is Governor { * clock mode) that is required to pass since the moment a proposal reaches quorum until its voting period ends. If * necessary the voting period will be extended beyond the one set during proposal creation. */ - constructor(uint64 initialVoteExtension) { + constructor(uint48 initialVoteExtension) { _setLateQuorumVoteExtension(initialVoteExtension); } @@ -62,7 +62,7 @@ abstract contract GovernorPreventLateQuorum is Governor { uint256 result = super._castVote(proposalId, account, support, reason, params); if (_extendedDeadlines[proposalId] == 0 && _quorumReached(proposalId)) { - uint64 extendedDeadline = clock() + lateQuorumVoteExtension(); + uint48 extendedDeadline = clock() + lateQuorumVoteExtension(); if (extendedDeadline > proposalDeadline(proposalId)) { emit ProposalExtended(proposalId, extendedDeadline); @@ -78,7 +78,7 @@ abstract contract GovernorPreventLateQuorum is Governor { * @dev Returns the current value of the vote extension parameter: the number of blocks that are required to pass * from the time a proposal reaches quorum until its voting period ends. */ - function lateQuorumVoteExtension() public view virtual returns (uint64) { + function lateQuorumVoteExtension() public view virtual returns (uint48) { return _voteExtension; } @@ -88,7 +88,7 @@ abstract contract GovernorPreventLateQuorum is Governor { * * Emits a {LateQuorumVoteExtensionSet} event. */ - function setLateQuorumVoteExtension(uint64 newVoteExtension) public virtual onlyGovernance { + function setLateQuorumVoteExtension(uint48 newVoteExtension) public virtual onlyGovernance { _setLateQuorumVoteExtension(newVoteExtension); } @@ -98,7 +98,7 @@ abstract contract GovernorPreventLateQuorum is Governor { * * Emits a {LateQuorumVoteExtensionSet} event. */ - function _setLateQuorumVoteExtension(uint64 newVoteExtension) internal virtual { + function _setLateQuorumVoteExtension(uint48 newVoteExtension) internal virtual { emit LateQuorumVoteExtensionSet(_voteExtension, newVoteExtension); _voteExtension = newVoteExtension; } diff --git a/contracts/governance/extensions/GovernorSettings.sol b/contracts/governance/extensions/GovernorSettings.sol index 64e081cc5..6168689ad 100644 --- a/contracts/governance/extensions/GovernorSettings.sol +++ b/contracts/governance/extensions/GovernorSettings.sol @@ -11,9 +11,12 @@ import "../Governor.sol"; * _Available since v4.4._ */ abstract contract GovernorSettings is Governor { - uint256 private _votingDelay; - uint256 private _votingPeriod; + // amount of token uint256 private _proposalThreshold; + // timepoint: limited to uint48 in core (same as clock() type) + uint48 private _votingDelay; + // duration: limited to uint32 in core + uint32 private _votingPeriod; event VotingDelaySet(uint256 oldVotingDelay, uint256 newVotingDelay); event VotingPeriodSet(uint256 oldVotingPeriod, uint256 newVotingPeriod); @@ -22,7 +25,7 @@ abstract contract GovernorSettings is Governor { /** * @dev Initialize the governance parameters. */ - constructor(uint256 initialVotingDelay, uint256 initialVotingPeriod, uint256 initialProposalThreshold) { + constructor(uint48 initialVotingDelay, uint32 initialVotingPeriod, uint256 initialProposalThreshold) { _setVotingDelay(initialVotingDelay); _setVotingPeriod(initialVotingPeriod); _setProposalThreshold(initialProposalThreshold); @@ -54,7 +57,7 @@ abstract contract GovernorSettings is Governor { * * Emits a {VotingDelaySet} event. */ - function setVotingDelay(uint256 newVotingDelay) public virtual onlyGovernance { + function setVotingDelay(uint48 newVotingDelay) public virtual onlyGovernance { _setVotingDelay(newVotingDelay); } @@ -63,7 +66,7 @@ abstract contract GovernorSettings is Governor { * * Emits a {VotingPeriodSet} event. */ - function setVotingPeriod(uint256 newVotingPeriod) public virtual onlyGovernance { + function setVotingPeriod(uint32 newVotingPeriod) public virtual onlyGovernance { _setVotingPeriod(newVotingPeriod); } @@ -81,7 +84,7 @@ abstract contract GovernorSettings is Governor { * * Emits a {VotingDelaySet} event. */ - function _setVotingDelay(uint256 newVotingDelay) internal virtual { + function _setVotingDelay(uint48 newVotingDelay) internal virtual { emit VotingDelaySet(_votingDelay, newVotingDelay); _votingDelay = newVotingDelay; } @@ -91,7 +94,7 @@ abstract contract GovernorSettings is Governor { * * Emits a {VotingPeriodSet} event. */ - function _setVotingPeriod(uint256 newVotingPeriod) internal virtual { + function _setVotingPeriod(uint32 newVotingPeriod) internal virtual { // voting period must be at least one block long if (newVotingPeriod == 0) { revert GovernorInvalidVotingPeriod(0); diff --git a/contracts/governance/extensions/GovernorTimelockCompound.sol b/contracts/governance/extensions/GovernorTimelockCompound.sol index 21439b4db..ed4d916a6 100644 --- a/contracts/governance/extensions/GovernorTimelockCompound.sol +++ b/contracts/governance/extensions/GovernorTimelockCompound.sol @@ -24,7 +24,7 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { ICompoundTimelock private _timelock; /// @custom:oz-retyped-from mapping(uint256 => GovernorTimelockCompound.ProposalTimelock) - mapping(uint256 => uint64) private _proposalTimelocks; + mapping(uint256 => uint256) private _proposalTimelocks; /** * @dev Emitted when the timelock controller used for proposal execution is modified. @@ -100,7 +100,7 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { } uint256 eta = block.timestamp + _timelock.delay(); - _proposalTimelocks[proposalId] = SafeCast.toUint64(eta); + _proposalTimelocks[proposalId] = eta; for (uint256 i = 0; i < targets.length; ++i) { if (_timelock.queuedTransactions(keccak256(abi.encode(targets[i], values[i], "", calldatas[i], eta)))) { From c44c220254d5decbc06ecc5008d7e8884c11bbb9 Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Fri, 23 Jun 2023 19:37:27 -0300 Subject: [PATCH 194/197] Update Ownable2Step docs (#4384) Co-authored-by: Francisco --- contracts/access/Ownable2Step.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/access/Ownable2Step.sol b/contracts/access/Ownable2Step.sol index 61005b7e3..f76eb2bba 100644 --- a/contracts/access/Ownable2Step.sol +++ b/contracts/access/Ownable2Step.sol @@ -10,7 +10,7 @@ import "./Ownable.sol"; * there is an account (an owner) that can be granted exclusive access to * specific functions. * - * By default, the owner account will be the one that deploys the contract. This + * The initial owner is specified at deployment time in the constructor for `Ownable`. This * can later be changed with {transferOwnership} and {acceptOwnership}. * * This module is used through inheritance. It will make available all functions From cb4bf950df5ae43356c4935b3900446f6dc20261 Mon Sep 17 00:00:00 2001 From: Francisco Date: Sat, 24 Jun 2023 00:23:25 -0300 Subject: [PATCH 195/197] Add unreleased disclaimer in readme --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbe7502e6..2936877e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +> **Warning** Version 5.0 is under active development and should not be used. Install the releases from npm or use the version tags in the repository. + ### Removals The following contracts, libraries and functions were removed: From 8cab922347e79732f6a532a75da5081ba7447a71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Mon, 26 Jun 2023 06:20:01 -0600 Subject: [PATCH 196/197] Rename `ERC1155InsufficientApprovalForAll` to `ERC1155MissingApprovalForAll` (#4381) --- contracts/interfaces/draft-IERC6093.sol | 2 +- contracts/token/ERC1155/ERC1155.sol | 4 ++-- contracts/token/ERC1155/extensions/ERC1155Burnable.sol | 4 ++-- test/token/ERC1155/ERC1155.behavior.js | 4 ++-- test/token/ERC1155/extensions/ERC1155Burnable.test.js | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/interfaces/draft-IERC6093.sol b/contracts/interfaces/draft-IERC6093.sol index cebda56a3..fbe31051a 100644 --- a/contracts/interfaces/draft-IERC6093.sol +++ b/contracts/interfaces/draft-IERC6093.sol @@ -138,7 +138,7 @@ interface IERC1155Errors { * @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); + error ERC1155MissingApprovalForAll(address operator, address owner); /** * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol index 9129992f4..e4bd74826 100644 --- a/contracts/token/ERC1155/ERC1155.sol +++ b/contracts/token/ERC1155/ERC1155.sol @@ -116,7 +116,7 @@ abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IER */ function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes memory data) public virtual { if (from != _msgSender() && !isApprovedForAll(from, _msgSender())) { - revert ERC1155InsufficientApprovalForAll(_msgSender(), from); + revert ERC1155MissingApprovalForAll(_msgSender(), from); } _safeTransferFrom(from, to, id, amount, data); } @@ -132,7 +132,7 @@ abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IER bytes memory data ) public virtual { if (from != _msgSender() && !isApprovedForAll(from, _msgSender())) { - revert ERC1155InsufficientApprovalForAll(_msgSender(), from); + revert ERC1155MissingApprovalForAll(_msgSender(), from); } _safeBatchTransferFrom(from, to, ids, amounts, data); } diff --git a/contracts/token/ERC1155/extensions/ERC1155Burnable.sol b/contracts/token/ERC1155/extensions/ERC1155Burnable.sol index ff099cb58..d7bee05d9 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Burnable.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Burnable.sol @@ -14,7 +14,7 @@ import "../ERC1155.sol"; abstract contract ERC1155Burnable is ERC1155 { function burn(address account, uint256 id, uint256 value) public virtual { if (account != _msgSender() && !isApprovedForAll(account, _msgSender())) { - revert ERC1155InsufficientApprovalForAll(_msgSender(), account); + revert ERC1155MissingApprovalForAll(_msgSender(), account); } _burn(account, id, value); @@ -22,7 +22,7 @@ abstract contract ERC1155Burnable is ERC1155 { function burnBatch(address account, uint256[] memory ids, uint256[] memory values) public virtual { if (account != _msgSender() && !isApprovedForAll(account, _msgSender())) { - revert ERC1155InsufficientApprovalForAll(_msgSender(), account); + revert ERC1155MissingApprovalForAll(_msgSender(), account); } _burnBatch(account, ids, values); diff --git a/test/token/ERC1155/ERC1155.behavior.js b/test/token/ERC1155/ERC1155.behavior.js index 3391be4c8..4bf4a7319 100644 --- a/test/token/ERC1155/ERC1155.behavior.js +++ b/test/token/ERC1155/ERC1155.behavior.js @@ -254,7 +254,7 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m this.token.safeTransferFrom(multiTokenHolder, recipient, firstTokenId, firstAmount, '0x', { from: proxy, }), - 'ERC1155InsufficientApprovalForAll', + 'ERC1155MissingApprovalForAll', [proxy, multiTokenHolder], ); }); @@ -609,7 +609,7 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m '0x', { from: proxy }, ), - 'ERC1155InsufficientApprovalForAll', + 'ERC1155MissingApprovalForAll', [proxy, multiTokenHolder], ); }); diff --git a/test/token/ERC1155/extensions/ERC1155Burnable.test.js b/test/token/ERC1155/extensions/ERC1155Burnable.test.js index 6af2308f8..65a2f95f4 100644 --- a/test/token/ERC1155/extensions/ERC1155Burnable.test.js +++ b/test/token/ERC1155/extensions/ERC1155Burnable.test.js @@ -38,7 +38,7 @@ contract('ERC1155Burnable', function (accounts) { it("unapproved accounts cannot burn the holder's tokens", async function () { await expectRevertCustomError( this.token.burn(holder, tokenIds[0], amounts[0].subn(1), { from: other }), - 'ERC1155InsufficientApprovalForAll', + 'ERC1155MissingApprovalForAll', [other, holder], ); }); @@ -63,7 +63,7 @@ contract('ERC1155Burnable', function (accounts) { it("unapproved accounts cannot burn the holder's tokens", async function () { await expectRevertCustomError( this.token.burnBatch(holder, tokenIds, [amounts[0].subn(1), amounts[1].subn(2)], { from: other }), - 'ERC1155InsufficientApprovalForAll', + 'ERC1155MissingApprovalForAll', [other, holder], ); }); From f29307cfe08c7d76d96a38bf94bab5fec223c943 Mon Sep 17 00:00:00 2001 From: Francisco Date: Mon, 26 Jun 2023 11:36:46 -0300 Subject: [PATCH 197/197] Add Foundry installation instructions with required warnings (#4389) --- README.md | 12 ++++++++++- scripts/upgradeable/upgradeable.patch | 31 ++++++++++++++++----------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 8b44917fe..9d1c405b6 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,23 @@ ### Installation +#### Hardhat, Truffle (npm) + ``` $ npm install @openzeppelin/contracts ``` OpenZeppelin Contracts features a [stable API](https://docs.openzeppelin.com/contracts/releases-stability#api-stability), which means that your contracts won't break unexpectedly when upgrading to a newer minor version. -An alternative to npm is to use the GitHub repository (`openzeppelin/openzeppelin-contracts`) to retrieve the contracts. When doing this, make sure to specify the tag for a release such as `v4.5.0`, instead of using the `master` branch. +#### Foundry (git) + +> **Warning** When installing via git, it is a common error to use the `master` branch. This is a development branch that should be avoided in favor of tagged releases. The release process involves security measures that the `master` branch does not guarantee. + +> **Warning** Foundry installs the latest version initially, but subsequent `forge update` commands will use the `master` branch. + +``` +$ forge install OpenZeppelin/openzeppelin-contracts +``` ### Usage diff --git a/scripts/upgradeable/upgradeable.patch b/scripts/upgradeable/upgradeable.patch index 554775ffe..ecd0a3711 100644 --- a/scripts/upgradeable/upgradeable.patch +++ b/scripts/upgradeable/upgradeable.patch @@ -59,10 +59,10 @@ index ff596b0c..00000000 - - diff --git a/README.md b/README.md -index aba99171..6656267b 100644 +index 9d1c405b..c264e29c 100644 --- a/README.md +++ b/README.md -@@ -19,17 +19,20 @@ +@@ -19,6 +19,9 @@ :building_construction: **Want to scale your decentralized application?** Check out [OpenZeppelin Defender](https://openzeppelin.com/defender) — a secure platform for automating and monitoring your operations. @@ -72,6 +72,8 @@ index aba99171..6656267b 100644 ## Overview ### Installation +@@ -26,7 +29,7 @@ + #### Hardhat, Truffle (npm) ``` -$ npm install @openzeppelin/contracts @@ -79,13 +81,16 @@ index aba99171..6656267b 100644 ``` OpenZeppelin Contracts features a [stable API](https://docs.openzeppelin.com/contracts/releases-stability#api-stability), which means that your contracts won't break unexpectedly when upgrading to a newer minor version. +@@ -38,7 +41,7 @@ OpenZeppelin Contracts features a [stable API](https://docs.openzeppelin.com/con + > **Warning** Foundry installs the latest version initially, but subsequent `forge update` commands will use the `master` branch. --An alternative to npm is to use the GitHub repository (`openzeppelin/openzeppelin-contracts`) to retrieve the contracts. When doing this, make sure to specify the tag for a release such as `v4.5.0`, instead of using the `master` branch. -+An alternative to npm is to use the GitHub repository (`openzeppelin/openzeppelin-contracts-upgradeable`) to retrieve the contracts. When doing this, make sure to specify the tag for a release such as `v4.5.0`, instead of using the `master` branch. + ``` +-$ forge install OpenZeppelin/openzeppelin-contracts ++$ forge install OpenZeppelin/openzeppelin-contracts-upgradeable + ``` ### Usage - -@@ -38,10 +41,11 @@ Once installed, you can use the contracts in the library by importing them: +@@ -48,10 +51,11 @@ Once installed, you can use the contracts in the library by importing them: ```solidity pragma solidity ^0.8.19; @@ -101,7 +106,7 @@ index aba99171..6656267b 100644 } ``` diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol -index 5b7e1b15..1ca745d6 100644 +index ebdf0a33..8888803e 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -18,6 +18,8 @@ import "../utils/Context.sol"; @@ -127,7 +132,7 @@ index 5d8318f4..ef3cde55 100644 abstract contract GovernorVotes is Governor { IERC5805 public immutable token; diff --git a/contracts/package.json b/contracts/package.json -index 4d0f576b..822fd471 100644 +index df141192..1cf90ad1 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,5 +1,5 @@ @@ -135,7 +140,7 @@ index 4d0f576b..822fd471 100644 - "name": "@openzeppelin/contracts", + "name": "@openzeppelin/contracts-upgradeable", "description": "Secure Smart Contract library for Solidity", - "version": "4.9.0", + "version": "4.9.2", "files": [ @@ -13,7 +13,7 @@ }, @@ -147,7 +152,7 @@ index 4d0f576b..822fd471 100644 "keywords": [ "solidity", diff --git a/contracts/token/ERC20/extensions/ERC20Capped.sol b/contracts/token/ERC20/extensions/ERC20Capped.sol -index cda07265..d314148c 100644 +index 41e9ce5c..1d910dfa 100644 --- a/contracts/token/ERC20/extensions/ERC20Capped.sol +++ b/contracts/token/ERC20/extensions/ERC20Capped.sol @@ -7,6 +7,8 @@ import "../ERC20.sol"; @@ -160,7 +165,7 @@ index cda07265..d314148c 100644 abstract contract ERC20Capped is ERC20 { uint256 private immutable _cap; diff --git a/contracts/token/ERC20/extensions/ERC20Permit.sol b/contracts/token/ERC20/extensions/ERC20Permit.sol -index 9379e445..e02f0644 100644 +index 4378eb7c..1da9e731 100644 --- a/contracts/token/ERC20/extensions/ERC20Permit.sol +++ b/contracts/token/ERC20/extensions/ERC20Permit.sol @@ -18,6 +18,8 @@ import "../../../utils/Nonces.sol"; @@ -173,7 +178,7 @@ index 9379e445..e02f0644 100644 abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { // solhint-disable-next-line var-name-mixedcase diff --git a/contracts/token/ERC20/extensions/ERC20Wrapper.sol b/contracts/token/ERC20/extensions/ERC20Wrapper.sol -index bf2b225c..0e5b3628 100644 +index 389965e9..66436b14 100644 --- a/contracts/token/ERC20/extensions/ERC20Wrapper.sol +++ b/contracts/token/ERC20/extensions/ERC20Wrapper.sol @@ -14,6 +14,8 @@ import "../utils/SafeERC20.sol"; @@ -356,7 +361,7 @@ index 2628014f..7d5193c8 100644 } } diff --git a/package.json b/package.json -index c070915f..9a513cac 100644 +index 37e8f871..d098669f 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@

XRhnQ}H08>D;q7lu(1A z|JW8O^aEGoB(@;4bf~A-KatR-#d&gp&+Gtx64^Z889@z)0TpWGq?b%|*YNn)An)DE z(I3@6^DvhUrq*hI^e~KeQ0PiMyy55R06Q>jZ5Y-V$f(7N5nTJHeaTc574a@Ikx6(i zlj5joXf#9U@8rr#Bpf*zx8|EBz+@klQ=t}KAS)a4Y(`#c-}M}Nuemy2k4x3i=PYX_ z_4?YUV7B-fu8s79r-8hK3G|Vc5n~L~BdhIj;CmJp`tfJl7XA&SA&rnVev;3|by$m` zjZ{4}7==~dYbifUuIJns6VO0 zx{4z+LLYgU*xrypv*(gg8S~Ld()lF0WRLZ^k_6f1?}WWM2>%ULpY30-0HQW)qc`Aa zy9N!_aB-lQ4=ynMFJ*j~klx(t4FJWXpI32u#f^1u) zfgO2Ixshs%WamxGwO77nmB|T(s7YVM--3HLt9 zSKP%4me953eW5H##E%l832P5OG-{`i-8*M?ca|0+EwKH6zve%JQ1y>>$^+-0edi_? zcR@c4g4a^E{<5lgG|d1NQ0b}o$pXWXZ`A|6Jl4dvwkk)kKxW?4)q^CGLC+$bP@auu z?%Mot-@`{OFLoJ|JlKj}pabrF3k?}1xb0CjWcB;KL#8iIcN_>2Xn}JIG#?l^93ZG@tzJb070jklPG)BP z`^^RyagD%%*nrg10`+2{p-$7B?upS+`a(et0(G8E<=e>u;WA6vMcWD4n9JL$V%MI8 zRZ8KWdz~X|_1y}AHF@`IwUY+m$-j+=E6C`9@oIu&&LX_E@7e*WcLOF2bL`6dk*sHrf5h_H4%HMI)g98C#r zm_k>FKCP{8Pz|6ng{forJlLq{*9-06aT`I^3T*_TBCM+D;|S$KulH!g5p}6WZ$>o^p@gR@dm!bFWJW=ceU+sUzsjnnsf@ z!NkYT7o#?~2jb^J;Suc<=~AJ_FyfX1a2vYL8R#G*-fDE_OioLy`UZz=25A95QJ{gK za^Bp)DI1I(+1SB1mWcG6)nwj%p^CUr{cT(Oy%r(Zycx7Sub690%ZDk3x3d3*#2qT3 ze@&^dVJx*AFLWbECa&bw*NSz=FEBfX^vIRDT! z=r^L7R?8GNt*I6G{j&crvh>isyqMT)lWCqXN(INAsw9sjNwdB{(<|#bhG#*Era$?M z^I2i`_6&Rr1udJrx&Bj5QQRC-f=77rUr*Gq`3t3@lJSAnaRV(Vn>Xyd;SeDxtiQAZg_SAIYvs)|5ClHO9dSPv@&-q#nOGcLuBh zoL|Mf@B)G}X=CUNCL29BChRVWk9j%aN>w+FI$np9O@gE~)pPw|5SXpJv|J+BVz`O? zUMa#!5BJoag-|NfT|`3-mcUYPd9PlTQM2=ph2{j#dQFcKtx}Gl&kj+AMlS?zt&ZW4 zu2VxTP*9pFsF+4*6h*kl-(;%C0dG_gB7|WcMop=q%e(~h&G=gzHlfI-hp{_5;)2i9 zL$cOg{QzXJ@t#y?F-fWm_sE;i8L(sDtkx#v&En53%cN!(81uK%J4sg~N;QfnLta{; z5AWtnMfVoH+%iE*4(X=qx~rfJC3+v}&UPOezrf&GwlTN90vM%}z1(pXUcsYLG_7%` zk*$I{IMHEm?2y<0IFxU#Thd!QH@k^>B|NAyC^6UCN$sWv|Jbk}Rng~kLl^TC52cxX zAEXFiUO(lw*OJy7KzorXs>GRz`Z)E!_-?0RELG43&P)wPq$G!+L;#i6}>O~)iq zn4MMGR#kYe#+RU?VwfaNi;^Z6#l?*s*@3(XoJqXF9qJ$9UnL#e6vZ3 z;1u2O@jOH zjfBUxxae%&Tr;E|B1zkq3g~-r%)`BA;aFD7;#M7eK^+oZPQMmu=4o-8-U)dQs78YgV17KNO&e>yJtduax}ZMwCQWyr z=}1M`Vae5#9%x+g6b3}X@%{oDx~pU#NiW+C?8gYR(?-;dU4Qop$RiLO^$dpdZ!RU zbz#8{`AZCeo*u^Vtv#+uWr%~!oRe&Lz)NbhgwKhGBTL9e|tH=S+_{mfthF{-G%%R|ZFf&iT$z}C93cTuX>o1yATQy#oI zoOZK(FkvRQ9u#v!N32Kel(Mr!LizAKI8?ZVkVc?#PnEk32}D8(200n$1tfM zM1X;sQKr_9a>s*q0DYUA%vV+BQL(`VNQa|(dI|a4PddJij3oz8h@n^7WQVe?9{<=- zbU?eJ-qzm1gZVRs+gDK+2YqKrsny+91(PL(>bfr@ejEkYB3Q+sq%F1PV+MXMw)pxZiKQ)Z<_uCGW^9x!wn$0M3fdlBUml+h(;{G~(LH}?oIx+#! z;h+oj)to-)TBu-W$7k=*EUgg~{6qJNc|>1(Wc}bE;rbW@K0$_N156aNT*YO?D_TDu zM7r5ny5*cCE3s)lk#&*&!U%q9WOxqN%W$56z(p`%3Ap0ZDp2QyU>oR_dZMcEq$)(j zQNcfC|Eew9OFDaBdn-QlALq-1;4X8rM#7pBrp(4uTAqvSKMcLK1ln7tU?-=~q zHK0YV*8Z!SsTtw8pW6%P2UNIGdxt>%SBgkD?i>q^)d)!RKY9Lq^Gx2B5%@u_J?OhY zkP;b6c8xFQiBV2pW)TNmh2C|q+%6S>&}@Z8Q5pxnu)8Q>NB$4ekOhKXn2e_~j1gg*|$BL5GRxofMwUUdRyh1Y5la1Qhisw z2KFILMa(W0m&`O7`*X#hz^hxew(Q05{2EC@Wo9aqpnbTKqb;pI7_`+8OH>dc)jyiC|hSvILP^*Qd?c>1qQ+4%i;5HK#W%myhVHai+uBq)uOCdr^ z@bL)b5^-+W(81*5{q;qpXKQ19x7uLwnBKsP$h}W|0Ua!1W}Q=J7GC@BTbrj$q?RR_ zYBoj@FPK6vHOy?j}RSWrXbRT2YL)`!e7wx z;<^9F9#v>;8M;D4!C75JS?mJ@Ae2Vxg-N3%n^sxrk zW3i`As_)ERc3)pJZMgL6*~}#S^5uy}HOUK7X-3%PX!Rr_cw!V4#IxV>M_eBE;GmFs`Q7`|Eq<~xM+lV9R2?jP84erDrBnCZpNZ4AT#h~cY#@_rqt3!h zBG^03DW(E{yoFuv)^(1g@67&5h@_nvKFvhQX}5V^r+{-YZQ9xSO#}0a=Kb)zvf=#i zu&vmOJ$9M!nHAmZ%N#iVm*+Xy=Xf!if*S?zvzOdTI8DNvj z2oJ?BSEM1O7DPAf>>fkzBFE{|gaEQJK)-dQ1AS*=^C*d?!jO99wcyCH&m=r?onAXJ z999XDQEF1PCy6n3M5tTjkG#PS@_Cl;!y0oCn@A^ipw2^AXk3qP{bW#)K8l01JA|l0 zs0ZITDt`mVHja30Qqp_kQ!^7BW)w9X0t?IC)cDq>oP;v_R1DYaEXqEIz}Ve%AgfUg zmeW+En%fEwVNNhI36%&l38*K|fj!Xy`pQll-^R+1%WWgM%?5!x-hQ43vj( ztAfS?Yah4?sM-Emg^gv+yJF^F3U5~b2)A{V_4(f4?|O;{RxF(;5?F?W+&zjLbDSar zZ}QN9Xv%?!)zt~STbqcFgT7;qW0KbqI9rff!Zgm!CgS(;z(u+hS>Mnlb$FF6teSWS}N9ZM9haV9;CBSB&Zl{nbn076@g+Z4> zj48xh;T)WS!s`7An0F|LE}9+b*75a~0o#I!si_l_r-z3k8ZCu0@$MXtzNVmUNLcSDQtGyUf<&*Z)plWxT7C{n2gAqD3A`zvEa|K ziuF!OFnK^iX+r}p9bM^ovZIyNCr+LtphrQo!FMuu?P&ga09xjB)&4Bu&tOmLp5J_- zaJkx6xjVUR8EHTtsi@~s4xZa^&-H&0*@uas9Z- z=QYz4w`Or-)SCZ&RfwSZXJ#2dAr&8{2i&^{+D zB0so)bkfSfrDwkmY=KoJ#OBRQt$kn5OD%z)v#=j4v4Yq5FOgxA>4_2p<0z6*Dlacj z$-)vQShIzT^GEmX3Fx>8IB%bz`~cE}{y)^np|88N!9$p5GdYI4ZA4{v2Pd8z$Xk3? z9a+(R^BB&8`Ns_7nXI>8$v7|D3)n9cdkFh>iiJ!K5lelNW-vXwJuX3|ZOpZ!za8<& zK+f^1kKQos`yq%1E>pY6aEyOkS+`{!b%X342{(b z!38x85vAJgs*l^7hhlgpW|smC#+XRjQ5`{qj0Wv%JNXLkCFdHy?h@3O7mGiB%oq5< zR9`m`Y{#eb^Xo1gOn?0O2-#_5a!v9Z64x0Q2ETRVEuu8{g(k$_gOdw!{+5=!%bus> zLivhTR}>7NKes@J{w?*u;Ilsc$uD8ir*#b`sUj`P3JDypEr$PS0bBiS@;>z}~BIsNwh_<2bW)r8r5uv8~3* z{^r26wn-Zo1vT99Q-M$>D%qbM^I&iN=?7^sH++k=MjmOuB8 zTAJu`oOKcvVei@?3q)l+N}zCvFjXK$f#3v2f3C$C&cd5>{m-w@MY!K4Uz0#)i)0O@)p^l1WVwW zA%{4fmg2J6CSi=Z^)M6COd%zCz|TJ|;9nNx;+CnY&YROcH+Q>-2k-=|#qsCnPJxp? z{g2~Li)!~T+mN7L4o`>~J%=Gw^}wqMBT){l!0*}$YH0qXk2K|bgn4bu(gmhhctig> z!>GubKq&_zOWiA*NVd1W7OQyeicPiA2Q`eCj8wPTW)q(si6Pw8ytQQ!MX|`=%r7RG zm)Gzpj3)C_?-W%erPy;%I;616dlW=Cq?7~MrFY;cHiug==LYtYQ6_p%5$2o&ng=pI zl@oAREgda~CVVS;jnpg|=Nv*F>DrJ~!8Tgj1eupzJn)b^uVdr`Od!YcoWG6QFYr&ioAyBY_Q zlOLM<8B~=yhkYE**U8tP=onR9E+9k zGSi@OuAJKSzZJ-PM-ro4@o{XlY$m^TX*M{?8C0#d+=dXP_Uw$;B`0TNEhh7ixnq_B zx@^D`oNAZ1{9akiPS{TBn=H8|UBAREBn==o8_T?Zif{w>Pvg+1=qkeq(~qNliG6m& zr$fs9E$vP%zi&-%a6P*tNoAVLzj~m+K6uepJx<^;im#A^iyG=6%2M(Z7LF&8V~vgg z(i;x}M>yS=S{(O3;JHItxlKGYytCm;JJ(0SLS{A}2^`JN2zb_nN{|K5#o%xjXQ0ry6uzXX0z^4M=_J$P2~w~*Nj zLgvikhpa2;8Y$d;f(pxJ6qmNXK4)$}^}5<>gHy(&qfMY*kN+Q8XKD0NihZ+&ag;}( z+HOC7wHn^YTCk6?miPY%!Jf*IcM~fH_W{`H1Pp|Ku($_fvS4K0{Y#EslRvf3(RdB( z$vX;}4N`|M-$_KCheFz|q{#DG_}{2EEx$Za#1neJIGt4L-}zQVZTEI!Ou~ zJP}%}Pk`{^@nYb!$*I}7A{$lswF8%Ydci%2Z8+mSOg zju(+A9g(8bkFhdEi-qBQSh#?{F{dx@2KsCy>_%Xfg^p5KM-VjGqrII;Rx zaU*x%&U@?lS*)rcgQt#F6^Pj2_7jPRN<4i41^0%!l+m@tJxR-Z>gy?5W1Tkilj-A{ zX&JgUXBoediP9^>9R9)+kNJXDL(@A3H+1U>ewpV;{gsBcbsf}sWY$LbqeuC|@C6W2 zi_5#Ddfatt>lg+6$9oTzQFDvEYwo$%T`2wKrI_tyUhI%}EO{=gnXY%Ye`Y$9ftT{S z>Y8C5?KOl(BZ^OWG5NVBrBI|ujt`YrFO=SEq~5evbxIaHw*jOOmV&g(0>Z5~Rg*Ob zC4)cSjb7&%fNHMU9zRocqQmOKj|cPi0?VF!+c75%nd!-OxoD8b_71x31a!5fL{7^5 zb2Hr+bGNrBACYf1T__kKqM>o=P_~aHF0hZh#7@tQKF?2>*r5@~Px>L}Y<)$U&pA<% zW`NZY%Pc2#Fk~}fg5Po38diQk_b6ii0k}mu06j|9ecbFFJvj^&mO^STI1p8k%9=|M zsncALT9PGy@tqG_0j6@Q=S-z@mN-v>GGJ4Bmk3tl3(xtgs(FZNOzjI@cjQt!lpPg? zeAQ%+Ab;5-dc0%~Jwywae!B`6!PV$WUy8{zn!rdTTaM|7OZuM1ll8sJRPVV_2X5TP z!9kTA*~R`~BSabvJWJqlCA<10t zF&X32`5RV`f}|f>I@obwpE|$)Nz2L-FCOp;;eK1cGeI1594h9IHKN&niG3#%2zVZG#?gJl|f)iayy{D0@W&oa0HB! zkgPi7tl8NkREqf7LgNM$6QUt~T@fO5Ep1ia!~^!$etqqxiCC@%|LR+IHz;A^G0lTsYuqenC8li; zUErA@r$zJzDvXH83F24J;*NmXLj4)kR(DRSRm?2bC2^xm=Z>{#ey>l{B4~W4QaRnz zQuGXccGQ#VvNY>j{pgW%qC$?&B4hceZuUyn7LAB=xA6Eh8@6s ziu&Dql8|n!o3Lb>E2ucs7ag@7e0y+KDQxM%?qT2mKw1ho3whtUNd-p>A_BKFd&Rp~ z-OmeQHEfS7XeuOf^faNlLMCEQ*``j1y^d2S=N%r)eLt-e?yl^6sLrd^H3NAOuV<_$ zBMbw6I5(iUq$Z~3PvrIs#L-UWlx=anSA^e8zfsdeU2~J)UvZ}#R1skBu_rJ{f$N5$F#^x$9%0rx#5VHq2v_NxDdP? zvpxB6$QVNKY1TpWVL^-aNTGO%V%cRsZWXWfq8kHuudI4B4@d9RHyMtMEX)RO311*) z>(@l8#~(Dj__$z)aAvDiM4Q0ltBOp-qc2upkM6r1HbP-CSUu3ti2>d&= zxqQ%l`}n;4X*R3sAQ=XT}^z+Fx!%Vm;B?*1l@7+#izn9<|WqEzB2yUKj=inTAD z=$+|TP{B^Vl5~+h^W+o5O8mKbLfUA3USZs0`KcRLkK3u+SrQGN(~ou>m{ptkcZjXegkTOv1KaPBVM*X09-!(4$P@^f_<_{>P#2cVUY9o@`C< z|4o>}nOKpfUK=P;xZ-!3QrBkV@|7!Lg)f(+k^^s63Qi;)I2tm14Vp^jgpHozv+xya z^3*s7`VZe(@pQSYpNSTl>$+VO?leOAR*o1|uE7x-v047-ehlb#51d-3$JPDlV-`dH zN94bZfdCOdG)@-UvfF4l5h8xc18=;UUCybpV$Q$R6i_aV5A4KxI~|6-@P|d4t=^NG zk8Dz`b+r23F|YGEI_U0#sH@le;W7*;Th% zt&h5Fh8@T!U$2qgI3K$b+PeQ>w8$d2;A68_TY5S0^-R7Sj$3hEgnxbjGiB|4>){=mu5Tr<9K0?1U!dZ|-1K=w_Z#asT~%3`#1_9JP2;&(ju$>NM#DNAFl*!GMh>hm%q2g*t;=#===B3-CjlJAwmkx55-VC z#R7z!vs3%z3Jx$i4Q`npzq3yjaQ9v+)L6GF@F*g0>I(WGLuH=zWl&% zqvB}GuNo2e0f}7ULQh6xef<$=6?gGR%2i1&RchazuiP8USL%*NlGT+}?hqy(X6ROTfuS_*@E?qbTlaq#?pYcffFaFH~S zyp3(wa%G-{eddr}TXcq3t!2iVVP-Lq0Q*XLcL2fJz0gEv&x_k=|Jd@%urH;IRq{_HvHys}q;5 zBudaaIy!=8(f|%)r(qtj5CCXV`1Y-WiV7ho7nkv&Tvp@9+h6n1i;#e#^?`OAjrKcF zi;Cc$Zj-!c6)|y!UHdKq&WM2_r|IYzcCa$Z4;7vLfkQclkI#AbY%WBaia0a?WwO>O zxpG26g6YA72k{qesa@UNjJH*?xPXKb$AkZFRRwXJmy2B8w0{mq_}%B8*s8(SwARdSl5>!r_0mIZwr zNqk=!!1~!nsJEQw^-l&;)wBm9#Spo#i_&nQ*G8#<2?ARs`R!2tr+8EZI*o^iaB6BQ zuw|(`LZD`ZvCFcxMzHqSc2dY};Xw7M+F=idv9P41f7?(p8-6I$4rKt$JiB*H>H`^Q zPmO%TXKtm23~#~&KN};wgDW{r($Z&z-n}uLABnrD7<|npEv|^QO7B*%w42i4h}tJJ zyF6hbn+nUZ8S75>#;UdpUaJjpx+u8!9h=R_VB2-9qH?%1rX(Da%&`eBs~7$ z{Uim5vK91LA^X8X{4XYNZ&S^6C7WYMmyL~WexQII@X7$T+P~ut7RSeK|E9}e7vz`H z-gH3tQDXbrz1Hfy$V7ys?6}xm1LBu;Ui71OMwRf!U3E0a!IjF#;v8m#Z%$LcD2~1x z=X_rH2!}xRxt4r-=$Qu$+}k^Ow#BB&M!I%bz~Q$Vu{*Y!NLb>zVwSgwDJ;An7})GE z4M;7&D>c&9E+iymF*@TnSPx|{lRl#{@Y1%IDfe)Nh$Td&{$-q!#nyb%)vRk0vr8rA z!c6vJ(c#HmCiUxF_okoLvw^HZ6^o@>UfiQ@H1TmZkE}l}z-N`$*82kUdg*u0&ob|q z)juu3h-(yXdQ@@o1@OuM{^r)>rKw3?ac7C|M;EMp@W15smgLmwf7ragbfY-r+R!w2 zSd=MVS#*Xr3ZdP#s=wcv)`I1BdGO>OdX6}Wxtk=Ce95K9CvRtN(aZ6?uex~1B)Bw; zBUO!aG5uM>;{Oq>@!N47n~Px&xe!zU<~_3QdEFNo7!ZJPgy!fqJRG^S?z)~Hn)SJk z9$Ts%4=^2pj^}TbrvyotI=Z@+C{)gh!5<>>fIF{?3I$8VhQQn&T;Dhn*nu!QvWbBj zMW4M~gC%cm&<~$g&iPby=8F{mFt}y}4)!`=^WZ_4>J+nb+Y4_7KRrDi81wwYmr+p< zfi4u2kZ3@@`b$?Uz+UHWyrVouKwu4lxTnE2?8g_IKi8QU3fNjIyyd`e6c!adsIIO~ z8lSrr)myV-4ihJMljIUA{eFJcqZK)HX47shUj1;!b1LGgSx0A@L&P;cT3>PV`*4B( zP-?BNQH5}YN@W}M)zbLF+lMJ4hNz`YGuvMn2w0N-E`Gw6H;C*VnP#FyAOueI`_KbHh5}3A4Cff2u6IbhtL2B7*HA9Q!b_{lIAD&ytCw=mE?b8Kky1n=d8?5^*T6X z5Bt_K``L@nLa|C2g%c%o&d(VR3Qc1+TXqqjK{*~WN?RY7qO z?5i_7{DydVcxX~5$r^O7KB~~MkLsn=i-^`F*5K}GW;4kY^|Aj6FOz(kcpnSVJqe0<6JC%#jW;Fs1BB+@#{(*GjfsORQi zYX5(2hOO=AkDmB?+72bWwZaqR^3_lx$^0Dg<)FL73B~}cN#11ySPk}xf^3A}!6l2s zQq_+R>qfFa;4DU3aR2m^{}mQvdYqNlXR-9YQH=HZgD>=9O%sw*FW}czfk{x0fVya3 zUcijChhUT74*iSbqXtuj7o_p6E4QZwBJ2L5#ParKpz|_#%CWeU(chQ?WHK>P;!SY= z*wDaoqDs>v3oKFxdlsa?iJG#&z&PB%KL~Db9FdfAiA)B=%aIAM$&&$JPFj>%3x?g< zn2bTTQJ@ahAe9rO6B;lTCk6k|%{jTZ8GSieJSGHqO?YM^@i$Som*>9ULZL#nD7Y=# zpSOGYycBG?XGCI}%lOpeh~KT3^?=mG_xjw|98bo)mi9rxeEDx=_&0^?S7$U_UI&i& zmtq2c?D~B*B0GS}td-`?slWl{(O&es=anf-uyc)R zvr_9)2k*uF5fVmQX`SPX_X0CAIBDv6!Xg6M{R#+J^k;pv=vO+Zk(@&wz9kgTKhK}7 z+G@FdjBH83-Z7k;W6=x$&Z!y8ZZE$P(K^%l{mk9-UDgIIj8~jjB171YG|ZiZF~Wrb ziqiQ-9#tF~=$eN9wA2okjSIT#6ed6M6P}P_=z!gnhJ`Kyvgw7L-6%q2koC3QD8y&_d|))f`lQ1OmTim*&&>1JTFA4 zpjQ*Lqzhj7gpY9ON5n*Tej{yXsP5)8P0BQ*IRs|F3 ziq;d|J!*rv5=3?1ebxqD1W25;zJ|6Pj53)xk9{;5npwQJ?h>bbLVk2t*c%HNDaDm5 z_Pvm~i$eY9(|;97Bun@h^5YuNeDv*H&Xe{cX7}9A;*+}5h?FWt0mUS?C>9sk9KB}_s7J>Jx{NGgYJFDK#+pz( zL$rY`geaZ5VfIJck5?s84adJBMxNV97dWj=5!xq;(L8=Pk+SK4h{F6rKH12(3m z!~FIbyd>R!MgjPsmUgffVI=73_Yq$BQ)aUB&d%xEnp=-9dv+~YTo5l!3ox-)Iw)@R zJ#N}vLE0(IQ)!`=VQqw6;oy{|Y9$tg$(@2DLK<7p?$ureo(g$3(8RHbKJ2&zKR^$D z00k$|ra_2h;qZtlrc=>v7e-`k*yo5hjq7SBK%c#RI)gM)urfx(yyBQrV_9BiBOD&# zYnJ+{plPCfTC$#nSLc7KypMtY(L4- zPphx2?6fwAvR&dQ_Guqe?7rmA>sAP4ef%1ffK!U7Y!@E;coDJnX0bbwSE~1B|zmJTbB2lbpZkgWyLuKSg&EQdp#5e7!e2m zZkz8bZu*J7($;zd3oPyTec-Nw$%n=_yhqRVXTVeqtNr08Ed+*vQ$tuy>=fd_=!HDeC{{UK9YdNxLW_>ybJj|o=9XX_ckc|8e11yu$ zH5E%Uz?N)jc3s;)rX6s$Ji>w+X>yqZ0c;aIyFR9>{Q+?nb%RM+-5qQ9g5fXy!m!v* zZuVv-yol_7e_gkgpD$m!-$=Jrfb$6YSk|Lz{; zkxJ0gsbks80%#Z&w>BA^MkZ|q?HysmD!GZ9dTk+oafssR(yvEV!Q(%)sZifXcNGC$K+El61{ zc6PkbcOJe_wllyH~@-LAJ^V25Y;F-a6v}fbf=lKmNb@}d` zNVn*fsoU@KgoFV_+gS$`ZO(eT7bk2!Yjk92MuSUrFvg8dNRiR2PV(4g(<9$$3-a=_uoAM zTByjXZLDI>dcg~nQoTGT63>_Qpt^zFcJ=l;2PBxidRf<273x(uaE{bv4QpPZ(pI0M zrTkim{o@6*88hB`=9P#b1HmE#nXyq9x^6~;wQipWgbN&wj^VHBI~R+^j9&&4c#}0+ z#3h+vYZ?yt*txm=K0t(+xYIE)`jgY?kRD+Acpxr9nZil=dh6J;_b-g9n0={i+|-Ju zYUGZ2e--h(JS-fApCFi6_j&4y)VvP?zPLBw+OvDl5-vD^YL)g^q4=K!fPahXf1y~G z-}nS`5UccqhgGgphdZqE%eFlB%h^U`V`R0#>*6Q9c;1TQSA^^yYQsHv${M=fe7&sH z_{7fVieEF%+tz=xSl2t%pQ)*57h1K+9G{6nXo<gLN8qBJ4>__ z*zLNQ;51&q@g(5mm0 zo8R9#NW7*)|5khHHMSlZ;t{7adc+Sc_eLS;`( z!?ZFen(CIxk%j#vnQtw1Xuv0Xsi_ONkh>(0-QAt%7CF(IA9LJbss$c6XE|s$t4Tu>sSmGGmrMWEddzG^HoKQUl zKPsVYF70hLozs$P!)z)a+V@cI2ki)YFV=%8YZlLI#Pj&x;xE)h;ng){(L7(GdWeAnbkkhvB}ebNeC|-iP7(ynsvxro|}wSC@icR5`TPi;7qD zCm|uZ3^&L zGckSEaoH%G$+%dPE3XO%q_6tQ;$H|u=B&6_u=lpJaudG7rC%1=pob4}Cj+6h0>N1p z;FyIgoBSuPC7PSaHtX}&%43z@-psY3D3^Dg8khb8w9+q~j1l&-H6|6vdA|mZta-bX zr!l3kU4g%u4r;nwX81Dnx{p!=Z6Z^madMUL=5+{36%o-l?0ma_z&T}9+5hY%_FyB4 z3dfuh{l@}?>gatYO10n*M+3N;>pW%szom<6gwxK&S^oTP{qgpFcpMtqnu!fuxjF8h zsp&`GpwqMX`Qg(1wAIlVwe+5m(w5%ac56OUSSW|6wtL>;TXx?FOfE$LcV*@Z-@SXy z?w696E8PELxDXdmFOeL?EE*4HJ7hHV1-;-?93#Hz4H!V zu#^%cKs+KN#1I*f@D<)tJO&~Qyl@t`%+A;^r#`l;DimfaPuDXXh45^I`q6(8$>Y=d z2mLRNW5SDsj@QiBkyqc(ixiIZ-2gH!Sn=C0CnQAHHzY(l(U700ev1Pt{!Q$ncMLYE zf$i;BstG64*m-%R*=TUR^xrrZQ-Ylazptv;{dNSeaU!j9sQfh@W%7f`<#9}$?&Ddf zrAJ7kPXLHtnlx31PTSoTo^uFSvb-zY0MtwAySdAzq2_|h9F^G&@a31Zuv4PK&%ygo zI8AH6TUuF#AtV+}50j;vxsyyjFjXCJ7ZZmoXBJp!&GHdIYYp81t-XkR_=fMpFm?FUd#2pG7U!y1nuSFmK6KHQ z7KkJgN#VmD*j01d5f5c$KP^j_)Zf{V zKL`~~DvYwd+`EZ4#UQ1)f4%WYbc~gP!GI(|vi2>pTd7h?m0Gwk1?i&0Vw`d!&nOBHLU*!g2m@3`<+j1^{wA& z?BmGE2n<$;dHH*dlZVHSM5{f||@&H^2y$nVe>qVg|SN00hf+O_7`#6+F;VXyFX6#AGBN_BYrxWQ>39jW zGG!Gkmc=y$!0-3;?>3ezxLu1Od=;lp`d-)!r}yw z8B8NJwmqjHZIFGXYMmScbCBpqhyvme6)la4e7jWcTE47Zd51g^f%Gjp?gZy8C@bskFePoZ4;fL;d3C z;v+afG&DmH^V(ABxYcAsptGA><-%j-TenU<^!H~@EXd9#1|jWda%gDi5gf?rvcnjS z7r4>Zm<_t3K?wHU+LYvc$3}Zi-O)mvC5cd+`2r zjJxGM2lD3w{^&cEDO3V8r0xVb43{9}7|#8#U+oO9U$<FMxq445h5*$MUc9^Fuy>d%I^UVB*NFG20h z38VL2T|q{9k8*OpX-wzm=Lbhd65zzYcrgWuhxLO+*1$BaLN`Ov?PIx$p>#+9DRzh_ zCnq2H?(fvsUs}wQiQ-Oym|+k$-uQW^X#iVw^Oae1X--3Tb03|X85^mnXWXzai3=)B z#2Sh~LKo;!8zvC1DlU^icYmapo0Q?4+&d?3H5A{Phx{?SBT`=^*<$;i#EAO1zV5i= z;MUt~gE0yRU6Hu`xxl#j00b@e@Ys-r^GZ7*tE<58K0bVfLL^IdbIiLyf7i&+u&!Zh zV>~w@KK|`H)w;n!1`ycZXeA9bjaHZgt*L*OC@5OprmBSdIOgl);knzl9UWTE>&+T& zd7g)!9;@Sk&$hiMEvJkw{RaHK18vfhB%7(kXZduUw)YA7;yrgNMa1ov*FBa}`rm$j zjmrCvOBzFkySIX93FPHDA)!l}nw=Wc5GXo7gvt3Rs~*K2TytT02krW$Ph`1e|C0{! zTK7~nBm}fP^px-ZJj9}?NL$*`{c)rt{3h%_mgi2#Gcj{49z1X^Dk)oF)A0GOucQ0G z!)5YxL0(JoLCXB6HvNpMg{TYS!Z z4y8>pro_$ZoCAiJFTa7glG?)3E^Ib>r|$a1NIJ$tt1|oQzyz>BV#aSHg~i}S=T4e8 zo|R-WJ5qk>|Ht^pz5Sdoov*Bycz7m;I_O9s7-=1YpHd{w#N=duR)C8j^bSws`vy|; z4-ABE9be*dpA1k=L*CnU>LA&VY?JYC$~N}F?5_Wag4`GfE-X3M?ANlX)P-^jEkm1JGmQYW1KE%xvvZd-r$M6Wb*1*YV?)-TMTjy)`!7KbOV?^0Aa(h8E6n1>*xtfMG27$92lyQs5ZeTYrCU>ck& zTn^>5;zupdl0^HNP#W}qQbI_|71id~+h4B=oSL(2j;eOveA^1kKuUB>oiKa08f?@p@ zK{kSR&087>UG%6$mso8La6~cpk~;cLiic@2+YY-(D6Et7iyhzW?DJ?wdh&4Y$?Jz# z#f;n?Mc6AKTsnSkBRX2t!4)Do99&*Ye&)rhqH+K)5! zg^S$zK^nSxKny7idY*42h(=pD@k0AOZD_l5!Fzf3|+adKfPc-tu)xZ`5{%}S4hmXcAiBpt&?ca2jgdq3l@gl{TQ3>=z&@I~$A zB_|lmd1A6j&Hqn%=N(V=`}grvR^gBlnMVpmR)|~%;S>|huOA*Ne#_5!$1Ua87l;S%Z5(ed{K(Qk=nky^# zxJ1JhX$2_*iZRth!^c~BjM|(z=az#madKI>Fp{N<=tPfPtZE> zl#?5uJJy`Tnnd7=;N9mh1U%)M3?Bb@wP(r*7Fi~vf3D9=jt4;^j>zHm8>O?JOh(=yY)0FU#C4*l@!CEMsKL?KmK-*oosR1bkpCd>GiZnd`N2natDu##3+ z)jqkHXTq*;)eqTRGDbjN7o{La!nbjey?lA?pzEsSU?8J^Ws}+m&RpNMqid_v2c5eO z%-$1Y9K{&Lole|5ES9WPU0q8b?(AFH)4naxPz)W^&Y7=)JsZkd`iz&UFK6D9(M+(k z>?Igs&;$c!-}<54hns8nIZJaVkGV?StdD%rXX{Y3tgMf&yDGx1Oe-*U#msyR2w5@1 z4OsIC+6S+AoM$YBT}S_EHvB#9mX`NOk%lEcuvsqPcts_s@IqVFjg});j|DkEC6LkE zPCZHfU819*a>}YxbTan5K{@+}i8J0vBLc69OV&DGyB})K478cW^l%q!W!ZioSdezW znCNuI;>pq3_p3dk82k7)X9w&0pS1SACr!}61iT#^TKNz+$BAfMGSrUDgltOa#a=+H z^pK1Ld$zaZaSNycfQ;-up;u@JV-}?T629X8q%dp%`Dmt)B)%+qmQ$zye)Qzh(d!uN zrX#bDdrh`mg3TV52rBNek;@qy6C69iCMRWQY&b#ksa~l#Q@J4w34FJ8vG(aIIkQ8M z@^Gqd^Bg{-t+OS5f-L}9>*G=p4#JX40!PSi`+O93syYo~fQM>(`}(4Y<#Y`);;?(- ziQjrWz37K#;u$b$Wzt;5RX4V?@@}D}IIji62z5qPCRln!k+j^vgZ#Ea@$fRq0aLSU z8663UZ$z@qk7k=~EQrk5h_|D|6=&ld|=a_k&5#cBJtN?p>fH9Mwjbt8`P7QCP{qY*O^{eRqEsy5WK_QPnH% z9w-z`o3i4GbBh-&A#?w4*J!h{99zlYc99g#EoGpe^Uy=xQ;&0V^}0oxl8%xiJK&VI zFY!TcM5ZvUeQqz@EYzHJq`Llf>cAz-P)U%=H`G${o||JD(+_#=;ty+9ZXONAOiSs% z$2_EIvP9ha3@SpTwq@VZCc5TQdmd28t>f(hwU@oY?sHt+&E3%$P z#QCc7!~z1xUliH#S8QLHJ?T5y(vW1SN0{AD$<#-Le2JP`?RvlaoIXLgEv6v^1Ryvdu6P!0x;B1@#<-BbrPMgSN ze-5Nb`73qp25n#D)-_!x+N$)W$~`7SI@A}(mt(|*@}aLmVQfH;jb?>OPE5dr`k zgeAzfmM8Gx_NKw<3(h3T@e-glw2}jv-+?lr@w!1FhHiJo%M61bXH@qMeexb%B(ALN|sT-)G@Tr-4}Ot z1p8XAP5rL>5-IYS8XJQd&mwo)j~QQQef#(6MMuwZspwlXexv2uay$b&L-l_X zdZ%}2p*)Nfoj-S9M(W=b{GIR$n#ANDJsl0N5#1UmX-V(U8{s}7M(6XR-!WUodfh`+ zTbM1QXt?MeQbeZ(yZwq&!MnmwU98qMW~~VjmYuC7I3088y??O5jj5#-t{k$c5$qby z9=>F8Y_XoVO|=RA#r$~(sg3bUm1Qr9rly&R8Oc&tqW#B;Zhq}-b`L!J-7UM}Szb#0 z1*6I`O=#%YM%{A$M?jbdP4(&;%BStu0qAd=+Uj zTCz}_so$TiW@0xs`kQq?3z)mw3v84Tj+}BW87MovZpsV#O0fO1E_NrEwT=^B)!*7{ zQe>ms7lurAmfqvBZbR{8mkI+1Fh^*IDR$6Z0q0J zt7eUTY>wB029fa=EAW|opyVI8HD5Iw>kPbr;n!yf#UgUi%&^}W8T=<=f#rOm#3aTF zwY_N9vaYfIbCbL)PZvOyTtDXr^F~ zTnsEcX!6w8?&se5f&4@eopFisnALniVY1*&8C(n2FlR{suHQ^ctUOrG+bG{q=(Q8Q zT4?t;B+F>DK#ED8H=RwqHgowP_lL<~rAOda`c?f^m9$s9LRvN-gXU+x{N*!-RV%d$ z6E2U~(s$ukTSi zHTTXNpNh}@f#ZJ&36ZY7;ok;rfaRyC=qL^6&b3^({2*e^!R{$v3f@?VSzH)^$wPa|Bxn&jNAzlE>i-|_(&xWPMfCZ z^=?7*^#1uqC3{PO{3|DGN?uZ2DMb4M1Re4LTGHbK111R41w!&KK7|=Mn?WUa7B|Yh zOIB~yI4G1EQ|@nL{Zf=m$R@>iLzeidWxRv>So=hBQ-sxackJ5DZ$ zq=RRiZ1EMY;ogC>Z>+Xz;h-3fG?{Iszi0~^w}`gIe@XNdDqrYD)4bh_hNiXY z`UcXb6no5w-o0J36QIL<4XnvUIhBfKWkJ-lHYN2`kqX(MVs|cFwB4SQ9b|!^dkJ`S zNBUWCOglhq6_-6TX09+tLuor-sZfR^Ak0bFeWrseu3hMUtyeC{E<#i?48R6?#?y88 zFIG)oumE4rK(7V%`t7#qSPvK5&EUp_nF_`?J)UyD8PYv=AfJP+_vKrTh^@_fn%$M8 z$)UZjV@Gv-@xz!C?9P|fFi?A<=E*F^jX7x?dmU;`*tU})h(~Wv4!X>_EO9NvGPISC_>{b{>1pJK+1OYj#I9%qsZBN_K#6p* zvh;2lxGVl5o>7}N*~c=A9!HKo$N)!QGIwOgN=Nv5p+DU&ateZL95t-YW(+q^Qx*I$ z&DI2{5M)?CnX|+40DwMf#>t{)CL-9Kjcf^aM;B?uj6p49=sLKu<--RDj>3UNV!z9s z3YQB5-aOFYC{z_}Ldae4;SbTQqP1R`|72@xTM2p^zox~uXXo7Qgm#CM1>eAOW{YC> z8@g_1U7qMv!8Y&%BiG$>YcoC(MV=B5gF0}L!qUk+kC?y=U*c|52z>njl`ckYT5>zF zLX$@>#&IW3PI&2<@#a>`pFl9nxU3c`Y=82ovxcrzWP#QgnPL8wZ)#H4-^M%%7DF;8w5#xFu;pA z_^iSI>F@TdhxqyHI?{C^Bg-iwVuo0X|Bm?jYyR>}4UmYqI08WMdYGO>)!}{YX>l>L zmX?-Y?p3Yz=oc@9>p}Dbh@HGw3CmSv7?V%$o)&~w2%FRQUA-$uv-HGotElGlX^?Xb z4h@-2)Lf0~_P!XEkU$OYe7d?_vGoY841>x3CNqo=$3GOYIHRNEKlQQ190Vl$#H{sv zR)3hzWt@H1k_4ib)YKX$T%eB5Y+p#N=AyK^U{d*=zf4pjFD6*8{;UP0Dxh2}76)5U7 z^?~81J#BPW|4GK;wF`d-f&KlPQ_4E^l}rvQM_!un5h8|a3hUl{bX$sgI)(ypd0?GZ zzVzh!Q(qvxrKa+LB1^YoR~gq{ctU`B1XoD@IfHW_dk=^=%~KT$*Gi{E^QmIxzH~0%a>h`7GI7Q`6zT_PS|4Q%NgC{u!k;m z>uYy!y~Udtzi(fn|^{v}79s%~P_@Zg`^LxWNQ(1_@UNJtXB zHaC_&r)N`oK$(b(i=^xP#BD%O?prz2Lky59aGB#cbSSL~*LtSr?Afz$D|+*jy^J7O zJAUE9!$fN3R$5xxJ@oYFHY-5yW4gL9>5n~fa)@(Yj29#XTwGjBkY#*v`89(ACXBNK zc#z;?+Fju#2foQdkQQW>b*0zP(D26|J85py-~^0BFv)`x{$)xE^+K&h4i*ZS7_(;` z*T4v~>L=+D&R4gI&6}V0+GJ^HXqcIqsl;QvtS4%;WZ=T%q@AcbJ3IZc_rxyGPqgag zTQi*sV-2jXmWQ@n-JYvZR&~Mx-ulekoV>n1^QOR6RJM1n)(>T=LMuo=TY+d-SW;5c z)5DBNETO-GWIqVs6PXQomR;oJJAZLHJ@)m);kdxS-9U~)p&&O5lEgKS=v$GIk$W(h zaIpDgSvix&ChfH$)6~=?WIu8m<|3#FB&I46N&?_LrZb*QPs8L-!CXzz+ZEim949jE))vdb2j#ZYACb@V2{(d2$Js^ZbU+)J~O^`x{JN3Dc zwYIh*(6VIZD1cZQR5X8EeR5;n7?_$?9%vuByZA7F$+pn6(!C4}Loe{`H9(isAI?Fm zD82}Zx&SPc%L;}`BpMV^i$%b{`2ctC@c*@N{ zdxPq%{5-0DlleCWHa7fJY*N0WR-FToUhaNrLA~(xeOruiVn^Ug$zu@6DTTAw-8-xe z=j`PU)Tc*T_U7A&hmw$ysXC0m&D_k;N#_z57ysl#jg1gp1VH7})#(c3T2tOf0xmN4 zBLT;3dU97Lfp8j`i##+*Bd(Uz1*x+(A62?a&+F)@98e0o=eab9>y$cIx5o~ELPg`r zZ(_v@Mof9b1Y8UXZ4=u8@c9g`0-$tJm#nP#_zjD5MX7rq18?#U2}SaB`DzwP_*oan zhThgBrKf>(Je@Z-fwPwk5a+b>VY<8LpXGs^`h@GqT_yctq241#J1j4EbaVs+1ywK2 z4u8P|$wwGitgOxG&fGSf(MFoX|(Nw~DhQr;%18rZblBnK?T^b;QS_2MI(Y=5c76ufj zPhBu03w)hMY~20(_dB}uA?I3MQ-gFSAJ*)%SEC9bpmBTv`F_bR$hc zOhVENH@A>JW+$q}_xfKd1-e`x%?{%c^e9pn0-&N7E`%$TEMjW&gS>x;xf~M}t>Z~> zzXO^>`J~o?Z(YXIEt=h}`3q96@H%2k*(B|WL4++2+Q)U-8o7<70{{hV>A|J%>KQr0 z&yUuWTX$R^A}|^`C)SL8k<#*#b36!tf!NiSs!4WcaXwGdVa#NrB{9=8L*>b2eqV2YrvXE68CWjr2Cd;PH?Vq73C1wAk6}*F`ndkXq9RE^e};Ih zE_8%j9%KI6Tac~LKgP*OIP|xmxE;q{I8*3QKqEP6Z*RXoe(xSV$f_$_TWKCWdK4|^ zg#~b_-+l#uL=&5?lPM4QSTHcQsZ7#gAJh5~UVn{k`?X}%RW zuWfla!IC816ilm-PbO3#B6}G$c~DEV^_%wp%|Ss3P7&N!SyqG~7W*Nn1o(W9#n=C| z>Js<>FA-SnxTq*P=-=!YOPy&D#A_&+%O*mgK&%A9*=1*7;)p^)C1fO*Dp=3%hN{@Y zqM|{lfyhWGfO;YT1$&p2NCWRI1SzUgR79bW$%EU1HR3r8qciL*AP`uT$(tC_nc3Of zPoHvuov|>KEa@OCz`)DbK+*|*_AsTdfzmSIQhL98clHRdvY-Jgsi{$7H`g4tIGxZp z2EqI`B!m)rE((Q^yrK2eSFFuZfWIs5>sM=FQ>S?L!3{v|6UEZ#1LFbU=|MTsU{=Xg zHNtRpU$V{6&m%`XVCCg^qADRorfS4j!^;dn2;((4Y9;W=hOzfjL@5v^_MM76sO*p3 z1+E!cYisU>g@pr9W~Enfo$YOcTzHw=V)d(66DjLDSr-EZE7oYo-^TkqeE43y_g!1t z;N)cJ{JbMPtB&w+a0387r!x@kU%*=_U7LFidh~C-UaOPhO^;+*9taxyhlZ*Ldul>@ zgAE*1poK)40$9lj?E4Q3!1@b^39G~W(LK}%R2xk zM7lae0;rE7K_Lo#DDi@ja~sN`);FnGCC+%c=_+umMSHW$x>iEdfPwN50`H!in`2nt zfN9*HNNa3WPtk}Ef++_AGMBI$BG!nL*zx%KI{xtqp9s)$L){Sd-o1O5@Nyek72L>d ze)sO(dM~e_rEY%qg_y?=Qbu_@Wp_|_U$|m%`_Dw+n0jdPx)g|H00DlcsQv?zW zzUv6<6UyS8+s@O%-5Wp^Ee%Zv-=Ra^psq*Ojdu?y?L<*wup#h+8Qfg7m`#uRDm=>m z!Y^4RK(#db1FO=tPd6z77*)f zz-=e?KLEX`4+7-ld=9%%C>|aj@J6SCD^hTEm0}RMs8@j)oKJLSvXD}-?gCQ>>%LMZ zSP=M?mC1Hy8^anY6k0qKs12qjc*;XFLe@VLl9GdWmDsDy!)5(7vCG6nWK@bqM(p6x zf%Eo8eehWPh)mrev>?BM`3?O0ejp$Mx4Sw`D`{Y0z*}x}jgC2#w7k5Wv7(T+_$Z&$P|T7yx$|Ee7}?wa literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/images/erc4626-rate-loglogext.png b/docs/modules/ROOT/images/erc4626-rate-loglogext.png new file mode 100644 index 0000000000000000000000000000000000000000..127bc7f2fddc37bcfd0863238db2c7874f4e210f GIT binary patch literal 109923 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�S;#YCJ>3E4Gp0$=R%K*`_zT?& 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 NxuSVFSK-!^{{fq-$;AKw literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index fd206aacd..084f76d0b 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -13,6 +13,7 @@ ** xref:erc721.adoc[ERC721] ** xref:erc777.adoc[ERC777] ** xref:erc1155.adoc[ERC1155] +** xref:erc4626.adoc[ERC4626] * xref:governance.adoc[Governance] diff --git a/docs/modules/ROOT/pages/erc4626.adoc b/docs/modules/ROOT/pages/erc4626.adoc new file mode 100644 index 000000000..11c13ca95 --- /dev/null +++ b/docs/modules/ROOT/pages/erc4626.adoc @@ -0,0 +1,193 @@ += ERC4626 +:stem: latexmath + +https://eips.ethereum.org/EIPS/eip-4626[ERC4626] is an extension of xref:erc20.adoc[ERC20] that proposes a standard interface for token vaults. This standard interface can be used by widely different contracts (including lending markets, aggregators, and intrinsically interest bearing tokens), which brings a number of subtleties. Navigating these potential issues is essential to implementing a compliant and composable token vault. + +We provide a base implementation of ERC4626 that includes a simple vault. This contract is designed in a way that allows developers to easily re-configure the vault's behavior, with minimal overrides, while staying compliant. In this guide, we will discuss some security considerations that affect ERC4626. We will also discuss common customizations of the vault. + +[[inflation-attack]] +== Security concern: Inflation attack + +=== Visualizing the vault + +In exchange for the assets deposited into an ERC4626 vault, a user receives shares. These shares can later be burned to redeem the corresponding underlying assets. The number of shares a user gets depends on the amount of assets they put in and on the exchange rate of the vault. This exchange rate is defined by the current liquidity held by the vault. + +- If a vault has 100 tokens to back 200 shares, then each share is worth 0.5 assets. +- If a vault has 200 tokens to back 100 shares, then each share is worth 2.0 assets. + +In other words, the exchange rate can be defined as the slope of the line that passes through the origin and the current number of assets and shares in the vault. Deposits and withdrawals move the vault in this line. + +image::erc4626-rate-linear.png[Exchange rates in linear scale] + +When plotted in log-log scale, the rate is defined similarly, but appears differently (because the point (0,0) is infinitely far away). Rates are represented by "diagonal" lines with different offsets. + +image::erc4626-rate-loglog.png[Exchange rates in logarithmic scale] + +In such a reprentation, widely different rates can be clearly visible in the same graph. This wouldn't be the case in linear scale. + +image::erc4626-rate-loglogext.png[More exchange rates in logarithmic scale] + +=== The attack + +When depositing tokens, the number of shares a user gets is rounded down. This rounding takes away value from the user in favor or the vault (i.e. in favor of all the current share holders). This rounding is often negligible because of the amount at stake. If you deposit 1e9 shares worth of tokens, the rounding will have you lose at most 0.0000001% of your deposit. However if you deposit 10 shares worth of tokens, you could lose 10% of your deposit. Even worse, if you deposit <1 share worth of tokens, then you get 0 shares, and you basically made a donation. + +For a given amount of assets, the more shares you receive the safer you are. If you want to limit your losses to at most 1%, you need to receive at least 100 shares. + +image::erc4626-deposit.png[Depositing assets] + +In the figure we can see that for a given deposit of 500 assets, the number of shares we get and the corresponding rounding losses depend on the exchange rate. If the exchange rate is that of the orange curve, we are getting less than a share, so we lose 100% of our deposit. However, if the exchange rate is that of the green curve, we get 5000 shares, which limits our rounding losses to at most 0.02%. + +image::erc4626-mint.png[Minting shares] + +Symmetrically, if we focus on limiting our losses to a maximum of 0.5%, we need to get at least 200 shares. With the green exchange rate that requires just 20 tokens, but with the orange rate that requires 200000 tokens. + +We can clearly see that that the blue and green curves correspond to vaults that are safer than the yellow and orange curves. + +The idea of an inflation attack is that an attacker can donate assets to the vault to move the rate curve to the right, and make the vault unsafe. + +image::erc4626-attack.png[Inflation attack without protection] + +Figure 6 shows how an attacker can manipulate the rate of an empty vault. First the attacker must deposit a small amount of tokens (1 token) and follow up with a donation of 1e5 tokens directly to the vault to move the exchange rate "right". This puts the vault in a state where any deposit smaller than 1e5 would be completely lost to the vault. Given that the attacker is the only share holder (from their donation), the attacker would steal all the tokens deposited. + +An attacker would typically wait for a user to do the first deposit into the vault, and would frontrun that operation with the attack described above. The risk is low, and the size of the "donation" required to manipulate the vault is equivalent to the size of the deposit that is being attacked. + +In math that gives: + +- stem:[a_0] the attacker deposit +- stem:[a_1] the attacker donation +- stem:[u] the user deposit + +[%header,cols=4*] +|=== +| +| Assets +| Shares +| Rate + +| initial +| stem:[0] +| stem:[0] +| - + +| after attacker's deposit +| stem:[a_0] +| stem:[a_0] +| stem:[1] + +| after attacker's donation +| stem:[a_0+a_1] +| stem:[a_0] +| stem:[\frac{a_0}{a_0+a_1}] +|=== + +This means a deposit of stem:[u] will give stem:[\frac{u \times a_0}{a_0 + a_1}] shares. + +For the attacker to dilute that deposit to 0 shares, causing the user to lose all its deposit, it must ensure that + +[stem] +++++ +\frac{u \times a_0}{a_0+a_1} < 1 \iff u < 1 + \frac{a_1}{a_0} +++++ + +Using stem:[a_0 = 1] and stem:[a_1 = u] is enough. So the attacker only needs stem:[u+1] assets to perform a successful attack. + +It is easy to generalize the above results to scenarios where the attacker is going after a smaller fraction of the user's deposit. In order to target stem:[\frac{u}{n}], the user needs to suffer rounding of a similar fraction, which means the user must receive at most stem:[n] shares. This results in: + +[stem] +++++ +\frac{u \times a_0}{a_0+a_1} < n \iff \frac{u}{n} < 1 + \frac{a_1}{a_0} +++++ + +In this scenario, the attack is stem:[n] times less powerful (in how much it is stealing) and costs stem:[n] times less to execute. In both cases, the amount of funds the attacker needs to commit is equivalent to its potential earnings. + +=== Defending with a virtual offset + +The defense we propose consists of two parts: + +- Use an offset between the "precision" of the representation of shares and assets. Said otherwise, we use more decimal places to represent the shares than the underlying token does to represent the assets. +- Include virtual shares and virtual assets in the exchange rate computation. These virtual assets enforce the conversion rate when the vault is empty. + +These two parts work together in enforcing the security of the vault. First, the increased precision corresponds to a high rate, which we saw is safer as it reduces the rounding error when computing the amount of shares. Second, the virtual assets and shares (in addition to simplifying a lot of the computations) capture part of the donation, making it unprofitable for a developer to perform an attack. + + +Following the previous math definitions, we have: + +- stem:[\delta] the vault offset +- stem:[a_0] the attacker deposit +- stem:[a_1] the attacker donation +- stem:[u] the user deposit + +[%header,cols=4*] +|=== +| +| Assets +| Shares +| Rate + +| initial +| stem:[1] +| stem:[10^\delta] +| stem:[10^\delta] + +| after attacker's deposit +| stem:[1+a_0] +| stem:[10^\delta \times (1+a_0)] +| stem:[10^\delta] + +| after attacker's donation +| stem:[1+a_0+a_1] +| stem:[10^\delta \times (1+a_0)] +| stem:[10^\delta \times \frac{1+a_0}{1+a_0+a_1}] +|=== + +One important thing to note is that the attacker only owns a fraction stem:[\frac{a_0}{1 + a_0}] of the shares, so when doing the donation, he will only be able to recover that fraction stem:[\frac{a_1 \times a_0}{1 + a_0}] of the donation. The remaining stem:[\frac{a_1}{1+a_0}] are captured by the vault. + +[stem] +++++ +\mathit{loss} = \frac{a_1}{1+a_0} +++++ + +When the user deposits stem:[u], he receives + +[stem] +++++ +10^\delta \times u \times \frac{1+a_0}{1+a_0+a_1} +++++ + +For the attacker to dilute that deposit to 0 shares, causing the user to lose all its deposit, it must ensure that + +[stem] +++++ +10^\delta \times u \times \frac{1+a_0}{1+a_0+a_1} < 1 +++++ + +[stem] +++++ +\iff 10^\delta \times u < \frac{1+a_0+a_1}{1+a_0} +++++ + +[stem] +++++ +\iff 10^\delta \times u < 1 + \frac{a_1}{1+a_0} +++++ + +[stem] +++++ +\iff 10^\delta \times u \le \mathit{loss} +++++ + +- If the offset is 0, the attacker loss is at least equal to the user's deposit. +- If the offset is greater than 0, the attacker will have to suffer losses that are orders of magnitude bigger than the amount of value that can hypothetically be stolen from the user. + +This shows that even with an offset of 0, the virtual shares and assets make this attack non profitable for the attacker. Bigger offsets increase the security even further by making any attack on the user extremely wasteful. + +The following figure shows how the offset impacts the initial rate and limits the ability of an attacker with limited funds to inflate it effectively. + +image::erc4626-attack-3a.png[Inflation attack without offset=3] +stem:[\delta = 3], stem:[a_0 = 1], stem:[a_1 = 10^5] + +image::erc4626-attack-3b.png[Inflation attack without offset=3 and an attacker deposit that limits its losses] +stem:[\delta = 3], stem:[a_0 = 100], stem:[a_1 = 10^5] + +image::erc4626-attack-6.png[Inflation attack without offset=6] +stem:[\delta = 6], stem:[a_0 = 1], stem:[a_1 = 10^5] diff --git a/test/token/ERC20/extensions/ERC4626.t.sol b/test/token/ERC20/extensions/ERC4626.t.sol index 3d7dd4a3e..03786b271 100644 --- a/test/token/ERC20/extensions/ERC4626.t.sol +++ b/test/token/ERC20/extensions/ERC4626.t.sol @@ -15,22 +15,4 @@ contract ERC4626StdTest is ERC4626Test { _vaultMayBeEmpty = false; _unlimitedAmount = true; } - - // solhint-disable-next-line func-name-mixedcase - function test_RT_mint_withdraw(ERC4626Test.Init memory init, uint256 shares) public override { - // There is an edge case where we currently behave different than the property tests, - // when all assets are lost to negative yield. - - // Sum all initially deposited assets. - int256 initAssets = 0; - for (uint256 i = 0; i < init.share.length; i++) { - vm.assume(init.share[i] <= uint256(type(int256).max - initAssets)); - initAssets += SafeCast.toInt256(init.share[i]); - } - - // Reject tests where the yield loses all assets from the vault. - vm.assume(init.yield > -initAssets); - - super.test_RT_mint_withdraw(init, shares); - } } diff --git a/test/token/ERC20/extensions/ERC4626.test.js b/test/token/ERC20/extensions/ERC4626.test.js index 5ed2aa132..0c67a2fc7 100644 --- a/test/token/ERC20/extensions/ERC4626.test.js +++ b/test/token/ERC20/extensions/ERC4626.test.js @@ -1,34 +1,16 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const ERC20Decimals = artifacts.require('$ERC20DecimalsMock'); const ERC4626 = artifacts.require('$ERC4626'); -const ERC4626Decimals = artifacts.require('$ERC4626DecimalsMock'); - -const parseToken = token => new BN(token).mul(new BN('1000000000000')); -const parseShare = share => new BN(share).mul(new BN('1000000000000000000')); +const ERC4626OffsetMock = artifacts.require('$ERC4626OffsetMock'); contract('ERC4626', function (accounts) { const [holder, recipient, spender, other, user1, user2] = accounts; const name = 'My Token'; const symbol = 'MTKN'; - - beforeEach(async function () { - this.token = await ERC20Decimals.new(name, symbol, 12); - this.vault = await ERC4626Decimals.new(name + ' Vault', symbol + 'V', this.token.address, 18); - - await this.token.$_mint(holder, web3.utils.toWei('100')); - await this.token.approve(this.vault.address, constants.MAX_UINT256, { from: holder }); - await this.vault.approve(spender, constants.MAX_UINT256, { from: holder }); - }); - - it('metadata', async function () { - expect(await this.vault.name()).to.be.equal(name + ' Vault'); - expect(await this.vault.symbol()).to.be.equal(symbol + 'V'); - expect(await this.vault.decimals()).to.be.bignumber.equal('18'); - expect(await this.vault.asset()).to.be.equal(this.token.address); - }); + const decimals = web3.utils.toBN(18); it('inherit decimals if from asset', async function () { for (const decimals of [0, 9, 12, 18, 36].map(web3.utils.toBN)) { @@ -38,372 +20,378 @@ contract('ERC4626', function (accounts) { } }); - describe('empty vault: no assets & no shares', function () { - it('status', async function () { - expect(await this.vault.totalAssets()).to.be.bignumber.equal('0'); - }); + for (const offset of [0, 6, 18].map(web3.utils.toBN)) { + const parseToken = token => web3.utils.toBN(10).pow(decimals).muln(token); + const parseShare = share => web3.utils.toBN(10).pow(decimals.add(offset)).muln(share); - it('deposit', async function () { - expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256); - expect(await this.vault.previewDeposit(parseToken(1))).to.be.bignumber.equal(parseShare(1)); + const virtualAssets = web3.utils.toBN(1); + const virtualShares = web3.utils.toBN(10).pow(offset); - const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder }); + describe(`offset: ${offset}`, function () { + beforeEach(async function () { + this.token = await ERC20Decimals.new(name, symbol, decimals); + this.vault = await ERC4626OffsetMock.new(name + ' Vault', symbol + 'V', this.token.address, offset); - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: parseToken(1), + await this.token.$_mint(holder, constants.MAX_INT256); // 50% of maximum + await this.token.approve(this.vault.address, constants.MAX_UINT256, { from: holder }); + await this.vault.approve(spender, constants.MAX_UINT256, { from: holder }); }); - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: parseShare(1), + it('metadata', async function () { + expect(await this.vault.name()).to.be.equal(name + ' Vault'); + expect(await this.vault.symbol()).to.be.equal(symbol + 'V'); + expect(await this.vault.decimals()).to.be.bignumber.equal(decimals.add(offset)); + expect(await this.vault.asset()).to.be.equal(this.token.address); + }); + + describe('empty vault: no assets & no shares', function () { + it('status', async function () { + expect(await this.vault.totalAssets()).to.be.bignumber.equal('0'); + }); + + it('deposit', async function () { + expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256); + expect(await this.vault.previewDeposit(parseToken(1))).to.be.bignumber.equal(parseShare(1)); + + const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: holder, + to: this.vault.address, + value: parseToken(1), + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: recipient, + value: parseShare(1), + }); + }); + + it('mint', async function () { + expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256); + expect(await this.vault.previewMint(parseShare(1))).to.be.bignumber.equal(parseToken(1)); + + const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: holder, + to: this.vault.address, + value: parseToken(1), + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: recipient, + value: parseShare(1), + }); + }); + + it('withdraw', async function () { + expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0'); + expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0'); + + const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: this.vault.address, + to: recipient, + value: '0', + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: holder, + to: constants.ZERO_ADDRESS, + value: '0', + }); + }); + + it('redeem', async function () { + expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal('0'); + expect(await this.vault.previewRedeem('0')).to.be.bignumber.equal('0'); + + const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: this.vault.address, + to: recipient, + value: '0', + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: holder, + to: constants.ZERO_ADDRESS, + value: '0', + }); + }); + }); + + describe('inflation attack: offset price by direct deposit of assets', function () { + beforeEach(async function () { + // Donate 1 token to the vault to offset the price + await this.token.$_mint(this.vault.address, parseToken(1)); + }); + + it('status', async function () { + expect(await this.vault.totalSupply()).to.be.bignumber.equal('0'); + expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1)); + }); + + /** + * | offset | deposited assets | redeemable assets | + * |--------|----------------------|----------------------| + * | 0 | 1.000000000000000000 | 0. | + * | 6 | 1.000000000000000000 | 0.999999000000000000 | + * | 18 | 1.000000000000000000 | 0.999999999999999999 | + * + * Attack is possible, but made difficult by the offset. For the attack to be successful + * the attacker needs to frontrun a deposit 10**offset times bigger than what the victim + * was trying to deposit + */ + it('deposit', async function () { + const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); + const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); + + const depositAssets = parseToken(1); + const expectedShares = depositAssets.mul(effectiveShares).div(effectiveAssets); + + expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256); + expect(await this.vault.previewDeposit(depositAssets)).to.be.bignumber.equal(expectedShares); + + const { tx } = await this.vault.deposit(depositAssets, recipient, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: holder, + to: this.vault.address, + value: depositAssets, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: recipient, + value: expectedShares, + }); + }); + + /** + * | offset | deposited assets | redeemable assets | + * |--------|----------------------|----------------------| + * | 0 | 1000000000000000001. | 1000000000000000001. | + * | 6 | 1000000000000000001. | 1000000000000000001. | + * | 18 | 1000000000000000001. | 1000000000000000001. | + * + * Using mint protects against inflation attack, but makes minting shares very expensive. + * The ER20 allowance for the underlying asset is needed to protect the user from (too) + * large deposits. + */ + it('mint', async function () { + const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); + const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); + + const mintShares = parseShare(1); + const expectedAssets = mintShares.mul(effectiveAssets).div(effectiveShares); + + expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256); + expect(await this.vault.previewMint(mintShares)).to.be.bignumber.equal(expectedAssets); + + const { tx } = await this.vault.mint(mintShares, recipient, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: holder, + to: this.vault.address, + value: expectedAssets, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: recipient, + value: mintShares, + }); + }); + + it('withdraw', async function () { + expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0'); + expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0'); + + const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: this.vault.address, + to: recipient, + value: '0', + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: holder, + to: constants.ZERO_ADDRESS, + value: '0', + }); + }); + + it('redeem', async function () { + expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal('0'); + expect(await this.vault.previewRedeem('0')).to.be.bignumber.equal('0'); + + const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: this.vault.address, + to: recipient, + value: '0', + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: holder, + to: constants.ZERO_ADDRESS, + value: '0', + }); + }); + }); + + describe('full vault: assets & shares', function () { + beforeEach(async function () { + // Add 1 token of underlying asset and 100 shares to the vault + await this.token.$_mint(this.vault.address, parseToken(1)); + await this.vault.$_mint(holder, parseShare(100)); + }); + + it('status', async function () { + expect(await this.vault.totalSupply()).to.be.bignumber.equal(parseShare(100)); + expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1)); + }); + + /** + * | offset | deposited assets | redeemable assets | + * |--------|--------------------- |----------------------| + * | 0 | 1.000000000000000000 | 0.999999999999999999 | + * | 6 | 1.000000000000000000 | 0.999999999999999999 | + * | 18 | 1.000000000000000000 | 0.999999999999999999 | + * + * Virtual shares & assets captures part of the value + */ + it('deposit', async function () { + const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); + const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); + + const depositAssets = parseToken(1); + const expectedShares = depositAssets.mul(effectiveShares).div(effectiveAssets); + + expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256); + expect(await this.vault.previewDeposit(depositAssets)).to.be.bignumber.equal(expectedShares); + + const { tx } = await this.vault.deposit(depositAssets, recipient, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: holder, + to: this.vault.address, + value: depositAssets, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: recipient, + value: expectedShares, + }); + }); + + /** + * | offset | deposited assets | redeemable assets | + * |--------|--------------------- |----------------------| + * | 0 | 0.010000000000000001 | 0.010000000000000000 | + * | 6 | 0.010000000000000001 | 0.010000000000000000 | + * | 18 | 0.010000000000000001 | 0.010000000000000000 | + * + * Virtual shares & assets captures part of the value + */ + it('mint', async function () { + const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); + const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); + + const mintShares = parseShare(1); + const expectedAssets = mintShares.mul(effectiveAssets).div(effectiveShares).addn(1); // add for the rounding + + expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256); + expect(await this.vault.previewMint(mintShares)).to.be.bignumber.equal(expectedAssets); + + const { tx } = await this.vault.mint(mintShares, recipient, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: holder, + to: this.vault.address, + value: expectedAssets, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: recipient, + value: mintShares, + }); + }); + + it('withdraw', async function () { + const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); + const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); + + const withdrawAssets = parseToken(1); + const expectedShares = withdrawAssets.mul(effectiveShares).div(effectiveAssets).addn(1); // add for the rounding + + expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal(withdrawAssets); + expect(await this.vault.previewWithdraw(withdrawAssets)).to.be.bignumber.equal(expectedShares); + + const { tx } = await this.vault.withdraw(withdrawAssets, recipient, holder, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: this.vault.address, + to: recipient, + value: withdrawAssets, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: holder, + to: constants.ZERO_ADDRESS, + value: expectedShares, + }); + }); + + it('withdraw with approval', async function () { + await expectRevert( + this.vault.withdraw(parseToken(1), recipient, holder, { from: other }), + 'ERC20: insufficient allowance', + ); + + await this.vault.withdraw(parseToken(1), recipient, holder, { from: spender }); + }); + + it('redeem', async function () { + expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal(parseShare(100)); + expect(await this.vault.previewRedeem(parseShare(100))).to.be.bignumber.equal(parseToken(1)); + + const { tx } = await this.vault.redeem(parseShare(100), recipient, holder, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: this.vault.address, + to: recipient, + value: parseToken(1), + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: holder, + to: constants.ZERO_ADDRESS, + value: parseShare(100), + }); + }); + + it('redeem with approval', async function () { + await expectRevert( + this.vault.redeem(parseShare(100), recipient, holder, { from: other }), + 'ERC20: insufficient allowance', + ); + + await this.vault.redeem(parseShare(100), recipient, holder, { from: spender }); + }); }); }); - - it('mint', async function () { - expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256); - expect(await this.vault.previewMint(parseShare(1))).to.be.bignumber.equal(parseToken(1)); - - const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: parseToken(1), - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: parseShare(1), - }); - }); - - it('withdraw', async function () { - expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0'); - expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0'); - - const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: '0', - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: '0', - }); - }); - - it('redeem', async function () { - expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal('0'); - expect(await this.vault.previewRedeem('0')).to.be.bignumber.equal('0'); - - const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: '0', - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: '0', - }); - }); - }); - - describe('partially empty vault: assets & no shares', function () { - beforeEach(async function () { - await this.token.$_mint(this.vault.address, parseToken(1)); // 1 token - }); - - it('status', async function () { - expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1)); - }); - - it('deposit', async function () { - expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256); - expect(await this.vault.previewDeposit(parseToken(1))).to.be.bignumber.equal(parseShare(1)); - - const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: parseToken(1), - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: parseShare(1), - }); - }); - - it('mint', async function () { - expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256); - expect(await this.vault.previewMint(parseShare(1))).to.be.bignumber.equal(parseToken(1)); - - const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: parseToken(1), - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: parseShare(1), - }); - }); - - it('withdraw', async function () { - expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0'); - expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0'); - - const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: '0', - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: '0', - }); - }); - - it('redeem', async function () { - expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal('0'); - expect(await this.vault.previewRedeem('0')).to.be.bignumber.equal('0'); - - const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: '0', - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: '0', - }); - }); - }); - - describe('partially empty vault: shares & no assets', function () { - beforeEach(async function () { - await this.vault.$_mint(holder, parseShare(1)); // 1 share - }); - - it('status', async function () { - expect(await this.vault.totalAssets()).to.be.bignumber.equal('0'); - }); - - it('deposit', async function () { - expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal('0'); - - // Can deposit 0 (max deposit) - const { tx } = await this.vault.deposit(0, recipient, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: '0', - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: '0', - }); - - // Cannot deposit more than 0 - await expectRevert.unspecified(this.vault.previewDeposit(parseToken(1))); - await expectRevert( - this.vault.deposit(parseToken(1), recipient, { from: holder }), - 'ERC4626: deposit more than max', - ); - }); - - it('mint', async function () { - expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256); - expect(await this.vault.previewMint(parseShare(1))).to.be.bignumber.equal('0'); - - const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: '0', - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: parseShare(1), - }); - }); - - it('withdraw', async function () { - expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0'); - expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0'); - await expectRevert.unspecified(this.vault.previewWithdraw('1')); - - const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: '0', - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: '0', - }); - }); - - it('redeem', async function () { - expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal(parseShare(1)); - expect(await this.vault.previewRedeem(parseShare(1))).to.be.bignumber.equal('0'); - - const { tx } = await this.vault.redeem(parseShare(1), recipient, holder, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: '0', - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: parseShare(1), - }); - }); - }); - - describe('full vault: assets & shares', function () { - beforeEach(async function () { - await this.token.$_mint(this.vault.address, parseToken(1)); // 1 tokens - await this.vault.$_mint(holder, parseShare(100)); // 100 share - }); - - it('status', async function () { - expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1)); - }); - - it('deposit', async function () { - expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256); - expect(await this.vault.previewDeposit(parseToken(1))).to.be.bignumber.equal(parseShare(100)); - - const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: parseToken(1), - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: parseShare(100), - }); - }); - - it('mint', async function () { - expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256); - expect(await this.vault.previewMint(parseShare(1))).to.be.bignumber.equal(parseToken(1).divn(100)); - - const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: parseToken(1).divn(100), - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: parseShare(1), - }); - }); - - it('withdraw', async function () { - expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal(parseToken(1)); - expect(await this.vault.previewWithdraw(parseToken(1))).to.be.bignumber.equal(parseShare(100)); - - const { tx } = await this.vault.withdraw(parseToken(1), recipient, holder, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: parseToken(1), - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: parseShare(100), - }); - }); - - it('withdraw with approval', async function () { - await expectRevert( - this.vault.withdraw(parseToken(1), recipient, holder, { from: other }), - 'ERC20: insufficient allowance', - ); - - await this.vault.withdraw(parseToken(1), recipient, holder, { from: spender }); - }); - - it('redeem', async function () { - expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal(parseShare(100)); - expect(await this.vault.previewRedeem(parseShare(100))).to.be.bignumber.equal(parseToken(1)); - - const { tx } = await this.vault.redeem(parseShare(100), recipient, holder, { from: holder }); - - await expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: parseToken(1), - }); - - await expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: parseShare(100), - }); - }); - - it('redeem with approval', async function () { - await expectRevert( - this.vault.redeem(parseShare(100), recipient, holder, { from: other }), - 'ERC20: insufficient allowance', - ); - - await this.vault.redeem(parseShare(100), recipient, holder, { from: spender }); - }); - }); + } /// Scenario inspired by solmate ERC4626 tests: /// https://github.com/transmissions11/solmate/blob/main/src/test/ERC4626.t.sol @@ -468,8 +456,8 @@ contract('ERC4626', function (accounts) { expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000'); expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3000'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('6000'); + expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2999'); // used to be 3000, but virtual assets/shares captures part of the yield + expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('5999'); // used to be 6000, but virtual assets/shares captures part of the yield expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('9000'); @@ -503,7 +491,7 @@ contract('ERC4626', function (accounts) { await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: user2, to: this.vault.address, - value: '3001', + value: '3000', // used to be 3001 }); await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, @@ -513,10 +501,10 @@ contract('ERC4626', function (accounts) { expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333'); expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('5000'); + expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('4999'); // used to be 5000 expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('9000'); expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333'); - expect(await this.vault.totalAssets()).to.be.bignumber.equal('14001'); + expect(await this.vault.totalAssets()).to.be.bignumber.equal('14000'); // used to be 14001 } // 6. Vault mutates by +3000 tokens @@ -525,10 +513,10 @@ contract('ERC4626', function (accounts) { expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333'); expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('6071'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10929'); + expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('6070'); // used to be 6071 + expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10928'); // used to be 10929 expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333'); - expect(await this.vault.totalAssets()).to.be.bignumber.equal('17001'); + expect(await this.vault.totalAssets()).to.be.bignumber.equal('17000'); // used to be 17001 // 7. Alice redeem 1333 shares (2428 assets) { @@ -541,7 +529,7 @@ contract('ERC4626', function (accounts) { await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: user1, - value: '2428', + value: '2427', // used to be 2428 }); expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000'); @@ -592,7 +580,7 @@ contract('ERC4626', function (accounts) { expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('0'); expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0'); - expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8001'); + expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8000'); // used to be 8001 expect(await this.vault.totalSupply()).to.be.bignumber.equal('4392'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('8001'); } @@ -608,7 +596,7 @@ contract('ERC4626', function (accounts) { await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: user2, - value: '8001', + value: '8000', // used to be 8001 }); expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('0'); @@ -616,7 +604,7 @@ contract('ERC4626', function (accounts) { expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0'); expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0'); expect(await this.vault.totalSupply()).to.be.bignumber.equal('0'); - expect(await this.vault.totalAssets()).to.be.bignumber.equal('0'); + expect(await this.vault.totalAssets()).to.be.bignumber.equal('1'); // used to be 0 } }); }); diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index 5542baf9d..916136cdd 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -177,7 +177,7 @@ contract MathTest is Test { } // Helpers - function _asRounding(uint8 r) private returns (Math.Rounding) { + function _asRounding(uint8 r) private pure returns (Math.Rounding) { vm.assume(r < uint8(type(Math.Rounding).max)); return Math.Rounding(r); } From 96a2297e15f1a4bbcf470d2d0d6cb9c579c63893 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 17 Feb 2023 10:26:10 +0100 Subject: [PATCH 040/197] Add IERC4906 interface and use in ERC721 (#4012) Co-authored-by: Francisco Giordano --- .changeset/lovely-dragons-appear.md | 5 +++++ .changeset/thirty-swans-exercise.md | 5 +++++ contracts/interfaces/IERC4906.sol | 19 +++++++++++++++++++ .../ERC721/extensions/ERC721URIStorage.sol | 7 ++++++- .../extensions/ERC721URIStorage.test.js | 8 +++++++- 5 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 .changeset/lovely-dragons-appear.md create mode 100644 .changeset/thirty-swans-exercise.md create mode 100644 contracts/interfaces/IERC4906.sol diff --git a/.changeset/lovely-dragons-appear.md b/.changeset/lovely-dragons-appear.md new file mode 100644 index 000000000..fe538634a --- /dev/null +++ b/.changeset/lovely-dragons-appear.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`IERC4906`: Add an interface for ERC-4906 that is now Final. diff --git a/.changeset/thirty-swans-exercise.md b/.changeset/thirty-swans-exercise.md new file mode 100644 index 000000000..a460271b0 --- /dev/null +++ b/.changeset/thirty-swans-exercise.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC721URIStorage`: Emit ERC-4906 `MetadataUpdate` in `_setTokenURI`. diff --git a/contracts/interfaces/IERC4906.sol b/contracts/interfaces/IERC4906.sol new file mode 100644 index 000000000..c9eaa1296 --- /dev/null +++ b/contracts/interfaces/IERC4906.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./IERC165.sol"; +import "./IERC721.sol"; + +/// @title EIP-721 Metadata Update Extension +interface IERC4906 is IERC165, IERC721 { + /// @dev This event emits when the metadata of a token is changed. + /// So that the third-party platforms such as NFT market could + /// timely update the images and related attributes of the NFT. + event MetadataUpdate(uint256 _tokenId); + + /// @dev This event emits when the metadata of a range of tokens is changed. + /// So that the third-party platforms such as NFT market could + /// timely update the images and related attributes of the NFTs. + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); +} diff --git a/contracts/token/ERC721/extensions/ERC721URIStorage.sol b/contracts/token/ERC721/extensions/ERC721URIStorage.sol index e83a5ede2..4b919b79e 100644 --- a/contracts/token/ERC721/extensions/ERC721URIStorage.sol +++ b/contracts/token/ERC721/extensions/ERC721URIStorage.sol @@ -4,11 +4,12 @@ pragma solidity ^0.8.0; import "../ERC721.sol"; +import "../../../interfaces/IERC4906.sol"; /** * @dev ERC721 token with storage based token URI management. */ -abstract contract ERC721URIStorage is ERC721 { +abstract contract ERC721URIStorage is IERC4906, ERC721 { using Strings for uint256; // Optional mapping for token URIs @@ -38,6 +39,8 @@ abstract contract ERC721URIStorage is ERC721 { /** * @dev Sets `_tokenURI` as the tokenURI of `tokenId`. * + * Emits {MetadataUpdate}. + * * Requirements: * * - `tokenId` must exist. @@ -45,6 +48,8 @@ abstract contract ERC721URIStorage is ERC721 { function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual { require(_exists(tokenId), "ERC721URIStorage: URI set of nonexistent token"); _tokenURIs[tokenId] = _tokenURI; + + emit MetadataUpdate(tokenId); } /** diff --git a/test/token/ERC721/extensions/ERC721URIStorage.test.js b/test/token/ERC721/extensions/ERC721URIStorage.test.js index c0274c669..0835505e9 100644 --- a/test/token/ERC721/extensions/ERC721URIStorage.test.js +++ b/test/token/ERC721/extensions/ERC721URIStorage.test.js @@ -1,4 +1,4 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); @@ -38,6 +38,12 @@ contract('ERC721URIStorage', function (accounts) { expect(await this.token.tokenURI(firstTokenId)).to.be.equal(sampleUri); }); + it('setting the uri emits an event', async function () { + expectEvent(await this.token.$_setTokenURI(firstTokenId, sampleUri), 'MetadataUpdate', { + _tokenId: firstTokenId, + }); + }); + it('reverts when setting for non existent token id', async function () { await expectRevert( this.token.$_setTokenURI(nonExistentTokenId, sampleUri), From 08d085f2c9cf65552993bc2cf91e7531acb4bc88 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 17 Feb 2023 17:43:23 +0100 Subject: [PATCH 041/197] Fix ERC4906 compliance of ERC721URIStorage (#4055) Co-authored-by: Francisco --- contracts/token/ERC721/extensions/ERC721URIStorage.sol | 7 +++++++ test/token/ERC721/extensions/ERC721URIStorage.test.js | 5 ++++- test/utils/introspection/SupportsInterface.behavior.js | 6 ++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/contracts/token/ERC721/extensions/ERC721URIStorage.sol b/contracts/token/ERC721/extensions/ERC721URIStorage.sol index 4b919b79e..201b05a81 100644 --- a/contracts/token/ERC721/extensions/ERC721URIStorage.sol +++ b/contracts/token/ERC721/extensions/ERC721URIStorage.sol @@ -15,6 +15,13 @@ abstract contract ERC721URIStorage is IERC4906, ERC721 { // Optional mapping for token URIs mapping(uint256 => string) private _tokenURIs; + /** + * @dev See {IERC165-supportsInterface} + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, IERC165) returns (bool) { + return interfaceId == bytes4(0x49064906) || super.supportsInterface(interfaceId); + } + /** * @dev See {IERC721Metadata-tokenURI}. */ diff --git a/test/token/ERC721/extensions/ERC721URIStorage.test.js b/test/token/ERC721/extensions/ERC721URIStorage.test.js index 0835505e9..60c80066c 100644 --- a/test/token/ERC721/extensions/ERC721URIStorage.test.js +++ b/test/token/ERC721/extensions/ERC721URIStorage.test.js @@ -1,7 +1,8 @@ const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); - const { expect } = require('chai'); +const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior'); + const ERC721URIStorageMock = artifacts.require('$ERC721URIStorageMock'); contract('ERC721URIStorage', function (accounts) { @@ -17,6 +18,8 @@ contract('ERC721URIStorage', function (accounts) { this.token = await ERC721URIStorageMock.new(name, symbol); }); + shouldSupportInterfaces(['0x49064906']); + describe('token URI', function () { beforeEach(async function () { await this.token.$_mint(owner, firstTokenId); diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index ba5aca2fc..02d147884 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -100,20 +100,22 @@ function shouldSupportInterfaces(interfaces = []) { it('supportsInterface uses less than 30k gas', async function () { for (const k of interfaces) { - const interfaceId = INTERFACE_IDS[k]; + const interfaceId = INTERFACE_IDS[k] ?? k; expect(await this.contractUnderTest.supportsInterface.estimateGas(interfaceId)).to.be.lte(30000); } }); it('all interfaces are reported as supported', async function () { for (const k of interfaces) { - const interfaceId = INTERFACE_IDS[k]; + const interfaceId = INTERFACE_IDS[k] ?? k; expect(await this.contractUnderTest.supportsInterface(interfaceId)).to.equal(true); } }); it('all interface functions are in ABI', async function () { for (const k of interfaces) { + // skip interfaces for which we don't have a function list + if (INTERFACES[k] === undefined) continue; for (const fnName of INTERFACES[k]) { const fnSig = FN_SIGNATURES[fnName]; expect(this.contractUnderTest.abi.filter(fn => fn.signature === fnSig).length).to.equal(1); From 7ac4349710d3b242dd3ceea3200f1298f1c3d2b3 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Fri, 17 Feb 2023 21:55:42 -0300 Subject: [PATCH 042/197] Delete no longer needed .gitattributes file --- .gitattributes | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 52031de51..000000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.sol linguist-language=Solidity From 815bfbf38395e0c836d09745ba69ae52844e3d8c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 21 Feb 2023 15:18:50 +0100 Subject: [PATCH 043/197] Deprecate the timers library (#4062) Co-authored-by: Francisco --- CHANGELOG.md | 1 + contracts/utils/Timers.sol | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccd279d53..8ad77fe68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Deprecations - `ERC20Permit`: Added the file `IERC20Permit.sol` and `ERC20Permit.sol` and deprecated `draft-IERC20Permit.sol` and `draft-ERC20Permit.sol` since [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) is no longer a Draft. Developers are encouraged to update their imports. ([#3793](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3793)) +- `Timers`: The `Timers` library is now deprecated and will be removed in the next major release. ([#4062](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4062)) ## 4.8.1 (2023-01-12) diff --git a/contracts/utils/Timers.sol b/contracts/utils/Timers.sol index 4bc86f202..1c92b029b 100644 --- a/contracts/utils/Timers.sol +++ b/contracts/utils/Timers.sol @@ -5,6 +5,8 @@ pragma solidity ^0.8.0; /** * @dev Tooling for timepoints, timers and delays + * + * CAUTION: This file is deprecated as of 4.9 and will be removed in the next major release. */ library Timers { struct Timestamp { From 1e245aa54bd4bd0cccdb63524b45010ce043f114 Mon Sep 17 00:00:00 2001 From: Yamen Merhi Date: Tue, 21 Feb 2023 17:16:44 +0200 Subject: [PATCH 044/197] Add `isValidERC1271SignatureNow` to SignatureChecker library (#3932) Co-authored-by: Hadrien Croubois Co-authored-by: Francisco --- .changeset/slimy-knives-hug.md | 5 ++ .../utils/cryptography/SignatureChecker.sol | 19 ++++- .../cryptography/SignatureChecker.test.js | 76 ++++++++++--------- 3 files changed, 61 insertions(+), 39 deletions(-) create mode 100644 .changeset/slimy-knives-hug.md diff --git a/.changeset/slimy-knives-hug.md b/.changeset/slimy-knives-hug.md new file mode 100644 index 000000000..94099eea7 --- /dev/null +++ b/.changeset/slimy-knives-hug.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`SignatureChecker`: Add `isValidERC1271SignatureNow` for checking a signature directly against a smart contract using ERC-1271. diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index e06778deb..c3a724dd0 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -23,10 +23,23 @@ library SignatureChecker { */ function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { (address recovered, ECDSA.RecoverError error) = ECDSA.tryRecover(hash, signature); - if (error == ECDSA.RecoverError.NoError && recovered == signer) { - return true; - } + return + (error == ECDSA.RecoverError.NoError && recovered == signer) || + isValidERC1271SignatureNow(signer, hash, signature); + } + /** + * @dev Checks if a signature is valid for a given signer and data hash. The signature is validated + * against the signer smart contract using ERC1271. + * + * NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus + * change through time. It could return true at block N and false at block N+1 (or the opposite). + */ + function isValidERC1271SignatureNow( + address signer, + bytes32 hash, + bytes memory signature + ) internal view returns (bool) { (bool success, bytes memory result) = signer.staticcall( abi.encodeWithSelector(IERC1271.isValidSignature.selector, hash, signature) ); diff --git a/test/utils/cryptography/SignatureChecker.test.js b/test/utils/cryptography/SignatureChecker.test.js index 11054c3e1..ba8b100d1 100644 --- a/test/utils/cryptography/SignatureChecker.test.js +++ b/test/utils/cryptography/SignatureChecker.test.js @@ -40,44 +40,48 @@ contract('SignatureChecker (ERC1271)', function (accounts) { }); context('ERC1271 wallet', function () { - it('with matching signer and signature', async function () { - expect( - await this.signaturechecker.$isValidSignatureNow( - this.wallet.address, - toEthSignedMessageHash(TEST_MESSAGE), - this.signature, - ), - ).to.equal(true); - }); + for (const signature of ['isValidERC1271SignatureNow', 'isValidSignatureNow']) { + context(signature, function () { + it('with matching signer and signature', async function () { + expect( + await this.signaturechecker[`$${signature}`]( + this.wallet.address, + toEthSignedMessageHash(TEST_MESSAGE), + this.signature, + ), + ).to.equal(true); + }); - it('with invalid signer', async function () { - expect( - await this.signaturechecker.$isValidSignatureNow( - this.signaturechecker.address, - toEthSignedMessageHash(TEST_MESSAGE), - this.signature, - ), - ).to.equal(false); - }); + it('with invalid signer', async function () { + expect( + await this.signaturechecker[`$${signature}`]( + this.signaturechecker.address, + toEthSignedMessageHash(TEST_MESSAGE), + this.signature, + ), + ).to.equal(false); + }); - it('with invalid signature', async function () { - expect( - await this.signaturechecker.$isValidSignatureNow( - this.wallet.address, - toEthSignedMessageHash(WRONG_MESSAGE), - this.signature, - ), - ).to.equal(false); - }); + it('with invalid signature', async function () { + expect( + await this.signaturechecker[`$${signature}`]( + this.wallet.address, + toEthSignedMessageHash(WRONG_MESSAGE), + this.signature, + ), + ).to.equal(false); + }); - it('with malicious wallet', async function () { - expect( - await this.signaturechecker.$isValidSignatureNow( - this.malicious.address, - toEthSignedMessageHash(TEST_MESSAGE), - this.signature, - ), - ).to.equal(false); - }); + it('with malicious wallet', async function () { + expect( + await this.signaturechecker[`$${signature}`]( + this.malicious.address, + toEthSignedMessageHash(TEST_MESSAGE), + this.signature, + ), + ).to.equal(false); + }); + }); + } }); }); From df814dd374519a9388a617f500019d61bc239382 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 19:26:14 -0300 Subject: [PATCH 045/197] Update crytic/slither-action action to v0.3.0 (#4064) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 8bb32dcc3..c37bf935b 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -82,7 +82,7 @@ jobs: - name: Set up environment uses: ./.github/actions/setup - run: rm foundry.toml - - uses: crytic/slither-action@v0.2.0 + - uses: crytic/slither-action@v0.3.0 codespell: if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' From 227473e88ce756afe41193b7d2407fdc01d7e784 Mon Sep 17 00:00:00 2001 From: Pascal Marco Caversaccio Date: Tue, 21 Feb 2023 23:27:34 +0100 Subject: [PATCH 046/197] Fix comment in `ERC4626` on decimals default (#4060) Co-authored-by: Hadrien Croubois --- contracts/token/ERC20/extensions/ERC4626.sol | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index 16656bd4f..3846cbbbb 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -79,9 +79,10 @@ abstract contract ERC4626 is ERC20, IERC4626 { } /** - * @dev Decimals are read from the underlying asset in the constructor and cached. If this fails (e.g., the asset - * has not been created yet), the cached value is set to a default obtained by `super.decimals()` (which depends on - * inheritance but is most likely 18). Override this function in order to set a guaranteed hardcoded value. + * @dev Decimals are computed by adding the decimal offset on top of the underlying asset's decimals. This + * "original" value is cached during construction of the vault contract. If this read operation fails (e.g., the + * asset has not been created yet), a default of 18 is used to represent the underlying asset's decimals. + * * See {IERC20Metadata-decimals}. */ function decimals() public view virtual override(IERC20Metadata, ERC20) returns (uint8) { @@ -194,9 +195,6 @@ abstract contract ERC4626 is ERC20, IERC4626 { /** * @dev Internal conversion function (from assets to shares) with support for rounding direction. - * - * Will revert if assets > 0, totalSupply > 0 and totalAssets = 0. That corresponds to a case where any asset - * would represent an infinite amount of shares. */ function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) { return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding); @@ -213,7 +211,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { * @dev Deposit/mint common workflow. */ function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual { - // If _asset is ERC777, `transferFrom` can trigger a reenterancy BEFORE the transfer happens through the + // If _asset is ERC777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the // `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer, // calls the vault, which is assumed not malicious. // From adb861fb3bb4b4167f10307b8d091326115502f9 Mon Sep 17 00:00:00 2001 From: Francisco Date: Wed, 22 Feb 2023 06:00:41 -0300 Subject: [PATCH 047/197] Change Governor.cancel to receive all parameters (#4056) --- contracts/governance/Governor.sol | 20 +++-- contracts/governance/IGovernor.sol | 11 ++- .../GovernorCompatibilityBravo.sol | 77 +++++++++++++++---- .../GovernorCompatibilityBravoMock.sol | 9 ++- contracts/mocks/wizard/MyGovernor3.sol | 9 ++- test/helpers/governance.js | 8 +- 6 files changed, 99 insertions(+), 35 deletions(-) diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index e3691a3ed..1483da6d2 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -336,10 +336,16 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive /** * @dev See {IGovernor-cancel}. */ - function cancel(uint256 proposalId) public virtual override { + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + 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"); - _cancel(proposalId); + return _cancel(targets, values, calldatas, descriptionHash); } /** @@ -407,16 +413,8 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive bytes[] memory calldatas, bytes32 descriptionHash ) internal virtual returns (uint256) { - return _cancel(hashProposal(targets, values, calldatas, descriptionHash)); - } + uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); - /** - * @dev Internal cancel mechanism: locks up the proposal timer, preventing it from being re-submitted. Marks it as - * canceled to allow distinguishing it from executed proposals. - * - * Emits a {IGovernor-ProposalCanceled} event. - */ - function _cancel(uint256 proposalId) internal virtual returns (uint256) { ProposalState status = state(proposalId); require( diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index b12d2f632..70f81efe8 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -235,12 +235,17 @@ abstract contract IGovernor is IERC165, IERC6372 { ) public payable virtual returns (uint256 proposalId); /** - * @dev Cancel a proposal. This is restricted to Pending proposal (before the vote starts) and is restricted to - * the proposal's proposer. + * @dev Cancel a proposal. A proposal is cancellable by the proposer, but only while it is Pending state, i.e. + * before the vote starts. * * Emits a {ProposalCanceled} event. */ - function cancel(uint256 proposalId) public virtual; + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public virtual returns (uint256 proposalId); /** * @dev Cast a vote diff --git a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol index 1b8c09368..39c895bf0 100644 --- a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol @@ -77,29 +77,55 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp * @dev See {IGovernorCompatibilityBravo-queue}. */ function queue(uint256 proposalId) public virtual override { - ProposalDetails storage details = _proposalDetails[proposalId]; - queue( - details.targets, - details.values, - _encodeCalldata(details.signatures, details.calldatas), - details.descriptionHash - ); + ( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) = _getProposalParameters(proposalId); + + queue(targets, values, calldatas, descriptionHash); } /** * @dev See {IGovernorCompatibilityBravo-execute}. */ function execute(uint256 proposalId) public payable virtual override { - ProposalDetails storage details = _proposalDetails[proposalId]; - execute( - details.targets, - details.values, - _encodeCalldata(details.signatures, details.calldatas), - details.descriptionHash - ); + ( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) = _getProposalParameters(proposalId); + + execute(targets, values, calldatas, descriptionHash); } - function cancel(uint256 proposalId) public virtual override(IGovernor, Governor) { + /** + * @dev Cancel a proposal with GovernorBravo logic. + */ + function cancel(uint256 proposalId) public virtual { + ( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) = _getProposalParameters(proposalId); + + cancel(targets, values, calldatas, descriptionHash); + } + + /** + * @dev Cancel a proposal with GovernorBravo logic. At any moment a proposal can be cancelled, either by the + * proposer, or by third parties if the proposer's voting power has dropped below the proposal threshold. + */ + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public virtual override(IGovernor, Governor) returns (uint256) { + uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); address proposer = _proposalDetails[proposalId].proposer; require( @@ -107,7 +133,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp "GovernorBravo: proposer above threshold" ); - _cancel(proposalId); + return _cancel(targets, values, calldatas, descriptionHash); } /** @@ -128,6 +154,25 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp return fullcalldatas; } + /** + * @dev Retrieve proposal parameters by id, with fully encoded calldatas. + */ + function _getProposalParameters( + uint256 proposalId + ) + private + view + returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) + { + ProposalDetails storage details = _proposalDetails[proposalId]; + return ( + details.targets, + details.values, + _encodeCalldata(details.signatures, details.calldatas), + details.descriptionHash + ); + } + /** * @dev Store proposal metadata for later lookup */ diff --git a/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol b/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol index 2727794f6..1b87d1433 100644 --- a/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol +++ b/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol @@ -66,8 +66,13 @@ abstract contract GovernorCompatibilityBravoMock is return super.execute(targets, values, calldatas, salt); } - function cancel(uint256 proposalId) public override(Governor, GovernorCompatibilityBravo, IGovernor) { - super.cancel(proposalId); + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public override(Governor, GovernorCompatibilityBravo, IGovernor) returns (uint256) { + return super.cancel(targets, values, calldatas, descriptionHash); } function _execute( diff --git a/contracts/mocks/wizard/MyGovernor3.sol b/contracts/mocks/wizard/MyGovernor3.sol index 4192cae94..f4d295156 100644 --- a/contracts/mocks/wizard/MyGovernor3.sol +++ b/contracts/mocks/wizard/MyGovernor3.sol @@ -54,8 +54,13 @@ contract MyGovernor is return super.propose(targets, values, calldatas, description); } - function cancel(uint256 proposalId) public override(Governor, GovernorCompatibilityBravo, IGovernor) { - super.cancel(proposalId); + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public override(Governor, GovernorCompatibilityBravo, IGovernor) returns (uint256) { + return super.cancel(targets, values, calldatas, descriptionHash); } function _execute( diff --git a/test/helpers/governance.js b/test/helpers/governance.js index 1ffa086cb..4b38b7588 100644 --- a/test/helpers/governance.js +++ b/test/helpers/governance.js @@ -68,7 +68,13 @@ class GovernorHelper { switch (visibility) { case 'external': - return this.governor.methods['cancel(uint256)'](...concatOpts([proposal.id], opts)); + if (proposal.useCompatibilityInterface) { + return this.governor.methods['cancel(uint256)'](...concatOpts([proposal.id], opts)); + } else { + return this.governor.methods['cancel(address[],uint256[],bytes[],bytes32)']( + ...concatOpts(proposal.shortProposal, opts), + ); + } case 'internal': return this.governor.methods['$_cancel(address[],uint256[],bytes[],bytes32)']( ...concatOpts(proposal.shortProposal, opts), From 53235263a377612b546a5c1922e1876689eba525 Mon Sep 17 00:00:00 2001 From: Yamen Merhi Date: Wed, 22 Feb 2023 16:22:50 +0200 Subject: [PATCH 048/197] Implement `0x00` version of EIP-191 in ECDSA Library (#4063) --- .changeset/small-cars-appear.md | 5 +++++ contracts/utils/cryptography/ECDSA.sol | 10 ++++++++++ test/helpers/sign.js | 16 ++++++++++++++++ test/utils/cryptography/ECDSA.test.js | 11 ++++++++++- 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 .changeset/small-cars-appear.md diff --git a/.changeset/small-cars-appear.md b/.changeset/small-cars-appear.md new file mode 100644 index 000000000..0263bcd18 --- /dev/null +++ b/.changeset/small-cars-appear.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ECDSA`: Add a function `toDataWithIntendedValidatorHash` that encodes data with version 0x00 following EIP-191. diff --git a/contracts/utils/cryptography/ECDSA.sol b/contracts/utils/cryptography/ECDSA.sol index a499b546b..77279eb4f 100644 --- a/contracts/utils/cryptography/ECDSA.sol +++ b/contracts/utils/cryptography/ECDSA.sol @@ -204,4 +204,14 @@ library ECDSA { data := keccak256(ptr, 0x42) } } + + /** + * @dev Returns an Ethereum Signed Data with intended validator, created from a + * `validator` and `data` according to the version 0 of EIP-191. + * + * See {recover}. + */ + function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x00", validator, data)); + } } diff --git a/test/helpers/sign.js b/test/helpers/sign.js index 417ef591d..d537116bb 100644 --- a/test/helpers/sign.js +++ b/test/helpers/sign.js @@ -4,6 +4,21 @@ function toEthSignedMessageHash(messageHex) { return web3.utils.sha3(Buffer.concat([prefix, messageBuffer])); } +/** + * Create a signed data with intended validator according to the version 0 of EIP-191 + * @param validatorAddress The address of the validator + * @param dataHex The data to be concatenated with the prefix and signed + */ +function toDataWithIntendedValidatorHash(validatorAddress, dataHex) { + const validatorBuffer = Buffer.from(web3.utils.hexToBytes(validatorAddress)); + const dataBuffer = Buffer.from(web3.utils.hexToBytes(dataHex)); + const preambleBuffer = Buffer.from('\x19'); + const versionBuffer = Buffer.from('\x00'); + const ethMessage = Buffer.concat([preambleBuffer, versionBuffer, validatorBuffer, dataBuffer]); + + return web3.utils.sha3(ethMessage); +} + /** * Create a signer between a contract and a signer for a voucher of method, args, and redeemer * Note that `method` is the web3 method, not the truffle-contract method @@ -43,5 +58,6 @@ const getSignFor = module.exports = { toEthSignedMessageHash, + toDataWithIntendedValidatorHash, getSignFor, }; diff --git a/test/utils/cryptography/ECDSA.test.js b/test/utils/cryptography/ECDSA.test.js index 3b19cde60..ae737086b 100644 --- a/test/utils/cryptography/ECDSA.test.js +++ b/test/utils/cryptography/ECDSA.test.js @@ -1,5 +1,5 @@ const { expectRevert } = require('@openzeppelin/test-helpers'); -const { toEthSignedMessageHash } = require('../../helpers/sign'); +const { toEthSignedMessageHash, toDataWithIntendedValidatorHash } = require('../../helpers/sign'); const { expect } = require('chai'); @@ -8,6 +8,7 @@ const ECDSA = artifacts.require('$ECDSA'); const TEST_MESSAGE = web3.utils.sha3('OpenZeppelin'); const WRONG_MESSAGE = web3.utils.sha3('Nope'); const NON_HASH_MESSAGE = '0x' + Buffer.from('abcd').toString('hex'); +const RANDOM_ADDRESS = web3.utils.toChecksumAddress(web3.utils.randomHex(20)); function to2098Format(signature) { const long = web3.utils.hexToBytes(signature); @@ -248,4 +249,12 @@ contract('ECDSA', function (accounts) { ); }); }); + + context('toDataWithIntendedValidatorHash', function () { + it('returns the hash correctly', async function () { + expect( + await this.ecdsa.methods['$toDataWithIntendedValidatorHash(address,bytes)'](RANDOM_ADDRESS, NON_HASH_MESSAGE), + ).to.equal(toDataWithIntendedValidatorHash(RANDOM_ADDRESS, NON_HASH_MESSAGE)); + }); + }); }); From 7b3e7b7055d6e1226d4920ca7d7c446c0c0bfff5 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 22 Feb 2023 19:23:06 +0100 Subject: [PATCH 049/197] Re-enable ERC4626 property test on empty vaults (#4068) --- test/token/ERC20/extensions/ERC4626.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/token/ERC20/extensions/ERC4626.t.sol b/test/token/ERC20/extensions/ERC4626.t.sol index 03786b271..95514531c 100644 --- a/test/token/ERC20/extensions/ERC4626.t.sol +++ b/test/token/ERC20/extensions/ERC4626.t.sol @@ -12,7 +12,7 @@ contract ERC4626StdTest is ERC4626Test { _underlying_ = address(new ERC20Mock()); _vault_ = address(new ERC4626Mock(_underlying_)); _delta_ = 0; - _vaultMayBeEmpty = false; + _vaultMayBeEmpty = true; _unlimitedAmount = true; } } From 9d06a1b64bcff51349d832222c165b03f9c1f316 Mon Sep 17 00:00:00 2001 From: Francisco Date: Thu, 23 Feb 2023 08:52:10 -0300 Subject: [PATCH 050/197] Remove unused Solhint overrides (#4069) --- contracts/utils/cryptography/EIP712.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol index d0e52c396..e06d0066b 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -38,7 +38,6 @@ abstract contract EIP712 is IERC5267 { bytes32 private constant _TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); - /* solhint-disable var-name-mixedcase */ // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to // invalidate the cached domain separator if the chain id changes. bytes32 private immutable _cachedDomainSeparator; @@ -53,8 +52,6 @@ abstract contract EIP712 is IERC5267 { bytes32 private immutable _hashedName; bytes32 private immutable _hashedVersion; - /* solhint-enable var-name-mixedcase */ - /** * @dev Initializes the domain separator and parameter caches. * From a6b8366980d8b28cbe4be7f1798719f0fac4cac1 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 23 Feb 2023 21:06:47 +0100 Subject: [PATCH 051/197] Improve ERC4626 event coverage (#4072) --- test/token/ERC20/extensions/ERC4626.test.js | 106 +++++++++++++++++++- 1 file changed, 101 insertions(+), 5 deletions(-) diff --git a/test/token/ERC20/extensions/ERC4626.test.js b/test/token/ERC20/extensions/ERC4626.test.js index 0c67a2fc7..2f7b99d38 100644 --- a/test/token/ERC20/extensions/ERC4626.test.js +++ b/test/token/ERC20/extensions/ERC4626.test.js @@ -66,6 +66,13 @@ contract('ERC4626', function (accounts) { to: recipient, value: parseShare(1), }); + + await expectEvent.inTransaction(tx, this.vault, 'Deposit', { + sender: holder, + owner: recipient, + assets: parseToken(1), + shares: parseShare(1), + }); }); it('mint', async function () { @@ -85,6 +92,13 @@ contract('ERC4626', function (accounts) { to: recipient, value: parseShare(1), }); + + await expectEvent.inTransaction(tx, this.vault, 'Deposit', { + sender: holder, + owner: recipient, + assets: parseToken(1), + shares: parseShare(1), + }); }); it('withdraw', async function () { @@ -104,6 +118,14 @@ contract('ERC4626', function (accounts) { to: constants.ZERO_ADDRESS, value: '0', }); + + await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { + sender: holder, + receiver: recipient, + owner: holder, + assets: '0', + shares: '0', + }); }); it('redeem', async function () { @@ -123,6 +145,14 @@ contract('ERC4626', function (accounts) { to: constants.ZERO_ADDRESS, value: '0', }); + + await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { + sender: holder, + receiver: recipient, + owner: holder, + assets: '0', + shares: '0', + }); }); }); @@ -171,6 +201,13 @@ contract('ERC4626', function (accounts) { to: recipient, value: expectedShares, }); + + await expectEvent.inTransaction(tx, this.vault, 'Deposit', { + sender: holder, + owner: recipient, + assets: depositAssets, + shares: expectedShares, + }); }); /** @@ -207,6 +244,13 @@ contract('ERC4626', function (accounts) { to: recipient, value: mintShares, }); + + await expectEvent.inTransaction(tx, this.vault, 'Deposit', { + sender: holder, + owner: recipient, + assets: expectedAssets, + shares: mintShares, + }); }); it('withdraw', async function () { @@ -226,6 +270,14 @@ contract('ERC4626', function (accounts) { to: constants.ZERO_ADDRESS, value: '0', }); + + await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { + sender: holder, + receiver: recipient, + owner: holder, + assets: '0', + shares: '0', + }); }); it('redeem', async function () { @@ -245,6 +297,14 @@ contract('ERC4626', function (accounts) { to: constants.ZERO_ADDRESS, value: '0', }); + + await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { + sender: holder, + receiver: recipient, + owner: holder, + assets: '0', + shares: '0', + }); }); }); @@ -292,6 +352,13 @@ contract('ERC4626', function (accounts) { to: recipient, value: expectedShares, }); + + await expectEvent.inTransaction(tx, this.vault, 'Deposit', { + sender: holder, + owner: recipient, + assets: depositAssets, + shares: expectedShares, + }); }); /** @@ -326,6 +393,13 @@ contract('ERC4626', function (accounts) { to: recipient, value: mintShares, }); + + await expectEvent.inTransaction(tx, this.vault, 'Deposit', { + sender: holder, + owner: recipient, + assets: expectedAssets, + shares: mintShares, + }); }); it('withdraw', async function () { @@ -351,6 +425,14 @@ contract('ERC4626', function (accounts) { to: constants.ZERO_ADDRESS, value: expectedShares, }); + + await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { + sender: holder, + receiver: recipient, + owner: holder, + assets: withdrawAssets, + shares: expectedShares, + }); }); it('withdraw with approval', async function () { @@ -363,21 +445,35 @@ contract('ERC4626', function (accounts) { }); it('redeem', async function () { - expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal(parseShare(100)); - expect(await this.vault.previewRedeem(parseShare(100))).to.be.bignumber.equal(parseToken(1)); + const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); + const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); - const { tx } = await this.vault.redeem(parseShare(100), recipient, holder, { from: holder }); + const redeemShares = parseShare(100); + const expectedAssets = redeemShares.mul(effectiveAssets).div(effectiveShares); + + expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal(redeemShares); + expect(await this.vault.previewRedeem(redeemShares)).to.be.bignumber.equal(expectedAssets); + + const { tx } = await this.vault.redeem(redeemShares, recipient, holder, { from: holder }); await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: recipient, - value: parseToken(1), + value: expectedAssets, }); await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: holder, to: constants.ZERO_ADDRESS, - value: parseShare(100), + value: redeemShares, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { + sender: holder, + receiver: recipient, + owner: holder, + assets: expectedAssets, + shares: redeemShares, }); }); From 6e88df28cb45712790c02b7aef7a866a3b9c5512 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 24 Feb 2023 00:06:22 +0100 Subject: [PATCH 052/197] Mark ERC777 and ERC1820 as deprecated (#4066) --- CHANGELOG.md | 2 ++ contracts/token/ERC777/ERC777.sol | 2 ++ contracts/token/ERC777/README.adoc | 2 ++ contracts/utils/introspection/ERC1820Implementer.sol | 2 ++ docs/modules/ROOT/pages/erc777.adoc | 2 ++ 5 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ad77fe68..e882fd516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - `ERC20Permit`: Added the file `IERC20Permit.sol` and `ERC20Permit.sol` and deprecated `draft-IERC20Permit.sol` and `draft-ERC20Permit.sol` since [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) is no longer a Draft. Developers are encouraged to update their imports. ([#3793](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3793)) - `Timers`: The `Timers` library is now deprecated and will be removed in the next major release. ([#4062](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4062)) +- `ERC777`: The `ERC777` token standard is no longer supported by OpenZeppelin. Our implementation is now deprecated and will be removed in the next major release. The corresponding standard interfaces remain available. ([#4066](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4066)) +- `ERC1820Implementer`: The `ERC1820` pseudo-introspection mechanism is no longer supported by OpenZeppelin. Our implementation is now deprecated and will be removed in the next major release. The corresponding standard interfaces remain available. ([#4066](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4066)) ## 4.8.1 (2023-01-12) diff --git a/contracts/token/ERC777/ERC777.sol b/contracts/token/ERC777/ERC777.sol index c1503c4df..0e5af5d08 100644 --- a/contracts/token/ERC777/ERC777.sol +++ b/contracts/token/ERC777/ERC777.sol @@ -25,6 +25,8 @@ import "../../utils/introspection/IERC1820Registry.sol"; * Additionally, the {IERC777-granularity} value is hard-coded to `1`, meaning that there * are no special restrictions in the amount of tokens that created, moved, or * destroyed. This makes integration with ERC20 applications seamless. + * + * CAUTION: This file is deprecated as of v4.9 and will be removed in the next major release. */ contract ERC777 is Context, IERC777, IERC20 { using Address for address; diff --git a/contracts/token/ERC777/README.adoc b/contracts/token/ERC777/README.adoc index 5012a3110..ac326abe6 100644 --- a/contracts/token/ERC777/README.adoc +++ b/contracts/token/ERC777/README.adoc @@ -3,6 +3,8 @@ [.readme-notice] NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/token/erc777 +CAUTION: As of v4.9, OpenZeppelin's implementation of ERC-777 is deprecated and will be removed in the next major release. + This set of interfaces and contracts are all related to the https://eips.ethereum.org/EIPS/eip-777[ERC777 token standard]. TIP: For an overview of ERC777 tokens and a walk through on how to create a token contract read our xref:ROOT:erc777.adoc[ERC777 guide]. diff --git a/contracts/utils/introspection/ERC1820Implementer.sol b/contracts/utils/introspection/ERC1820Implementer.sol index ac5a884c0..cf4b50498 100644 --- a/contracts/utils/introspection/ERC1820Implementer.sol +++ b/contracts/utils/introspection/ERC1820Implementer.sol @@ -12,6 +12,8 @@ import "./IERC1820Implementer.sol"; * declare their willingness to be implementers. * {IERC1820Registry-setInterfaceImplementer} should then be called for the * registration to be complete. + * + * CAUTION: This file is deprecated as of v4.9 and will be removed in the next major release. */ contract ERC1820Implementer is IERC1820Implementer { bytes32 private constant _ERC1820_ACCEPT_MAGIC = keccak256("ERC1820_ACCEPT_MAGIC"); diff --git a/docs/modules/ROOT/pages/erc777.adoc b/docs/modules/ROOT/pages/erc777.adoc index d79fbee28..4a0af16c3 100644 --- a/docs/modules/ROOT/pages/erc777.adoc +++ b/docs/modules/ROOT/pages/erc777.adoc @@ -1,5 +1,7 @@ = ERC777 +CAUTION: As of v4.9, OpenZeppelin's implementation of ERC-777 is deprecated and will be removed in the next major release. + Like xref:erc20.adoc[ERC20], ERC777 is a standard for xref:tokens.adoc#different-kinds-of-tokens[_fungible_ tokens], and is focused around allowing more complex interactions when trading tokens. More generally, it brings tokens and Ether closer together by providing the equivalent of a `msg.value` field, but for tokens. The standard also brings multiple quality-of-life improvements, such as getting rid of the confusion around `decimals`, minting and burning with proper events, among others, but its killer feature is *receive hooks*. A hook is simply a function in a contract that is called when tokens are sent to it, meaning *accounts and contracts can react to receiving tokens*. From 62dbb1b06a5e86ef72fb9e0716d37fd20dbe522d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 24 Feb 2023 15:48:30 +0100 Subject: [PATCH 053/197] Document clock modes for token and governor (#4058) Co-authored-by: Francisco --- contracts/interfaces/README.adoc | 13 ++- docs/modules/ROOT/pages/governance.adoc | 121 ++++++++++++++++++++++-- 2 files changed, 125 insertions(+), 9 deletions(-) diff --git a/contracts/interfaces/README.adoc b/contracts/interfaces/README.adoc index 5b4cedf95..8748da8a4 100644 --- a/contracts/interfaces/README.adoc +++ b/contracts/interfaces/README.adoc @@ -30,6 +30,11 @@ are useful to interact with third party contracts that implement them. - {IERC3156FlashLender} - {IERC3156FlashBorrower} - {IERC4626} +- {IERC4906} +- {IERC5267} +- {IERC5313} +- {IERC5805} +- {IERC6372} == Detailed ABI @@ -53,4 +58,10 @@ are useful to interact with third party contracts that implement them. {{IERC3156FlashBorrower}} -{{IERC4626}} \ No newline at end of file +{{IERC4626}} + +{{IERC5267}} + +{{IERC5805}} + +{{IERC6372}} diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index eb76321bf..cd7dd4e6f 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -119,13 +119,15 @@ contract MyToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper { } ``` -NOTE:The only other source of voting power available in OpenZeppelin Contracts currently is xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`]. ERC721 tokens that don't provide this functionality can be wrapped into a voting tokens using a combination of xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`] and xref:api:token/ERC721Wrapper.adoc#ERC721Wrapper[`ERC721Wrapper`]. +NOTE: The only other source of voting power available in OpenZeppelin Contracts currently is xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`]. ERC721 tokens that don't provide this functionality can be wrapped into a voting tokens using a combination of xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`] and xref:api:token/ERC721Wrapper.adoc#ERC721Wrapper[`ERC721Wrapper`]. + +NOTE: The internal clock used by the token to store voting balances will dictate the operating mode of the Governor contract attached to it. By default, block numbers are used. Since v4.9, developers can override the xref:api:interfaces.adoc#IERC6372[IERC6372] clock to use timestamps instead of block numbers. === Governor Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, 3) what options people have when casting a vote and how those votes are counted, and 4) what type of token should be used to vote. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. -For 1) we will use the GovernorVotes module, which hooks to an IVotes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. +For 1) we will use the GovernorVotes module, which hooks to an IVotes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. This module also discovers the clock mode (ERC6372) used by the token and applies it to the Governor. For 2) we will use GovernorVotesQuorumFraction which works together with ERC20Votes to define quorum as a percentage of the total supply at the block a proposal’s voting power is retrieved. This requires a constructor parameter to set the percentage. Most Governors nowadays use 4%, so we will initialize the module with parameter 4 (this indicates the percentage, resulting in 4%). @@ -137,7 +139,7 @@ votingDelay: How long after a proposal is created should voting power be fixed. votingPeriod: How long does a proposal remain open to votes. -These parameters are specified in number of blocks. Assuming block time of around 12 seconds, we will set votingDelay = 1 day = 7200 blocks, and votingPeriod = 1 week = 50400 blocks. +These parameters are specified in the unit defined in the token's clock. Assuming the token uses block numbers, and assuming block time of around 12 seconds, we will have set votingDelay = 1 day = 7200 blocks, and votingPeriod = 1 week = 50400 blocks. We can optionally set a proposal threshold as well. This restricts proposal creation to accounts who have enough voting power. @@ -160,11 +162,11 @@ contract MyGovernor is Governor, GovernorCompatibilityBravo, GovernorVotes, Gove {} function votingDelay() public pure override returns (uint256) { - return 6575; // 1 day + return 7200; // 1 day } function votingPeriod() public pure override returns (uint256) { - return 46027; // 1 week + return 50400; // 1 week } function proposalThreshold() public pure override returns (uint256) { @@ -223,7 +225,6 @@ contract MyGovernor is Governor, GovernorCompatibilityBravo, GovernorVotes, Gove return super.supportsInterface(interfaceId); } } - ``` === Timelock @@ -261,7 +262,7 @@ const grantAmount = ...; const transferCalldata = token.interface.encodeFunctionData(‘transfer’, [teamAddress, grantAmount]); ``` -Now we are ready to call the propose function of the governor. Note that we don’t pass in one array of actions, but instead three arrays corresponding to the list of targets, the list of values, and the list of calldatas. In this case it’s a single action, so it’s simple: +Now we are ready to call the propose function of the Governor. Note that we don’t pass in one array of actions, but instead three arrays corresponding to the list of targets, the list of values, and the list of calldatas. In this case it’s a single action, so it’s simple: ```javascript await governor.propose( @@ -305,7 +306,7 @@ await governor.queue( ); ``` -This will cause the governor to interact with the timelock contract and queue the actions for execution after the required delay. +This will cause the Governor to interact with the timelock contract and queue the actions for execution after the required delay. After enough time has passed (according to the timelock parameters), the proposal can be executed. If there was no timelock to begin with, this step can be ran immediately after the proposal succeeds. @@ -319,3 +320,107 @@ await governor.execute( ``` Executing the proposal will transfer the ERC20 tokens to the chosen recipient. To wrap up: we set up a system where a treasury is controlled by the collective decision of the token holders of a project, and all actions are executed via proposals enforced by on-chain votes. + +== Timestamp based governance + +=== Motivation + +It is sometimes difficult to deal with durations expressed in number of blocks because of inconsistent or unpredictable time between blocks. This is particularly true of some L2 networks where blocks are produced based on blockchain usage. Using number of blocks can also lead to the governance rules being affected by network upgrades that modify the expected time between blocks. + +The difficulty of replacing block numbers with timestamps is that the Governor and the token must both use the same format when querying past votes. If a token is designed around block numbers, it is not possible for a Governor to reliably do timestamp based lookups. + +Therefore, designing a timestamp based voting system starts with the token. + +=== Token + +Since v4.9, all voting contracts (including xref:api:token/ERC20.adoc#ERC20Votes[`ERC20Votes`] and xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`]) rely on xref:api:interfaces.adoc#IERC6372[IERC6372] for clock management. In order to change from operating with block numbers to operating with timestamps, all that is required is to override the `clock()` and `CLOCK_MODE()` functions. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "github.com/openzeppelin/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import "github.com/openzeppelin/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import "github.com/openzeppelin/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Votes.sol"; + +contract MyToken is ERC20, ERC20Permit, ERC20Votes { + constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {} + + // Overrides IERC6372 functions to make the token & governor timestamp-based + + function clock() public view override returns (uint48) { + return uint48(block.timestamp); + } + + function CLOCK_MODE() public pure override returns (string memory) { + return "mode=timestamp"; + } + + // The functions below are overrides required by Solidity. + + function _afterTokenTransfer(address from, address to, uint256 amount) + internal + override(ERC20, ERC20Votes) + { + super._afterTokenTransfer(from, to, amount); + } + + function _mint(address to, uint256 amount) + internal + override(ERC20, ERC20Votes) + { + super._mint(to, amount); + } + + function _burn(address account, uint256 amount) + internal + override(ERC20, ERC20Votes) + { + super._burn(account, amount); + } +} +``` + +=== Governor + +The Governor will automatically detect the clock mode used by the token and adapt to it. There is no need to override anything in the Governor contract. However, the clock mode does affect how some values are interpreted. It is therefore necessary to set the `votingDelay()` and `votingPeriod()` accordingly. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "@openzeppelin/contracts/governance/Governor.sol"; +import "@openzeppelin/contracts/governance/compatibility/GovernorCompatibilityBravo.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol"; + +contract MyGovernor is Governor, GovernorCompatibilityBravo, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl { + constructor(IVotes _token, TimelockController _timelock) + Governor("MyGovernor") + GovernorVotes(_token) + GovernorVotesQuorumFraction(4) + GovernorTimelockControl(_timelock) + {} + + function votingDelay() public pure virtual override returns (uint256) { + return 1 days; + } + + function votingPeriod() public pure virtual override returns (uint256) { + return 1 weeks; + } + + function proposalThreshold() public pure virtual override returns (uint256) { + return 0; + } + + // ... +} +``` + +=== Disclaimer + +Timestamp based voting is a recent feature that was formalized in EIP-6372 and EIP-5805, and introduced in v4.9. At the time this feature is released, governance tooling such as https://www.tally.xyz[Tally] does not support it yet. While support for timestamps should come soon, users can expect invalid reporting of deadlines & durations. This invalid reporting by offchain tools does not affect the onchain security and functionality of the governance contract. + +Governors with timestamp support (v4.9 and above) are compatible with old tokens (before v4.9) and will operate in "block number" mode (which is the mode all old tokens operate on). On the other hand, old Governor instances (before v4.9) are not compatible with new tokens operating using timestamps. If you update your token code to use timestamps, make sure to also update your Governor code. From d5581531deabf016552705eac9234a5d4c2935b1 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 24 Feb 2023 15:49:10 +0100 Subject: [PATCH 054/197] Add a "fees" section to the ERC4626 guide (#4054) Co-authored-by: Francisco Giordano --- contracts/mocks/docs/ERC4626Fees.sol | 87 ++++++++++++++ contracts/mocks/token/ERC4646FeesMock.sol | 40 ++++++ docs/modules/ROOT/pages/erc4626.adoc | 22 ++++ scripts/prepare-docs.sh | 8 ++ test/token/ERC20/extensions/ERC4626.test.js | 127 ++++++++++++++++++++ 5 files changed, 284 insertions(+) create mode 100644 contracts/mocks/docs/ERC4626Fees.sol create mode 100644 contracts/mocks/token/ERC4646FeesMock.sol diff --git a/contracts/mocks/docs/ERC4626Fees.sol b/contracts/mocks/docs/ERC4626Fees.sol new file mode 100644 index 000000000..8ff162953 --- /dev/null +++ b/contracts/mocks/docs/ERC4626Fees.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../../token/ERC20/extensions/ERC4626.sol"; + +abstract contract ERC4626Fees is ERC4626 { + using Math for uint256; + + /** @dev See {IERC4626-previewDeposit}. */ + function previewDeposit(uint256 assets) public view virtual override returns (uint256) { + uint256 fee = _feeOnTotal(assets, _entryFeeBasePoint()); + return super.previewDeposit(assets - fee); + } + + /** @dev See {IERC4626-previewMint}. */ + function previewMint(uint256 shares) public view virtual override returns (uint256) { + uint256 assets = super.previewMint(shares); + return assets + _feeOnRaw(assets, _entryFeeBasePoint()); + } + + /** @dev See {IERC4626-previewWithdraw}. */ + function previewWithdraw(uint256 assets) public view virtual override returns (uint256) { + uint256 fee = _feeOnRaw(assets, _exitFeeBasePoint()); + return super.previewWithdraw(assets + fee); + } + + /** @dev See {IERC4626-previewRedeem}. */ + function previewRedeem(uint256 shares) public view virtual override returns (uint256) { + uint256 assets = super.previewRedeem(shares); + return assets - _feeOnTotal(assets, _exitFeeBasePoint()); + } + + /** @dev See {IERC4626-_deposit}. */ + function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override { + uint256 fee = _feeOnTotal(assets, _entryFeeBasePoint()); + address recipient = _entryFeeRecipient(); + + super._deposit(caller, receiver, assets, shares); + + if (fee > 0 && recipient != address(this)) { + SafeERC20.safeTransfer(IERC20(asset()), recipient, fee); + } + } + + /** @dev See {IERC4626-_deposit}. */ + function _withdraw( + address caller, + address receiver, + address owner, + uint256 assets, + uint256 shares + ) internal virtual override { + uint256 fee = _feeOnRaw(assets, _exitFeeBasePoint()); + address recipient = _exitFeeRecipient(); + + super._withdraw(caller, receiver, owner, assets, shares); + + if (fee > 0 && recipient != address(this)) { + SafeERC20.safeTransfer(IERC20(asset()), recipient, fee); + } + } + + function _entryFeeBasePoint() internal view virtual returns (uint256) { + return 0; + } + + function _entryFeeRecipient() internal view virtual returns (address) { + return address(0); + } + + function _exitFeeBasePoint() internal view virtual returns (uint256) { + return 0; + } + + function _exitFeeRecipient() internal view virtual returns (address) { + return address(0); + } + + function _feeOnRaw(uint256 assets, uint256 feeBasePoint) private pure returns (uint256) { + return assets.mulDiv(feeBasePoint, 1e5, Math.Rounding.Up); + } + + function _feeOnTotal(uint256 assets, uint256 feeBasePoint) private pure returns (uint256) { + return assets.mulDiv(feeBasePoint, feeBasePoint + 1e5, Math.Rounding.Up); + } +} diff --git a/contracts/mocks/token/ERC4646FeesMock.sol b/contracts/mocks/token/ERC4646FeesMock.sol new file mode 100644 index 000000000..cd8f41012 --- /dev/null +++ b/contracts/mocks/token/ERC4646FeesMock.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../docs/ERC4626Fees.sol"; + +abstract contract ERC4626FeesMock is ERC4626Fees { + uint256 private immutable _entryFeeBasePointValue; + address private immutable _entryFeeRecipientValue; + uint256 private immutable _exitFeeBasePointValue; + address private immutable _exitFeeRecipientValue; + + constructor( + uint256 entryFeeBasePoint, + address entryFeeRecipient, + uint256 exitFeeBasePoint, + address exitFeeRecipient + ) { + _entryFeeBasePointValue = entryFeeBasePoint; + _entryFeeRecipientValue = entryFeeRecipient; + _exitFeeBasePointValue = exitFeeBasePoint; + _exitFeeRecipientValue = exitFeeRecipient; + } + + function _entryFeeBasePoint() internal view virtual override returns (uint256) { + return _entryFeeBasePointValue; + } + + function _entryFeeRecipient() internal view virtual override returns (address) { + return _entryFeeRecipientValue; + } + + function _exitFeeBasePoint() internal view virtual override returns (uint256) { + return _exitFeeBasePointValue; + } + + function _exitFeeRecipient() internal view virtual override returns (address) { + return _exitFeeRecipientValue; + } +} diff --git a/docs/modules/ROOT/pages/erc4626.adoc b/docs/modules/ROOT/pages/erc4626.adoc index 11c13ca95..00d46a4d0 100644 --- a/docs/modules/ROOT/pages/erc4626.adoc +++ b/docs/modules/ROOT/pages/erc4626.adoc @@ -191,3 +191,25 @@ stem:[\delta = 3], stem:[a_0 = 100], stem:[a_1 = 10^5] image::erc4626-attack-6.png[Inflation attack without offset=6] stem:[\delta = 6], stem:[a_0 = 1], stem:[a_1 = 10^5] + + +[[fees]] +== Custom behavior: Adding fees to the vault + +In an ERC4626 vaults, fees can be captured during the deposit/mint and/or during the withdraw/redeem steps. In both cases it is essential to remain compliant with the ERC4626 requirements with regard to the preview functions. + +For example, if calling `deposit(100, receiver)`, the caller should deposit exactly 100 underlying tokens, including fees, and the receiver should receive a number of shares that matches the value returned by `previewDeposit(100)`. Similarly, `previewMint` should account for the fees that the user will have to pay on top of share's cost. + +As for the `Deposit` event, while this is less clear in the EIP spec itself, there seems to be consensus that it should include the number of assets paid for by the user, including the fees. + +On the other hand, when withdrawing assets, the number given by the user should correspond to what he receives. Any fees should be added to the quote (in shares) performed by `previewWithdraw`. + +The `Withdraw` event should include the number of shares the user burns (including fees) and the number of assets the user actually receives (after fees are deducted). + +The consequence of this design is that both the `Deposit` and `Withdraw` events will describe two exchange rates. The spread between the "Buy-in" and the "Exit" prices correspond to the fees taken by the vault. + +The following example describes how fees proportional to the deposited/withdrawn amount can be implemented: + +```solidity +include::api:example$ERC4626Fees.sol[] +``` diff --git a/scripts/prepare-docs.sh b/scripts/prepare-docs.sh index 4fc0c957a..bb9c5d0ad 100755 --- a/scripts/prepare-docs.sh +++ b/scripts/prepare-docs.sh @@ -12,4 +12,12 @@ rm -rf "$OUTDIR" hardhat docgen +# copy examples and adjust imports +examples_dir="docs/modules/api/examples" +mkdir -p "$examples_dir" +for f in contracts/mocks/docs/*.sol; do + name="$(basename "$f")" + sed -e '/^import/s|\.\./\.\./|@openzeppelin/contracts/|' "$f" > "docs/modules/api/examples/$name" +done + node scripts/gen-nav.js "$OUTDIR" > "$OUTDIR/../nav.adoc" diff --git a/test/token/ERC20/extensions/ERC4626.test.js b/test/token/ERC20/extensions/ERC4626.test.js index 2f7b99d38..c9e5a4098 100644 --- a/test/token/ERC20/extensions/ERC4626.test.js +++ b/test/token/ERC20/extensions/ERC4626.test.js @@ -4,6 +4,7 @@ const { expect } = require('chai'); const ERC20Decimals = artifacts.require('$ERC20DecimalsMock'); const ERC4626 = artifacts.require('$ERC4626'); const ERC4626OffsetMock = artifacts.require('$ERC4626OffsetMock'); +const ERC4626FeesMock = artifacts.require('$ERC4626FeesMock'); contract('ERC4626', function (accounts) { const [holder, recipient, spender, other, user1, user2] = accounts; @@ -489,6 +490,132 @@ contract('ERC4626', function (accounts) { }); } + describe('ERC4626Fees', function () { + const feeBasePoint = web3.utils.toBN(5e3); + const amountWithoutFees = web3.utils.toBN(10000); + const fees = amountWithoutFees.mul(feeBasePoint).divn(1e5); + const amountWithFees = amountWithoutFees.add(fees); + + describe('input fees', function () { + beforeEach(async function () { + this.token = await ERC20Decimals.new(name, symbol, 18); + this.vault = await ERC4626FeesMock.new( + name + ' Vault', + symbol + 'V', + this.token.address, + feeBasePoint, + other, + 0, + constants.ZERO_ADDRESS, + ); + + await this.token.$_mint(holder, constants.MAX_INT256); + await this.token.approve(this.vault.address, constants.MAX_INT256, { from: holder }); + }); + + it('deposit', async function () { + expect(await this.vault.previewDeposit(amountWithFees)).to.be.bignumber.equal(amountWithoutFees); + ({ tx: this.tx } = await this.vault.deposit(amountWithFees, recipient, { from: holder })); + }); + + it('mint', async function () { + expect(await this.vault.previewMint(amountWithoutFees)).to.be.bignumber.equal(amountWithFees); + ({ tx: this.tx } = await this.vault.mint(amountWithoutFees, recipient, { from: holder })); + }); + + afterEach(async function () { + // get total + await expectEvent.inTransaction(this.tx, this.token, 'Transfer', { + from: holder, + to: this.vault.address, + value: amountWithFees, + }); + + // redirect fees + await expectEvent.inTransaction(this.tx, this.token, 'Transfer', { + from: this.vault.address, + to: other, + value: fees, + }); + + // mint shares + await expectEvent.inTransaction(this.tx, this.vault, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: recipient, + value: amountWithoutFees, + }); + + // deposit event + await expectEvent.inTransaction(this.tx, this.vault, 'Deposit', { + sender: holder, + owner: recipient, + assets: amountWithFees, + shares: amountWithoutFees, + }); + }); + }); + + describe('output fees', function () { + beforeEach(async function () { + this.token = await ERC20Decimals.new(name, symbol, 18); + this.vault = await ERC4626FeesMock.new( + name + ' Vault', + symbol + 'V', + this.token.address, + 0, + constants.ZERO_ADDRESS, + 5e3, // 5% + other, + ); + + await this.token.$_mint(this.vault.address, constants.MAX_INT256); + await this.vault.$_mint(holder, constants.MAX_INT256); + }); + + it('redeem', async function () { + expect(await this.vault.previewRedeem(amountWithFees)).to.be.bignumber.equal(amountWithoutFees); + ({ tx: this.tx } = await this.vault.redeem(amountWithFees, recipient, holder, { from: holder })); + }); + + it('withdraw', async function () { + expect(await this.vault.previewWithdraw(amountWithoutFees)).to.be.bignumber.equal(amountWithFees); + ({ tx: this.tx } = await this.vault.withdraw(amountWithoutFees, recipient, holder, { from: holder })); + }); + + afterEach(async function () { + // withdraw principal + await expectEvent.inTransaction(this.tx, this.token, 'Transfer', { + from: this.vault.address, + to: recipient, + value: amountWithoutFees, + }); + + // redirect fees + await expectEvent.inTransaction(this.tx, this.token, 'Transfer', { + from: this.vault.address, + to: other, + value: fees, + }); + + // mint shares + await expectEvent.inTransaction(this.tx, this.vault, 'Transfer', { + from: holder, + to: constants.ZERO_ADDRESS, + value: amountWithFees, + }); + + // withdraw event + await expectEvent.inTransaction(this.tx, this.vault, 'Withdraw', { + sender: holder, + receiver: recipient, + owner: holder, + assets: amountWithoutFees, + shares: amountWithFees, + }); + }); + }); + }); + /// Scenario inspired by solmate ERC4626 tests: /// https://github.com/transmissions11/solmate/blob/main/src/test/ERC4626.t.sol it('multiple mint, deposit, redeem & withdrawal', async function () { From b4d765b130d7306fd496a4c449b6eae403173be8 Mon Sep 17 00:00:00 2001 From: Harshit sharma <79695575+HarshitSharma007@users.noreply.github.com> Date: Fri, 24 Feb 2023 20:35:46 +0530 Subject: [PATCH 055/197] Allow return data length >= 32 in SignatureChecker (#4038) Co-authored-by: Francisco Giordano --- .changeset/warm-masks-obey.md | 5 +++++ contracts/utils/cryptography/SignatureChecker.sol | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/warm-masks-obey.md diff --git a/.changeset/warm-masks-obey.md b/.changeset/warm-masks-obey.md new file mode 100644 index 000000000..3bcfa9bdd --- /dev/null +++ b/.changeset/warm-masks-obey.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`SignatureChecker`: Allow return data length greater than 32 from EIP-1271 signers. diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index c3a724dd0..b81cf40be 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -44,7 +44,7 @@ library SignatureChecker { abi.encodeWithSelector(IERC1271.isValidSignature.selector, hash, signature) ); return (success && - result.length == 32 && + result.length >= 32 && abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)); } } From 8a43ebac28a3e3ae6870387a473582064473cef1 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 24 Feb 2023 17:48:55 +0100 Subject: [PATCH 056/197] Document "duplicate" proposal storing in GovernorCompatibilityBravo (#4073) Co-authored-by: Francisco --- .../compatibility/GovernorCompatibilityBravo.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol index 39c895bf0..25f404403 100644 --- a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol @@ -55,6 +55,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp bytes[] memory calldatas, string memory description ) public virtual override(IGovernor, Governor) returns (uint256) { + // Stores the proposal details (if not already present) and executes the propose logic from the core. _storeProposal(_msgSender(), targets, values, new string[](calldatas.length), calldatas, description); return super.propose(targets, values, calldatas, description); } @@ -69,6 +70,10 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp bytes[] memory calldatas, string memory description ) public virtual override returns (uint256) { + // 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 + // is added their is also executed when calling this alternative interface. _storeProposal(_msgSender(), targets, values, signatures, calldatas, description); return propose(targets, values, _encodeCalldata(signatures, calldatas), description); } @@ -174,7 +179,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp } /** - * @dev Store proposal metadata for later lookup + * @dev Store proposal metadata (if not already present) for later lookup. */ function _storeProposal( address proposer, From eb38c059d81ecf9296ef7526233ed962aba070eb Mon Sep 17 00:00:00 2001 From: Pascal Marco Caversaccio Date: Fri, 24 Feb 2023 19:34:03 +0100 Subject: [PATCH 057/197] Add comment on unchecked arithmetic (division by zero) in `Math.sol` (#4050) Co-authored-by: Francisco --- contracts/utils/math/Math.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index 8400d0669..f8e7ca0a9 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -67,6 +67,9 @@ library Math { // Handle non-overflow cases, 256 by 256 division. if (prod1 == 0) { + // Solidity will revert if denominator == 0, unlike the div opcode on its own. + // The surrounding unchecked block does not change this fact. + // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic. return prod0 / denominator; } From 2c6ef8c875c8b307d3e715cb918acc5c2c42714e Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 24 Feb 2023 20:14:49 +0100 Subject: [PATCH 058/197] Fix ERC1363 interfaceId (#4074) --- contracts/interfaces/IERC1363.sol | 11 +++-------- contracts/interfaces/README.adoc | 4 ++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/contracts/interfaces/IERC1363.sol b/contracts/interfaces/IERC1363.sol index 1a8dc79f4..1517197fe 100644 --- a/contracts/interfaces/IERC1363.sol +++ b/contracts/interfaces/IERC1363.sol @@ -8,17 +8,12 @@ import "./IERC165.sol"; interface IERC1363 is IERC165, IERC20 { /* - * Note: the ERC-165 identifier for this interface is 0x4bbee2df. - * 0x4bbee2df === + * Note: the ERC-165 identifier for this interface is 0xb0202a11. + * 0xb0202a11 === * bytes4(keccak256('transferAndCall(address,uint256)')) ^ * bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^ * bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^ - * bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) - */ - - /* - * Note: the ERC-165 identifier for this interface is 0xfb9ec8ce. - * 0xfb9ec8ce === + * bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^ * bytes4(keccak256('approveAndCall(address,uint256)')) ^ * bytes4(keccak256('approveAndCall(address,uint256,bytes)')) */ diff --git a/contracts/interfaces/README.adoc b/contracts/interfaces/README.adoc index 8748da8a4..710c078e8 100644 --- a/contracts/interfaces/README.adoc +++ b/contracts/interfaces/README.adoc @@ -22,6 +22,8 @@ are useful to interact with third party contracts that implement them. - {IERC1155MetadataURI} - {IERC1271} - {IERC1363} +- {IERC1363Receiver} +- {IERC1363Spender} - {IERC1820Implementer} - {IERC1820Registry} - {IERC1822Proxiable} @@ -44,6 +46,8 @@ are useful to interact with third party contracts that implement them. {{IERC1363Receiver}} +{{IERC1363Spender}} + {{IERC1820Implementer}} {{IERC1820Registry}} From 0fbd039554c05f2872639850d0e0127587551946 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Feb 2023 17:14:21 -0300 Subject: [PATCH 059/197] Update lockfile (#4018) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 7545 ++++++++++++++++----------------------------- 1 file changed, 2719 insertions(+), 4826 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f808cf30..87fd3a795 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,81 +86,10 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/runtime": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", - "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", + "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.13.11" @@ -190,24 +119,6 @@ "semver": "^5.4.1" } }, - "node_modules/@changesets/apply-release-plan/node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@changesets/apply-release-plan/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@changesets/apply-release-plan/node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -304,83 +215,6 @@ "changeset": "bin.js" } }, - "node_modules/@changesets/cli/node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@changesets/cli/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/cli/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/cli/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@changesets/cli/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@changesets/cli/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@changesets/cli/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/cli/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@changesets/cli/node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -390,18 +224,6 @@ "semver": "bin/semver" } }, - "node_modules/@changesets/cli/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@changesets/config": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@changesets/config/-/config-2.3.0.tgz", @@ -439,65 +261,6 @@ "semver": "^5.4.1" } }, - "node_modules/@changesets/get-dependents-graph/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/get-dependents-graph/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/get-dependents-graph/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@changesets/get-dependents-graph/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@changesets/get-dependents-graph/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@changesets/get-dependents-graph/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/@changesets/get-dependents-graph/node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -507,18 +270,6 @@ "semver": "bin/semver" } }, - "node_modules/@changesets/get-dependents-graph/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@changesets/get-github-info": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.5.2.tgz", @@ -574,77 +325,6 @@ "chalk": "^2.1.0" } }, - "node_modules/@changesets/logger/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/logger/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/logger/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@changesets/logger/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@changesets/logger/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@changesets/logger/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/logger/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@changesets/parse": { "version": "0.3.16", "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.3.16.tgz", @@ -684,77 +364,6 @@ "p-filter": "^2.1.0" } }, - "node_modules/@changesets/read/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/read/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/read/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@changesets/read/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@changesets/read/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@changesets/read/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/read/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@changesets/types": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/@changesets/types/-/types-5.2.1.tgz", @@ -1730,12 +1339,6 @@ "fs-extra": "^8.1.0" } }, - "node_modules/@manypkg/find-root/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - }, "node_modules/@manypkg/find-root/node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -1784,26 +1387,6 @@ "node": ">=6 <7 || >=8" } }, - "node_modules/@manypkg/get-packages/node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@metamask/eth-sig-util": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz", @@ -1845,9 +1428,9 @@ } }, "node_modules/@noble/hashes": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", - "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", + "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", "dev": true, "funding": [ { @@ -1857,9 +1440,9 @@ ] }, "node_modules/@noble/secp256k1": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.6.3.tgz", - "integrity": "sha512-T04e4iTurVy7I8Sw4+c5OSN9/RkPlo1uKxAomtxQNLq8j1uPAqnsqG1bqvY3Jv7c13gyr6dui0zmh/I3+f/JaQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", "dev": true, "funding": [ { @@ -2087,9 +1670,9 @@ } }, "node_modules/@nomicfoundation/hardhat-network-helpers": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.7.tgz", - "integrity": "sha512-X+3mNvn8B7BY5hpIaLO+TrfzWq12bpux+ajGGdmdcfC78NXmYmOZkAtiz1QZx1YIZGMS1LaXzPXyBExxKFpCaw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.8.tgz", + "integrity": "sha512-MNqQbzUJZnCMIYvlniC3U+kcavz/PhhQSsY90tbEtUyMj/IQqsLwIRZa4ctjABh3Bz0KCh9OXUZ7Yk/d9hr45Q==", "dev": true, "dependencies": { "ethereumjs-util": "^7.1.4" @@ -2379,6 +1962,73 @@ "oz-docs": "oz-docs.js" } }, + "node_modules/@openzeppelin/docs-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@openzeppelin/docs-utils/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@openzeppelin/docs-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@openzeppelin/docs-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@openzeppelin/docs-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@openzeppelin/docs-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@openzeppelin/test-helpers": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/@openzeppelin/test-helpers/-/test-helpers-0.5.16.tgz", @@ -2397,6 +2047,15 @@ "web3-utils": "^1.2.5" } }, + "node_modules/@openzeppelin/test-helpers/node_modules/ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@openzeppelin/test-helpers/node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -2407,9 +2066,9 @@ } }, "node_modules/@openzeppelin/upgrades-core": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.20.6.tgz", - "integrity": "sha512-KWdtlahm+iunlAlzLsdpBueanwEx0LLPfAkDL1p0C4SPjMiUqHHFlyGtmmWwdiqDpJ//605vfwkd5RqfnFrHSg==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.24.0.tgz", + "integrity": "sha512-lXf1tUrCZ3Q/YmWhw0cuSSOHMp0OAsmeOg1fhSGEM6nQQ6cIVlFvq2pCV5hZMb7xkOm5pmmzV8JW1W3kfW6Lfw==", "dev": true, "dependencies": { "cbor": "^8.0.0", @@ -2421,16 +2080,19 @@ "solidity-ast": "^0.4.15" } }, - "node_modules/@openzeppelin/upgrades-core/node_modules/cbor": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", - "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", + "node_modules/@openzeppelin/upgrades-core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { - "nofilter": "^3.1.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=12.19" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/@openzeppelin/upgrades-core/node_modules/chalk": { @@ -2449,13 +2111,43 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@openzeppelin/upgrades-core/node_modules/nofilter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", - "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "node_modules/@openzeppelin/upgrades-core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { - "node": ">=12.19" + "node": ">=8" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/@scure/base": { @@ -2471,9 +2163,9 @@ ] }, "node_modules/@scure/bip32": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.0.tgz", - "integrity": "sha512-ftTW3kKX54YXLCxH6BB7oEEoJfoE2pIgw7MINKAs5PsS6nqKPuKk1haTF/EuHmYqG330t5GSrdmtRuHaY1a62Q==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz", + "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==", "dev": true, "funding": [ { @@ -2482,15 +2174,15 @@ } ], "dependencies": { - "@noble/hashes": "~1.1.1", - "@noble/secp256k1": "~1.6.0", + "@noble/hashes": "~1.2.0", + "@noble/secp256k1": "~1.7.0", "@scure/base": "~1.1.0" } }, "node_modules/@scure/bip39": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz", - "integrity": "sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz", + "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==", "dev": true, "funding": [ { @@ -2499,7 +2191,7 @@ } ], "dependencies": { - "@noble/hashes": "~1.1.1", + "@noble/hashes": "~1.2.0", "@scure/base": "~1.1.0" } }, @@ -2639,14 +2331,14 @@ } }, "node_modules/@truffle/abi-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@truffle/abi-utils/-/abi-utils-0.3.6.tgz", - "integrity": "sha512-61aTH2QmwVA1INaPMufRHTsS6jsEhS+GCkuCDdvBDmwctSnCKGDOr185BGt65QrpMRxYmIoH6WFBSNMYxW9GRw==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@truffle/abi-utils/-/abi-utils-0.3.8.tgz", + "integrity": "sha512-RevMtlEuQw9cE2VWzQuvLDSYPCVSZQQUOsaK8ArcwxXct6ugiJCDC7tNouhHzP1Y4ccdZtG2y/XPHQJdzgYdZQ==", "dev": true, "dependencies": { "change-case": "3.0.2", "fast-check": "3.1.1", - "web3-utils": "1.8.1" + "web3-utils": "1.8.2" } }, "node_modules/@truffle/blockchain-utils": { @@ -2656,13 +2348,13 @@ "dev": true }, "node_modules/@truffle/codec": { - "version": "0.14.11", - "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.14.11.tgz", - "integrity": "sha512-NgfMNYemgMXqoEcJA5ZsEhxChCwq33rSxtNxlececEH/1Nf0r+ryfrfmLlyPmv8f3jorVf1GWa0zI0AedGCGYQ==", + "version": "0.14.15", + "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.14.15.tgz", + "integrity": "sha512-cooWy8blmvYQQRBKgzWJnDUS6mZE9cvnmpVN15jU6TseAQkBtmfYfCH12QCKEWMq2+R4yQICP54WNCYg78g72g==", "dev": true, "dependencies": { - "@truffle/abi-utils": "^0.3.6", - "@truffle/compile-common": "^0.9.1", + "@truffle/abi-utils": "^0.3.8", + "@truffle/compile-common": "^0.9.3", "big.js": "^6.0.3", "bn.js": "^5.1.3", "cbor": "^5.2.0", @@ -2670,7 +2362,16 @@ "lodash": "^4.17.21", "semver": "7.3.7", "utf8": "^3.0.0", - "web3-utils": "1.8.1" + "web3-utils": "1.8.2" + } + }, + "node_modules/@truffle/codec/node_modules/bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "dev": true, + "engines": { + "node": "*" } }, "node_modules/@truffle/codec/node_modules/bn.js": { @@ -2679,6 +2380,19 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", "dev": true }, + "node_modules/@truffle/codec/node_modules/cbor": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-5.2.0.tgz", + "integrity": "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A==", + "dev": true, + "dependencies": { + "bignumber.js": "^9.0.1", + "nofilter": "^1.0.4" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@truffle/codec/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -2691,6 +2405,15 @@ "node": ">=10" } }, + "node_modules/@truffle/codec/node_modules/nofilter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz", + "integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@truffle/codec/node_modules/semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -2713,71 +2436,71 @@ "dev": true }, "node_modules/@truffle/compile-common": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@truffle/compile-common/-/compile-common-0.9.1.tgz", - "integrity": "sha512-mhdkX6ExZImHSBO3jGm6aAn8NpVtMTdjq50jRXY/O59/ZNC0J9WpRapxrAKUVNc+XydMdBlfeEpXoqTJg7cbXw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@truffle/compile-common/-/compile-common-0.9.3.tgz", + "integrity": "sha512-9S86H5DRC0zEj164KeClP/6jVt1M/nRd7St89h6QbuIU0JjpqSz1SXpkvqhbFoV9hhW+4ZGh0NysjrdPlk7gFw==", "dev": true, "dependencies": { - "@truffle/error": "^0.1.1", + "@truffle/error": "^0.2.0", "colors": "1.4.0" } }, + "node_modules/@truffle/compile-common/node_modules/@truffle/error": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@truffle/error/-/error-0.2.0.tgz", + "integrity": "sha512-Fe0/z4WWb7IP2gBnv3l6zqP87Y0kSMs7oiSLakKJq17q3GUunrHSdioKuNspdggxkXIBhEQLhi8C+LJdwmHKWQ==", + "dev": true + }, "node_modules/@truffle/contract": { - "version": "4.6.10", - "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.10.tgz", - "integrity": "sha512-69IZSXeQKRP3EutILqe+vLY5A5gUpeXUiZhm/Fy/qHHkP238vMjtOkTZGkY6bonYqmgk+vDY7KSYSYKzDNPdCA==", + "version": "4.6.15", + "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.15.tgz", + "integrity": "sha512-3V+50z94XRh+I3axz6Zrot1wlMhRiG2O9XjR20LwR5YBnzHV7YFfOs8x6pluXVLQyxcVJBISJyqxvGxpkecrZg==", "dev": true, "dependencies": { "@ensdomains/ensjs": "^2.1.0", "@truffle/blockchain-utils": "^0.1.6", - "@truffle/contract-schema": "^3.4.11", - "@truffle/debug-utils": "^6.0.42", - "@truffle/error": "^0.1.1", - "@truffle/interface-adapter": "^0.5.26", + "@truffle/contract-schema": "^3.4.12", + "@truffle/debug-utils": "^6.0.46", + "@truffle/error": "^0.2.0", + "@truffle/interface-adapter": "^0.5.29", "bignumber.js": "^7.2.1", "debug": "^4.3.1", "ethers": "^4.0.32", - "web3": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-promievent": "1.8.1", - "web3-eth-abi": "1.8.1", - "web3-utils": "1.8.1" + "web3": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-utils": "1.8.2" } }, "node_modules/@truffle/contract-schema": { - "version": "3.4.11", - "resolved": "https://registry.npmjs.org/@truffle/contract-schema/-/contract-schema-3.4.11.tgz", - "integrity": "sha512-wReyVZUPyU9Zy5PSCugBLG1nnruBmRAJ/gmoirQiJ9N2n+s1iGBTY49tkDqFMz3XUUE0kplfdb9YKZJlLkTWzQ==", + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/@truffle/contract-schema/-/contract-schema-3.4.12.tgz", + "integrity": "sha512-XpMMps/bqHwiRuCyLiEEGWEAvGGzGj4u1X1+lzxrtIsrwbQhSZcdgEbXl9vGxOOJWOup3HXRCIsjlao27kS4OA==", "dev": true, "dependencies": { "ajv": "^6.10.0", "debug": "^4.3.1" } }, + "node_modules/@truffle/contract/node_modules/@truffle/error": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@truffle/error/-/error-0.2.0.tgz", + "integrity": "sha512-Fe0/z4WWb7IP2gBnv3l6zqP87Y0kSMs7oiSLakKJq17q3GUunrHSdioKuNspdggxkXIBhEQLhi8C+LJdwmHKWQ==", + "dev": true + }, "node_modules/@truffle/debug-utils": { - "version": "6.0.42", - "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.42.tgz", - "integrity": "sha512-9v70tj+My0Z2UZJ9OsuUlfo4Dt2AJqAQa/YWtGe28H8zsi+o9Dca0RsKWecuprdllgzrEs7ad8QUtSINhwjIlg==", + "version": "6.0.46", + "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.46.tgz", + "integrity": "sha512-MWko3c1M0NI8IT0yNh8Dh2xoDgR2R1apP8hc/9VsR/4Q7P6OE/578WRpZnvo2HWXYBiJ2PrWBDtMYBHntImrrA==", "dev": true, "dependencies": { - "@truffle/codec": "^0.14.11", + "@truffle/codec": "^0.14.15", "@trufflesuite/chromafi": "^3.0.0", "bn.js": "^5.1.3", "chalk": "^2.4.2", "debug": "^4.3.1", - "highlightjs-solidity": "^2.0.5" - } - }, - "node_modules/@truffle/debug-utils/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" + "highlightjs-solidity": "^2.0.6" } }, "node_modules/@truffle/debug-utils/node_modules/bn.js": { @@ -2786,65 +2509,6 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", "dev": true }, - "node_modules/@truffle/debug-utils/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@truffle/debug-utils/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@truffle/debug-utils/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@truffle/debug-utils/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@truffle/debug-utils/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@truffle/debug-utils/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@truffle/error": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@truffle/error/-/error-0.1.1.tgz", @@ -2852,14 +2516,14 @@ "dev": true }, "node_modules/@truffle/interface-adapter": { - "version": "0.5.26", - "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.26.tgz", - "integrity": "sha512-fBhoqtT+CT4XKXcOijvw0RIMgyUi3FJg+n5i5PyGBsoRzqbLZd9cZq+oMNjOZPdf3GH68hsOFOaQO5tZH7oZow==", + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.29.tgz", + "integrity": "sha512-6UlJ+f87z7y/dWk9UfbIU+4e80iRsp8h03LEiE5B+PvZbr6cuMjLJUBtBBQZMo3+xrIcS/2u3p5hOxW8OJm8tw==", "dev": true, "dependencies": { "bn.js": "^5.1.3", "ethers": "^4.0.32", - "web3": "1.8.1" + "web3": "1.8.2" } }, "node_modules/@truffle/interface-adapter/node_modules/bn.js": { @@ -2884,73 +2548,11 @@ "strip-indent": "^2.0.0" } }, - "node_modules/@trufflesuite/chromafi/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/@trufflesuite/chromafi/node_modules/detect-indent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", + "integrity": "sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==", "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@trufflesuite/chromafi/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@trufflesuite/chromafi/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@trufflesuite/chromafi/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@trufflesuite/chromafi/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@trufflesuite/chromafi/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@trufflesuite/chromafi/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, "engines": { "node": ">=4" } @@ -3041,21 +2643,6 @@ "ci-info": "^3.1.0" } }, - "node_modules/@types/is-ci/node_modules/ci-info": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.1.tgz", - "integrity": "sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -3084,9 +2671,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", - "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", "dev": true }, "node_modules/@types/normalize-package-data": { @@ -3214,9 +2801,9 @@ } }, "node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -3310,9 +2897,9 @@ } }, "node_modules/ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "engines": { "node": ">=6" @@ -3355,25 +2942,25 @@ } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" + "color-convert": "^1.9.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=4" } }, "node_modules/antlr4": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.7.1.tgz", - "integrity": "sha512-haHyTW7Y9joE5MVs37P2lNYfU2RWBLfcRDD8OWldcdZm5TiCE91B5Xl1oWSwiDUSd4rlExpt2pu1fksYQjRBYQ==", - "dev": true + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.12.0.tgz", + "integrity": "sha512-23iB5IzXJZRZeK9TigzUyrNc9pSmNqAerJRBcNq1ETrmttMWRgaYZzC561IgEO3ygKsDJTYDTozABXa4b/fTQQ==", + "dev": true, + "engines": { + "node": ">=16" + } }, "node_modules/antlr4ts": { "version": "0.5.0-alpha.4", @@ -3488,18 +3075,6 @@ "safer-buffer": "~2.1.0" } }, - "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "dev": true, - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -3525,12 +3100,12 @@ "dev": true }, "node_modules/astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/async": { @@ -3585,9 +3160,9 @@ } }, "node_modules/aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", "dev": true }, "node_modules/balanced-match": { @@ -3738,13 +3313,13 @@ "dev": true }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dev": true, "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -3752,7 +3327,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -3851,68 +3426,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "node_modules/browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "dev": true, - "dependencies": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "node_modules/browserify-rsa/node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - }, - "node_modules/browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "dev": true, - "dependencies": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - } - }, - "node_modules/browserify-sign/node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - }, "node_modules/bs58": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", @@ -4079,39 +3592,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", - "dev": true, - "dependencies": { - "callsites": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/caller-callsite/node_modules/callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", - "dev": true, - "dependencies": { - "caller-callsite": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4166,15 +3646,6 @@ "node": ">=6" } }, - "node_modules/camelcase-keys/node_modules/quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -4191,25 +3662,15 @@ } }, "node_modules/cbor": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-5.2.0.tgz", - "integrity": "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", + "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", "dev": true, "dependencies": { - "bignumber.js": "^9.0.1", - "nofilter": "^1.0.4" + "nofilter": "^3.1.0" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/cbor/node_modules/bignumber.js": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", - "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", - "dev": true, - "engines": { - "node": "*" + "node": ">=12.19" } }, "node_modules/chai": { @@ -4241,16 +3702,17 @@ } }, "node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/change-case": { @@ -4375,10 +3837,19 @@ "dev": true }, "node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } }, "node_modules/cids": { "version": "0.7.5", @@ -4451,18 +3922,6 @@ "node": ">=6" } }, - "node_modules/cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "dev": true, - "dependencies": { - "restore-cursor": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/cli-table3": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", @@ -4479,12 +3938,6 @@ "colors": "^1.1.2" } }, - "node_modules/cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", - "dev": true - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4574,21 +4027,18 @@ } }, "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "color-name": "1.1.3" } }, "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, "node_modules/colors": { @@ -4619,10 +4069,13 @@ "dev": true }, "node_modules/commander": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz", - "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==", - "dev": true + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz", + "integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==", + "dev": true, + "engines": { + "node": ">=14" + } }, "node_modules/compare-versions": { "version": "5.0.3", @@ -4652,9 +4105,9 @@ } }, "node_modules/concat-stream/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "dependencies": { "core-util-is": "~1.0.0", @@ -4715,9 +4168,9 @@ } }, "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "engines": { "node": ">= 0.6" @@ -4758,40 +4211,36 @@ } }, "node_modules/cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.0.0.tgz", + "integrity": "sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ==", "dev": true, "dependencies": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=14" } }, - "node_modules/cosmiconfig/node_modules/import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "dependencies": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" + "argparse": "^2.0.1" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cosmiconfig/node_modules/resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", - "dev": true, - "engines": { - "node": ">=4" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, "node_modules/crc-32": { @@ -4806,16 +4255,6 @@ "node": ">=0.8" } }, - "node_modules/create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, "node_modules/create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -4852,6 +4291,26 @@ "node-fetch": "2.6.7" } }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4890,28 +4349,6 @@ "sha3": "^2.1.1" } }, - "node_modules/crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "dependencies": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - }, - "engines": { - "node": "*" - } - }, "node_modules/crypto-js": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", @@ -5031,15 +4468,12 @@ } }, "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, "node_modules/decamelize-keys": { @@ -5058,15 +4492,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/decamelize-keys/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/decamelize-keys/node_modules/map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", @@ -5152,9 +4577,9 @@ } }, "node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", "dev": true, "dependencies": { "has-property-descriptors": "^1.0.0", @@ -5185,16 +4610,6 @@ "node": ">= 0.8" } }, - "node_modules/des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -5206,12 +4621,12 @@ } }, "node_modules/detect-indent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", - "integrity": "sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", "dev": true, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/detect-port": { @@ -5237,17 +4652,6 @@ "node": ">=0.3.1" } }, - "node_modules/diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, "node_modules/difflib": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", @@ -5430,15 +4834,6 @@ "node": ">=8.6" } }, - "node_modules/enquirer/node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/entities": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", @@ -5470,27 +4865,33 @@ } }, "node_modules/es-abstract": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz", - "integrity": "sha512-7h8MM2EQhsCA7pU/Nv78qOXFpD8Rhqd12gYiSJVkrH9+e8VuA8JlPJK/hQjjlLv6pJvx/z1iRFKzYb0XT/RuAQ==", + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz", + "integrity": "sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==", "dev": true, "dependencies": { + "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", "get-intrinsic": "^1.1.3", "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", "gopd": "^1.0.1", "has": "^1.0.3", "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", + "internal-slot": "^1.0.4", + "is-array-buffer": "^3.0.1", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", "is-weakref": "^1.0.2", "object-inspect": "^1.12.2", "object-keys": "^1.1.1", @@ -5499,25 +4900,9 @@ "safe-regex-test": "^1.0.0", "string.prototype.trimend": "^1.0.6", "string.prototype.trimstart": "^1.0.6", - "unbox-primitive": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract/node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" }, "engines": { "node": ">= 0.4" @@ -5532,6 +4917,20 @@ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", "dev": true }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-shim-unscopables": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", @@ -5616,15 +5015,12 @@ "dev": true }, "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.8.0" } }, "node_modules/escodegen": { @@ -5723,9 +5119,9 @@ } }, "node_modules/eslint": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.31.0.tgz", - "integrity": "sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.34.0.tgz", + "integrity": "sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg==", "dev": true, "dependencies": { "@eslint/eslintrc": "^1.4.1", @@ -5848,6 +5244,21 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/eslint/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5870,6 +5281,36 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5898,6 +5339,15 @@ "node": ">=10.13.0" } }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/eslint/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -5967,6 +5417,18 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/espree": { "version": "9.4.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", @@ -5998,9 +5460,9 @@ } }, "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz", + "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -6113,18 +5575,6 @@ "node": ">=6" } }, - "node_modules/eth-gas-reporter/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/eth-gas-reporter/node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -6134,32 +5584,6 @@ "node": ">=6" } }, - "node_modules/eth-gas-reporter/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eth-gas-reporter/node_modules/chalk/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/eth-gas-reporter/node_modules/chokidar": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", @@ -6192,21 +5616,6 @@ "wrap-ansi": "^5.1.0" } }, - "node_modules/eth-gas-reporter/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/eth-gas-reporter/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "node_modules/eth-gas-reporter/node_modules/debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -6217,15 +5626,6 @@ "ms": "^2.1.1" } }, - "node_modules/eth-gas-reporter/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/eth-gas-reporter/node_modules/diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -6241,25 +5641,16 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, - "node_modules/eth-gas-reporter/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/eth-gas-reporter/node_modules/ethereum-cryptography": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.1.2.tgz", - "integrity": "sha512-XDSJlg4BD+hq9N2FjvotwUET9Tfxpxc3kWGE2AqUG5vcbeunnbImVk3cj6e/xT3phdW21mE8R5IugU4fspQDcQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", "dev": true, "dependencies": { - "@noble/hashes": "1.1.2", - "@noble/secp256k1": "1.6.3", - "@scure/bip32": "1.1.0", - "@scure/bip39": "1.1.0" + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" } }, "node_modules/eth-gas-reporter/node_modules/find-up": { @@ -6318,15 +5709,6 @@ "node": "*" } }, - "node_modules/eth-gas-reporter/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/eth-gas-reporter/node_modules/js-yaml": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", @@ -6438,6 +5820,21 @@ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, + "node_modules/eth-gas-reporter/node_modules/object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eth-gas-reporter/node_modules/p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -6961,6 +6358,30 @@ "node": ">= 0.10.0" } }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/express/node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -6985,6 +6406,21 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -7100,27 +6536,6 @@ "reusify": "^1.0.4" } }, - "node_modules/figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -7418,9 +6833,9 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", "dev": true, "dependencies": { "function-bind": "^1.1.1", @@ -7490,81 +6905,10 @@ "testrpc-sc": "index.js" } }, - "node_modules/ghost-testrpc/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ghost-testrpc/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ghost-testrpc/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/ghost-testrpc/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/ghost-testrpc/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/ghost-testrpc/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/ghost-testrpc/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -7602,9 +6946,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -7662,9 +7006,9 @@ } }, "node_modules/globals": { - "version": "13.19.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz", - "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -7676,43 +7020,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", - "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "dependencies": { - "@types/glob": "^7.1.1", "array-union": "^2.1.0", "dir-glob": "^3.0.1", - "fast-glob": "^3.0.3", - "glob": "^7.1.3", - "ignore": "^5.1.1", - "merge2": "^1.2.3", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", "slash": "^3.0.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/globby/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/gopd": { @@ -7847,9 +7187,9 @@ } }, "node_modules/hardhat": { - "version": "2.12.5", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.12.5.tgz", - "integrity": "sha512-f/t7+hLlhsnQZ6LDXyV+8rHGRZFZY1sgFvgrwr9fBjMdGp1Bu6hHq1KXS4/VFZfZcVdL1DAWWEkryinZhqce+A==", + "version": "2.12.7", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.12.7.tgz", + "integrity": "sha512-voWoN6zn5d8BOEaczSyK/1PyfdeOeI3SbGCFb36yCHTJUt6OIqLb+ZDX30VhA1UsYKzLqG7UnWl3fKJUuANc6A==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.1.2", @@ -7899,7 +7239,7 @@ "source-map-support": "^0.5.13", "stacktrace-parser": "^0.1.10", "tsort": "0.0.1", - "undici": "^5.4.0", + "undici": "^5.14.0", "uuid": "^8.3.2", "ws": "^7.4.6" }, @@ -7923,9 +7263,9 @@ } }, "node_modules/hardhat-exposed": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.1.tgz", - "integrity": "sha512-qyHXdS3NmzrtXF+XL547BMsTAK+IEMW9OOYMH4d362DlPn4L2B2KXKG6OpuJczxYjgMFJ0LunLxNgu6jtRvjRg==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.2.tgz", + "integrity": "sha512-qhi60I2bfjoPZwKgrY7BIpuUiBE7aC/bHN2MzHxPcZdxaeFnjKJ50n59LE7yK3GK2qYzE8DMjzqfnH6SlKPUjw==", "dev": true, "dependencies": { "micromatch": "^4.0.4", @@ -7950,9 +7290,9 @@ } }, "node_modules/hardhat-ignore-warnings": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/hardhat-ignore-warnings/-/hardhat-ignore-warnings-0.2.6.tgz", - "integrity": "sha512-GQgvjprONI8VF8b85+QJ8H9v3L9TCCtQvUx+9QaRL+sCPw1cOZHfhlEz9V6Lq7GNCQMqBORVzNzUzoP/tKAEQQ==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/hardhat-ignore-warnings/-/hardhat-ignore-warnings-0.2.8.tgz", + "integrity": "sha512-vPX94rJyTzYsCOzGIYdOcJgn3iQI6qa+CI9ZZfgDhdXJpda8ljpOT7bdUKAYC4LyoP0Z5fWTmupXoPaQrty0gw==", "dev": true, "dependencies": { "minimatch": "^5.1.0", @@ -7970,9 +7310,9 @@ } }, "node_modules/hardhat-ignore-warnings/node_modules/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -7981,45 +7321,10 @@ "node": ">=10" } }, - "node_modules/hardhat/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hardhat/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hardhat/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/hardhat/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "node_modules/hardhat/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, "node_modules/hardhat/node_modules/commander": { @@ -8028,25 +7333,16 @@ "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", "dev": true }, - "node_modules/hardhat/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/hardhat/node_modules/ethereum-cryptography": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.1.2.tgz", - "integrity": "sha512-XDSJlg4BD+hq9N2FjvotwUET9Tfxpxc3kWGE2AqUG5vcbeunnbImVk3cj6e/xT3phdW21mE8R5IugU4fspQDcQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", "dev": true, "dependencies": { - "@noble/hashes": "1.1.2", - "@noble/secp256k1": "1.6.3", - "@scure/bip32": "1.1.0", - "@scure/bip39": "1.1.0" + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" } }, "node_modules/hardhat/node_modules/find-up": { @@ -8081,15 +7377,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/hardhat/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/hardhat/node_modules/jsonfile": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", @@ -8229,18 +7516,6 @@ "semver": "bin/semver" } }, - "node_modules/hardhat/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -8263,12 +7538,12 @@ } }, "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/has-property-descriptors": { @@ -8283,6 +7558,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", @@ -8369,9 +7656,9 @@ } }, "node_modules/highlightjs-solidity": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-2.0.5.tgz", - "integrity": "sha512-ReXxQSGQkODMUgHcWzVSnfDCDrL2HshOYgw3OlIYmfHeRzUPkfJTUIp95pK4CmbiNG2eMTOmNLpfCz9Zq7Cwmg==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-2.0.6.tgz", + "integrity": "sha512-DySXWfQghjm2l6a/flF+cteroJqD4gI8GSdL4PtvxZSsAHie8m3yVe2JFoRg03ROKT6hp2Lc/BxXkqerNmtQYg==", "dev": true }, "node_modules/hmac-drbg": { @@ -8496,6 +7783,18 @@ "node": ">=10.19.0" } }, + "node_modules/http2-wrapper/node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -8569,9 +7868,9 @@ } }, "node_modules/immutable": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.1.tgz", - "integrity": "sha512-7WYV7Q5BTs0nlQm7tl92rDYYoyELLKHoDMBKhrxEoiV4mrfVdRz8hzPiYOzH7yWjzoVEamxRuAqhxL2PLRwZYQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz", + "integrity": "sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w==", "dev": true }, "node_modules/import-fresh": { @@ -8590,6 +7889,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -8630,138 +7938,13 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, - "node_modules/inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", - "dev": true, - "dependencies": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/inquirer/node_modules/ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/inquirer/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/inquirer/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/inquirer/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/inquirer/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/inquirer/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/internal-slot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz", - "integrity": "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", "dev": true, "dependencies": { - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "has": "^1.0.3", "side-channel": "^1.0.4" }, @@ -8821,6 +8004,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-array-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz", + "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -8914,21 +8111,6 @@ "is-ci": "bin.js" } }, - "node_modules/is-ci/node_modules/ci-info": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.1.tgz", - "integrity": "sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, "node_modules/is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -8944,15 +8126,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -9069,12 +8242,12 @@ } }, "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/is-port-reachable": { @@ -9248,9 +8421,9 @@ "dev": true }, "node_modules/js-sdsl": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", - "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", "dev": true, "funding": { "type": "opencollective", @@ -9294,12 +8467,6 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -9586,6 +8753,18 @@ "node": ">=0.10.0" } }, + "node_modules/load-json-file/node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "dev": true, + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/load-yaml-file": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz", @@ -9601,15 +8780,6 @@ "node": ">=6" } }, - "node_modules/load-yaml-file/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -9652,6 +8822,12 @@ "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", "dev": true }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, "node_modules/lodash.zip": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", @@ -9674,6 +8850,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/log-symbols/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -9690,6 +8881,45 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/loupe": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", @@ -9836,92 +9066,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/meow/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/meow/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/meow/node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/meow/node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/meow/node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/meow/node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -9934,19 +9078,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/meow/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -10009,19 +9140,6 @@ "node": ">=8.6" } }, - "node_modules/miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "bin": { - "miller-rabin": "bin/miller-rabin" - } - }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -10055,15 +9173,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -10116,9 +9225,9 @@ } }, "node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10138,15 +9247,6 @@ "node": ">= 6" } }, - "node_modules/minimist-options/node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/minipass": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", @@ -10167,9 +9267,9 @@ } }, "node_modules/mixme": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.4.tgz", - "integrity": "sha512-3KYa4m4Vlqx98GPdOHghxSdNtTvcP8E0kkaJ5Dlh+h2DRzF7zpuVVcA8B0QpKd11YJeP9QQ7ASkKzOeu195Wzw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.5.tgz", + "integrity": "sha512-/6IupbRx32s7jjEwHcycXikJwFD5UujbVNuJFkeKLYje+92OvtuPniF6JhnFm5JCTDUhS+kYK3W/4BWYQYXz7w==", "dev": true, "engines": { "node": ">= 8.0.0" @@ -10284,6 +9384,18 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mocha/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -10332,6 +9444,15 @@ "node": "*" } }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/mocha/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -10484,6 +9605,15 @@ "node": ">=10" } }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/mock-fs": { "version": "4.14.0", "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-4.14.0.tgz", @@ -10548,12 +9678,6 @@ "buffer": "^5.5.0" } }, - "node_modules/mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", - "dev": true - }, "node_modules/nano-base32": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/nano-base32/-/nano-base32-1.0.1.tgz", @@ -10611,12 +9735,6 @@ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "dev": true }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, "node_modules/no-case": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", @@ -10661,9 +9779,9 @@ } }, "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", "dev": true, "dependencies": { "whatwg-url": "^5.0.0" @@ -10681,9 +9799,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz", - "integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", "dev": true, "bin": { "node-gyp-build": "bin.js", @@ -10704,12 +9822,12 @@ } }, "node_modules/nofilter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz", - "integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", "dev": true, "engines": { - "node": ">=8" + "node": ">=12.19" } }, "node_modules/nopt": { @@ -10826,9 +9944,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10844,18 +9962,21 @@ } }, "node_modules/object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "dev": true, "dependencies": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.getownpropertydescriptors": { @@ -10912,18 +10033,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -11076,19 +10185,6 @@ "node": ">=6" } }, - "node_modules/parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "dev": true, - "dependencies": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, "node_modules/parse-cache-control": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", @@ -11102,16 +10198,21 @@ "dev": true }, "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "dependencies": { + "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/parse5": { @@ -11185,12 +10286,6 @@ "node": ">=0.10.0" } }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", - "dev": true - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -11306,6 +10401,15 @@ "node": ">=8" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/preferred-pm": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.0.3.tgz", @@ -11392,9 +10496,9 @@ } }, "node_modules/prettier": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz", - "integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", + "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", "dev": true, "bin": { "prettier": "bin-prettier.js" @@ -11407,12 +10511,12 @@ } }, "node_modules/prettier-plugin-solidity": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.1.tgz", - "integrity": "sha512-uD24KO26tAHF+zMN2nt1OUzfknzza5AgxjogQQrMLZc7j8xiQrDoNWNeOlfFC0YLTwo12CLD10b9niLyP6AqXg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.2.tgz", + "integrity": "sha512-KC5oNbFJfyBaFiO0kl56J6AXnDmr9tUlBV1iqo864x4KQrKYKaBZvW9jhT2oC0NHoNp7/GoMJNxqL8pp8k7C/g==", "dev": true, "dependencies": { - "@solidity-parser/parser": "^0.14.5", + "@solidity-parser/parser": "^0.15.0", "semver": "^7.3.8", "solidity-comments-extractor": "^0.0.7" }, @@ -11423,39 +10527,15 @@ "prettier": ">=2.3.0 || >=3.0.0-alpha.0" } }, - "node_modules/prettier-plugin-solidity/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/prettier-plugin-solidity/node_modules/@solidity-parser/parser": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.15.0.tgz", + "integrity": "sha512-5UFJJTzWi1hgFk6aGCZ5rxG2DJkCJOzJ74qg7UkWSNCDSigW+CJLoYUb5bLiKrtI34Nr9rpFSUNHfkqtlL+N/w==", "dev": true, "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" + "antlr4ts": "^0.5.0-alpha.4" } }, - "node_modules/prettier-plugin-solidity/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prettier-plugin-solidity/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -11471,15 +10551,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/promise": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", @@ -11525,20 +10596,6 @@ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, - "node_modules/public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -11624,15 +10681,12 @@ ] }, "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/randombytes": { @@ -11644,16 +10698,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -11664,9 +10708,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -11679,78 +10723,53 @@ } }, "node_modules/read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", "dev": true, "dependencies": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", "dev": true, "dependencies": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", - "dev": true, - "dependencies": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" + "node": ">=8" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-pkg-up/node_modules/path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true, - "dependencies": { - "pinkie-promise": "^2.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/read-pkg/node_modules/path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/read-yaml-file": { @@ -11768,19 +10787,10 @@ "node": ">=6" } }, - "node_modules/read-yaml-file/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", + "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", "dev": true, "dependencies": { "inherits": "^2.0.3", @@ -12047,12 +11057,12 @@ "dev": true }, "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/responselike": { @@ -12076,19 +11086,6 @@ "node": ">=8" } }, - "node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", - "dev": true, - "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -12180,15 +11177,6 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", "dev": true }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12241,18 +11229,6 @@ "integrity": "sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==", "dev": true }, - "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -12755,46 +11731,64 @@ } }, "node_modules/slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "dependencies": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/slice-ansi/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/slice-ansi/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/smartwrap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/smartwrap/-/smartwrap-2.0.2.tgz", @@ -12824,13 +11818,19 @@ "node": ">=8" } }, - "node_modules/smartwrap/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "node_modules/smartwrap/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=6" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/smartwrap/node_modules/cliui": { @@ -12844,15 +11844,24 @@ "wrap-ansi": "^6.2.0" } }, - "node_modules/smartwrap/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "node_modules/smartwrap/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, "engines": { - "node": ">=0.10.0" + "node": ">=7.0.0" } }, + "node_modules/smartwrap/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/smartwrap/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -12930,19 +11939,6 @@ "node": ">=8" } }, - "node_modules/smartwrap/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/snake-case": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-2.1.0.tgz", @@ -12997,11 +11993,15 @@ "wrap-ansi": "^2.0.0" } }, - "node_modules/solc/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "node_modules/solc/node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", "dev": true, + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, "engines": { "node": ">=0.10.0" } @@ -13066,6 +12066,68 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/solc/node_modules/path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "dev": true, + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/solc/node_modules/path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/solc/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/solc/node_modules/read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "dev": true, + "dependencies": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/solc/node_modules/read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "dev": true, + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/solc/node_modules/require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", @@ -13177,395 +12239,128 @@ } }, "node_modules/solhint": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/solhint/-/solhint-3.3.7.tgz", - "integrity": "sha512-NjjjVmXI3ehKkb3aNtRJWw55SUVJ8HMKKodwe0HnejA+k0d2kmhw7jvpa+MCTbcEgt8IWSwx0Hu6aCo/iYOZzQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/solhint/-/solhint-3.4.0.tgz", + "integrity": "sha512-FYEs/LoTxMsWFP/OGsEqR1CBDn3Bn7hrTWsgtjai17MzxITgearIdlo374KKZjjIycu8E2xBcJ+RSWeoBvQmkw==", "dev": true, "dependencies": { - "@solidity-parser/parser": "^0.14.1", - "ajv": "^6.6.1", - "antlr4": "4.7.1", - "ast-parents": "0.0.1", - "chalk": "^2.4.2", - "commander": "2.18.0", - "cosmiconfig": "^5.0.7", - "eslint": "^5.6.0", - "fast-diff": "^1.1.2", - "glob": "^7.1.3", - "ignore": "^4.0.6", - "js-yaml": "^3.12.0", - "lodash": "^4.17.11", - "semver": "^6.3.0" + "@solidity-parser/parser": "^0.15.0", + "ajv": "^6.12.6", + "antlr4": "^4.11.0", + "ast-parents": "^0.0.1", + "chalk": "^4.1.2", + "commander": "^10.0.0", + "cosmiconfig": "^8.0.0", + "fast-diff": "^1.2.0", + "glob": "^8.0.3", + "ignore": "^5.2.4", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "pluralize": "^8.0.0", + "semver": "^6.3.0", + "strip-ansi": "^6.0.1", + "table": "^6.8.1", + "text-table": "^0.2.0" }, "bin": { "solhint": "solhint.js" }, "optionalDependencies": { - "prettier": "^1.14.3" + "prettier": "^2.8.3" } }, - "node_modules/solhint/node_modules/acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "node_modules/solhint/node_modules/@solidity-parser/parser": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.15.0.tgz", + "integrity": "sha512-5UFJJTzWi1hgFk6aGCZ5rxG2DJkCJOzJ74qg7UkWSNCDSigW+CJLoYUb5bLiKrtI34Nr9rpFSUNHfkqtlL+N/w==", + "dev": true, + "dependencies": { + "antlr4ts": "^0.5.0-alpha.4" + } + }, + "node_modules/solhint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "bin": { - "acorn": "bin/acorn" - }, "engines": { - "node": ">=0.4.0" + "node": ">=8" } }, "node_modules/solhint/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/solhint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "node_modules/solhint/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/solhint/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/solhint/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/solhint/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/solhint/node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/solhint/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/solhint/node_modules/eslint": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", - "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.9.1", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^4.0.3", - "eslint-utils": "^1.3.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^5.0.1", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.7.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^6.2.2", - "js-yaml": "^3.13.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.11", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^5.5.1", - "strip-ansi": "^4.0.0", - "strip-json-comments": "^2.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^6.14.0 || ^8.10.0 || >=9.10.0" - } - }, - "node_modules/solhint/node_modules/eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/solhint/node_modules/eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/solhint/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/solhint/node_modules/eslint/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/solhint/node_modules/espree": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", - "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", - "dev": true, - "dependencies": { - "acorn": "^6.0.7", - "acorn-jsx": "^5.0.0", - "eslint-visitor-keys": "^1.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/solhint/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/solhint/node_modules/file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "dependencies": { - "flat-cache": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/solhint/node_modules/flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "dependencies": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/solhint/node_modules/flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true - }, - "node_modules/solhint/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/solhint/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/solhint/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/solhint/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/solhint/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "node_modules/solhint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/solhint/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/solhint/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/solhint/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/solhint/node_modules/prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", - "dev": true, - "optional": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/solhint/node_modules/regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true, - "engines": { - "node": ">=6.5.0" - } - }, - "node_modules/solhint/node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" + "argparse": "^2.0.1" }, "bin": { - "rimraf": "bin.js" + "js-yaml": "bin/js-yaml.js" } }, "node_modules/solhint/node_modules/semver": { @@ -13577,76 +12372,34 @@ "semver": "bin/semver.js" } }, - "node_modules/solhint/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "node_modules/solhint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "dependencies": { - "shebang-regex": "^1.0.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/solhint/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/solhint/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/solhint/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/solhint/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/solhint/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" + "node": ">=8" } }, "node_modules/solidity-ast": { - "version": "0.4.40", - "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.40.tgz", - "integrity": "sha512-M8uLBT2jgFB7B0iVAC5a2l71J8vim7aEm03AZkaHbDqyrl1pE+i5PriMEw6WlwGfHp3/Ym7cn9BqvVLQgRk+Yw==", + "version": "0.4.46", + "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.46.tgz", + "integrity": "sha512-MlPZQfPhjWXqh7YxWcBGDXaPZIfMYCOHYoLEhGDWulNwEPIQQZuB7mA9eP17CU0jY/bGR4avCEUVVpvHtT2gbA==", "dev": true }, "node_modules/solidity-comments": { @@ -13888,18 +12641,6 @@ "node": ">=6" } }, - "node_modules/solidity-coverage/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/solidity-coverage/node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -13909,20 +12650,6 @@ "node": ">=6" } }, - "node_modules/solidity-coverage/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/solidity-coverage/node_modules/chokidar": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", @@ -13955,21 +12682,6 @@ "wrap-ansi": "^5.1.0" } }, - "node_modules/solidity-coverage/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/solidity-coverage/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "node_modules/solidity-coverage/node_modules/debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -13980,15 +12692,6 @@ "ms": "^2.1.1" } }, - "node_modules/solidity-coverage/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/solidity-coverage/node_modules/diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -14004,15 +12707,6 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, - "node_modules/solidity-coverage/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/solidity-coverage/node_modules/find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -14067,29 +12761,42 @@ } }, "node_modules/solidity-coverage/node_modules/glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, "engines": { "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/solidity-coverage/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/solidity-coverage/node_modules/globby": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", + "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", "dev": true, + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/solidity-coverage/node_modules/js-yaml": { @@ -14130,18 +12837,6 @@ "node": ">=8" } }, - "node_modules/solidity-coverage/node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/solidity-coverage/node_modules/mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", @@ -14197,16 +12892,33 @@ "url": "https://opencollective.com/mochajs" } }, - "node_modules/solidity-coverage/node_modules/mocha/node_modules/supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "node_modules/solidity-coverage/node_modules/mocha/node_modules/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "dependencies": { - "has-flag": "^3.0.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=6" + "node": "*" + } + }, + "node_modules/solidity-coverage/node_modules/mocha/node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, "node_modules/solidity-coverage/node_modules/ms": { @@ -14215,6 +12927,21 @@ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, + "node_modules/solidity-coverage/node_modules/object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/solidity-coverage/node_modules/p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -14284,15 +13011,15 @@ } }, "node_modules/solidity-coverage/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", "dev": true, "dependencies": { "has-flag": "^3.0.0" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/solidity-coverage/node_modules/which": { @@ -14682,15 +13409,12 @@ } }, "node_modules/strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, - "dependencies": { - "is-utf8": "^0.2.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, "node_modules/strip-hex-prefix": { @@ -14728,15 +13452,15 @@ } }, "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "has-flag": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/swap-case": { @@ -14856,6 +13580,18 @@ "node": ">=8" } }, + "node_modules/swarm-js/node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sync-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz", @@ -14880,59 +13616,94 @@ } }, "node_modules/table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", "dev": true, "dependencies": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=6.0.0" + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/table/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/table/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, + "node_modules/table/node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/table/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/table/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "dependencies": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/tar": { @@ -15006,12 +13777,6 @@ "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==", "dev": true }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, "node_modules/timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", @@ -15078,9 +13843,9 @@ } }, "node_modules/tough-cookie/node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true, "engines": { "node": ">=6" @@ -15152,6 +13917,21 @@ "node": ">=8" } }, + "node_modules/tty-table/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/tty-table/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -15168,6 +13948,33 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/tty-table/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/tty-table/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/tty-table/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/tty-table/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -15180,6 +13987,18 @@ "node": ">=8" } }, + "node_modules/tty-table/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -15256,6 +14075,20 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -15312,9 +14145,9 @@ "dev": true }, "node_modules/undici": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.14.0.tgz", - "integrity": "sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.20.0.tgz", + "integrity": "sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==", "dev": true, "dependencies": { "busboy": "^1.6.0" @@ -15476,28 +14309,28 @@ } }, "node_modules/web3": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.8.1.tgz", - "integrity": "sha512-tAqFsQhGv340C9OgRJIuoScN7f7wa1tUvsnnDUMt9YE6J4gcm7TV2Uwv+KERnzvV+xgdeuULYpsioRRNKrUvoQ==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.8.2.tgz", + "integrity": "sha512-92h0GdEHW9wqDICQQKyG4foZBYi0OQkyg4CRml2F7XBl/NG+fu9o6J19kzfFXzSBoA4DnJXbyRgj/RHZv5LRiw==", "dev": true, "hasInstallScript": true, "dependencies": { - "web3-bzz": "1.8.1", - "web3-core": "1.8.1", - "web3-eth": "1.8.1", - "web3-eth-personal": "1.8.1", - "web3-net": "1.8.1", - "web3-shh": "1.8.1", - "web3-utils": "1.8.1" + "web3-bzz": "1.8.2", + "web3-core": "1.8.2", + "web3-eth": "1.8.2", + "web3-eth-personal": "1.8.2", + "web3-net": "1.8.2", + "web3-shh": "1.8.2", + "web3-utils": "1.8.2" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-bzz": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.8.1.tgz", - "integrity": "sha512-dJJHS84nvpoxv6ijTMkdUSlRr5beCXNtx4UZcrFLHBva8dT63QEtKdLyDt2AyMJJdVzTCk78uir/6XtVWrdS6w==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.8.2.tgz", + "integrity": "sha512-1EEnxjPnFnvNWw3XeeKuTR8PBxYd0+XWzvaLK7OJC/Go9O8llLGxrxICbKV+8cgIE0sDRBxiYx02X+6OhoAQ9w==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -15509,63 +14342,57 @@ "node": ">=8.0.0" } }, - "node_modules/web3-bzz/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - }, "node_modules/web3-core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.8.1.tgz", - "integrity": "sha512-LbRZlJH2N6nS3n3Eo9Y++25IvzMY7WvYnp4NM/Ajhh97dAdglYs6rToQ2DbL2RLvTYmTew4O/y9WmOk4nq9COw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.8.2.tgz", + "integrity": "sha512-DJTVEAYcNqxkqruJE+Rxp3CIv0y5AZMwPHQmOkz/cz+MM75SIzMTc0AUdXzGyTS8xMF8h3YWMQGgGEy8SBf1PQ==", "dev": true, "dependencies": { "@types/bn.js": "^5.1.0", "@types/node": "^12.12.6", "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-core-requestmanager": "1.8.1", - "web3-utils": "1.8.1" + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-requestmanager": "1.8.2", + "web3-utils": "1.8.2" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-helpers": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.8.1.tgz", - "integrity": "sha512-ClzNO6T1S1gifC+BThw0+GTfcsjLEY8T1qUp6Ly2+w4PntAdNtKahxWKApWJ0l9idqot/fFIDXwO3Euu7I0Xqw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.8.2.tgz", + "integrity": "sha512-6B1eLlq9JFrfealZBomd1fmlq1o4A09vrCVQSa51ANoib/jllT3atZrRDr0zt1rfI7TSZTZBXdN/aTdeN99DWw==", "dev": true, "dependencies": { - "web3-eth-iban": "1.8.1", - "web3-utils": "1.8.1" + "web3-eth-iban": "1.8.2", + "web3-utils": "1.8.2" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-method": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.8.1.tgz", - "integrity": "sha512-oYGRodktfs86NrnFwaWTbv2S38JnpPslFwSSARwFv4W9cjbGUW3LDeA5MKD/dRY+ssZ5OaekeMsUCLoGhX68yA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.8.2.tgz", + "integrity": "sha512-1qnr5mw5wVyULzLOrk4B+ryO3gfGjGd/fx8NR+J2xCGLf1e6OSjxT9vbfuQ3fErk/NjSTWWreieYWLMhaogcRA==", "dev": true, "dependencies": { "@ethersproject/transactions": "^5.6.2", - "web3-core-helpers": "1.8.1", - "web3-core-promievent": "1.8.1", - "web3-core-subscriptions": "1.8.1", - "web3-utils": "1.8.1" + "web3-core-helpers": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-utils": "1.8.2" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-promievent": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.8.1.tgz", - "integrity": "sha512-9mxqHlgB0MrZI4oUIRFkuoJMNj3E7btjrMv3sMer/Z9rYR1PfoSc1aAokw4rxKIcAh+ylVtd/acaB2HKB7aRPg==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.8.2.tgz", + "integrity": "sha512-nvkJWDVgoOSsolJldN33tKW6bKKRJX3MCPDYMwP5SUFOA/mCzDEoI88N0JFofDTXkh1k7gOqp1pvwi9heuaxGg==", "dev": true, "dependencies": { "eventemitter3": "4.0.4" @@ -15575,40 +14402,34 @@ } }, "node_modules/web3-core-requestmanager": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.8.1.tgz", - "integrity": "sha512-x+VC2YPPwZ1khvqA6TA69LvfFCOZXsoUVOxmTx/vIN22PrY9KzKhxcE7pBSiGhmab1jtmRYXUbcQSVpAXqL8cw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.8.2.tgz", + "integrity": "sha512-p1d090RYs5Mu7DK1yyc3GCBVZB/03rBtFhYFoS2EruGzOWs/5Q0grgtpwS/DScdRAm8wB8mYEBhY/RKJWF6B2g==", "dev": true, "dependencies": { - "util": "^0.12.0", - "web3-core-helpers": "1.8.1", - "web3-providers-http": "1.8.1", - "web3-providers-ipc": "1.8.1", - "web3-providers-ws": "1.8.1" + "util": "^0.12.5", + "web3-core-helpers": "1.8.2", + "web3-providers-http": "1.8.2", + "web3-providers-ipc": "1.8.2", + "web3-providers-ws": "1.8.2" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-subscriptions": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.8.1.tgz", - "integrity": "sha512-bmCMq5OeA3E2vZUh8Js1HcJbhwtsE+yeMqGC4oIZB3XsL5SLqyKLB/pU+qUYqQ9o4GdcrFTDPhPg1bgvf7p1Pw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.8.2.tgz", + "integrity": "sha512-vXQogHDmAIQcKpXvGiMddBUeP9lnKgYF64+yQJhPNE5PnWr1sAibXuIPV7mIPihpFr/n/DORRj6Wh1pUv9zaTw==", "dev": true, "dependencies": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.1" + "web3-core-helpers": "1.8.2" }, "engines": { "node": ">=8.0.0" } }, - "node_modules/web3-core/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - }, "node_modules/web3-core/node_modules/bignumber.js": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", @@ -15619,58 +14440,57 @@ } }, "node_modules/web3-eth": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.8.1.tgz", - "integrity": "sha512-LgyzbhFqiFRd8M8sBXoFN4ztzOnkeckl3H/9lH5ek7AdoRMhBg7tYpYRP3E5qkhd/q+yiZmcUgy1AF6NHrC1wg==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.8.2.tgz", + "integrity": "sha512-JoTiWWc4F4TInpbvDUGb0WgDYJsFhuIjJlinc5ByjWD88Gvh+GKLsRjjFdbqe5YtwIGT4NymwoC5LQd1K6u/QQ==", "dev": true, "dependencies": { - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-core-subscriptions": "1.8.1", - "web3-eth-abi": "1.8.1", - "web3-eth-accounts": "1.8.1", - "web3-eth-contract": "1.8.1", - "web3-eth-ens": "1.8.1", - "web3-eth-iban": "1.8.1", - "web3-eth-personal": "1.8.1", - "web3-net": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-eth-accounts": "1.8.2", + "web3-eth-contract": "1.8.2", + "web3-eth-ens": "1.8.2", + "web3-eth-iban": "1.8.2", + "web3-eth-personal": "1.8.2", + "web3-net": "1.8.2", + "web3-utils": "1.8.2" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-abi": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.8.1.tgz", - "integrity": "sha512-0mZvCRTIG0UhDhJwNQJgJxu4b4DyIpuMA0GTfqxqeuqzX4Q/ZvmoNurw0ExTfXaGPP82UUmmdkRi6FdZOx+C6w==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.8.2.tgz", + "integrity": "sha512-Om9g3kaRNjqiNPAgKwGT16y+ZwtBzRe4ZJFGjLiSs6v5I7TPNF+rRMWuKnR6jq0azQZDj6rblvKFMA49/k48Og==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.6.3", - "web3-utils": "1.8.1" + "web3-utils": "1.8.2" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-accounts": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.8.1.tgz", - "integrity": "sha512-mgzxSYgN54/NsOFBO1Fq1KkXp1S5KlBvI/DlgvajU72rupoFMq6Cu6Yp9GUaZ/w2ij9PzEJuFJk174XwtfMCmg==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.8.2.tgz", + "integrity": "sha512-c367Ij63VCz9YdyjiHHWLFtN85l6QghgwMQH2B1eM/p9Y5lTlTX7t/Eg/8+f1yoIStXbk2w/PYM2lk+IkbqdLA==", "dev": true, "dependencies": { "@ethereumjs/common": "2.5.0", "@ethereumjs/tx": "3.3.2", - "crypto-browserify": "3.12.0", "eth-lib": "0.2.8", - "ethereumjs-util": "^7.0.10", + "ethereumjs-util": "^7.1.5", "scrypt-js": "^3.0.1", "uuid": "^9.0.0", - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-utils": "1.8.2" }, "engines": { "node": ">=8.0.0" @@ -15697,51 +14517,51 @@ } }, "node_modules/web3-eth-contract": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.8.1.tgz", - "integrity": "sha512-1wphnl+/xwCE2io44JKnN+ti3oa47BKRiVzvWd42icwRbcpFfRxH9QH+aQX3u8VZIISNH7dAkTWpGIIJgGFTmg==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.8.2.tgz", + "integrity": "sha512-ID5A25tHTSBNwOPjiXSVzxruz006ULRIDbzWTYIFTp7NJ7vXu/kynKK2ag/ObuTqBpMbobP8nXcA9b5EDkIdQA==", "dev": true, "dependencies": { "@types/bn.js": "^5.1.0", - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-core-promievent": "1.8.1", - "web3-core-subscriptions": "1.8.1", - "web3-eth-abi": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-utils": "1.8.2" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-ens": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.8.1.tgz", - "integrity": "sha512-FT8xTI9uN8RxeBQa/W8pLa2aoFh4+EE34w7W2271LICKzla1dtLyb6XSdn48vsUcPmhWsTVk9mO9RTU0l4LGQQ==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.8.2.tgz", + "integrity": "sha512-PWph7C/CnqdWuu1+SH4U4zdrK4t2HNt0I4XzPYFdv9ugE8EuojselioPQXsVGvjql+Nt3jDLvQvggPqlMbvwRw==", "dev": true, "dependencies": { "content-hash": "^2.5.2", "eth-ens-namehash": "2.0.8", - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-promievent": "1.8.1", - "web3-eth-abi": "1.8.1", - "web3-eth-contract": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-eth-contract": "1.8.2", + "web3-utils": "1.8.2" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-iban": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.8.1.tgz", - "integrity": "sha512-DomoQBfvIdtM08RyMGkMVBOH0vpOIxSSQ+jukWk/EkMLGMWJtXw/K2c2uHAeq3L/VPWNB7zXV2DUEGV/lNE2Dg==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.8.2.tgz", + "integrity": "sha512-h3vNblDWkWMuYx93Q27TAJz6lhzpP93EiC3+45D6xoz983p6si773vntoQ+H+5aZhwglBtoiBzdh7PSSOnP/xQ==", "dev": true, "dependencies": { "bn.js": "^5.2.1", - "web3-utils": "1.8.1" + "web3-utils": "1.8.2" }, "engines": { "node": ">=8.0.0" @@ -15754,78 +14574,72 @@ "dev": true }, "node_modules/web3-eth-personal": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.8.1.tgz", - "integrity": "sha512-myIYMvj7SDIoV9vE5BkVdon3pya1WinaXItugoii2VoTcQNPOtBxmYVH+XS5ErzCJlnxzphpQrkywyY64bbbCA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.8.2.tgz", + "integrity": "sha512-Vg4HfwCr7doiUF/RC+Jz0wT4+cYaXcOWMAW2AHIjHX6Z7Xwa8nrURIeQgeEE62qcEHAzajyAdB1u6bJyTfuCXw==", "dev": true, "dependencies": { "@types/node": "^12.12.6", - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-net": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-net": "1.8.2", + "web3-utils": "1.8.2" }, "engines": { "node": ">=8.0.0" } }, - "node_modules/web3-eth-personal/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - }, "node_modules/web3-net": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.8.1.tgz", - "integrity": "sha512-LyEJAwogdFo0UAXZqoSJGFjopdt+kLw0P00FSZn2yszbgcoI7EwC+nXiOsEe12xz4LqpYLOtbR7+gxgiTVjjHQ==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.8.2.tgz", + "integrity": "sha512-1itkDMGmbgb83Dg9nporFes9/fxsU7smJ3oRXlFkg4ZHn8YJyP1MSQFPJWWwSc+GrcCFt4O5IrUTvEkHqE3xag==", "dev": true, "dependencies": { - "web3-core": "1.8.1", - "web3-core-method": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.8.2", + "web3-core-method": "1.8.2", + "web3-utils": "1.8.2" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-providers-http": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.8.1.tgz", - "integrity": "sha512-1Zyts4O9W/UNEPkp+jyL19Jc3D15S4yp8xuLTjVhcUEAlHo24NDWEKxtZGUuHk4HrKL2gp8OlsDbJ7MM+ESDgg==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.8.2.tgz", + "integrity": "sha512-2xY94IIEQd16+b+vIBF4IC1p7GVaz9q4EUFscvMUjtEq4ru4Atdzjs9GP+jmcoo49p70II0UV3bqQcz0TQfVyQ==", "dev": true, "dependencies": { "abortcontroller-polyfill": "^1.7.3", "cross-fetch": "^3.1.4", "es6-promise": "^4.2.8", - "web3-core-helpers": "1.8.1" + "web3-core-helpers": "1.8.2" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-providers-ipc": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.8.1.tgz", - "integrity": "sha512-nw/W5nclvi+P2z2dYkLWReKLnocStflWqFl+qjtv0xn3MrUTyXMzSF0+61i77+16xFsTgzo4wS/NWIOVkR0EFA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.8.2.tgz", + "integrity": "sha512-p6fqKVGFg+WiXGHWnB1hu43PbvPkDHTz4RgoEzbXugv5rtv5zfYLqm8Ba6lrJOS5ks9kGKR21a0y3NzE3u7V4w==", "dev": true, "dependencies": { "oboe": "2.1.5", - "web3-core-helpers": "1.8.1" + "web3-core-helpers": "1.8.2" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-providers-ws": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.8.1.tgz", - "integrity": "sha512-TNefIDAMpdx57+YdWpYZ/xdofS0P+FfKaDYXhn24ie/tH9G+AB+UBSOKnjN0KSadcRSCMBwGPRiEmNHPavZdsA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.8.2.tgz", + "integrity": "sha512-3s/4K+wHgbiN+Zrp9YjMq2eqAF6QGABw7wFftPdx+m5hWImV27/MoIx57c6HffNRqZXmCHnfWWFCNHHsi7wXnA==", "dev": true, "dependencies": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.1", + "web3-core-helpers": "1.8.2", "websocket": "^1.0.32" }, "engines": { @@ -15833,25 +14647,25 @@ } }, "node_modules/web3-shh": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.8.1.tgz", - "integrity": "sha512-sqHgarnfcY2Qt3PYS4R6YveHrDy7hmL09yeLLHHCI+RKirmjLVqV0rc5LJWUtlbYI+kDoa5gbgde489M9ZAC0g==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.8.2.tgz", + "integrity": "sha512-uZ+3MAoNcaJsXXNCDnizKJ5viBNeHOFYsCbFhV755Uu52FswzTOw6DtE7yK9nYXMtIhiSgi7nwl1RYzP8pystw==", "dev": true, "hasInstallScript": true, "dependencies": { - "web3-core": "1.8.1", - "web3-core-method": "1.8.1", - "web3-core-subscriptions": "1.8.1", - "web3-net": "1.8.1" + "web3-core": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-net": "1.8.2" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-utils": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.1.tgz", - "integrity": "sha512-LgnM9p6V7rHHUGfpMZod+NST8cRfGzJ1BTXAyNo7A9cJX9LczBfSRxJp+U/GInYe9mby40t3v22AJdlELibnsQ==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", + "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", "dev": true, "dependencies": { "bn.js": "^5.2.1", @@ -16058,6 +14872,39 @@ "node": ">=8" } }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -16099,18 +14946,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "node_modules/write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "dependencies": { - "mkdirp": "^0.5.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/ws": { "version": "7.5.9", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", @@ -16211,9 +15046,9 @@ "dev": true }, "node_modules/yargs": { - "version": "17.6.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", - "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", "dev": true, "dependencies": { "cliui": "^8.0.1", @@ -16229,12 +15064,25 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs-parser/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "engines": { - "node": ">=10" + "node": ">=6" } }, "node_modules/yargs-unparser": { @@ -16264,6 +15112,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/yargs/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -16355,70 +15224,12 @@ "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } } }, "@babel/runtime": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", - "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", + "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", "dev": true, "requires": { "regenerator-runtime": "^0.13.11" @@ -16445,18 +15256,6 @@ "semver": "^5.4.1" }, "dependencies": { - "detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -16548,79 +15347,11 @@ "tty-table": "^4.1.5" }, "dependencies": { - "ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -16661,67 +15392,11 @@ "semver": "^5.4.1" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -16778,64 +15453,6 @@ "dev": true, "requires": { "chalk": "^2.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } } }, "@changesets/parse": { @@ -16875,64 +15492,6 @@ "chalk": "^2.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } } }, "@changesets/types": { @@ -17573,12 +16132,6 @@ "fs-extra": "^8.1.0" }, "dependencies": { - "@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - }, "fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -17622,20 +16175,6 @@ "jsonfile": "^4.0.0", "universalify": "^0.1.0" } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } } } }, @@ -17679,15 +16218,15 @@ } }, "@noble/hashes": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", - "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", + "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", "dev": true }, "@noble/secp256k1": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.6.3.tgz", - "integrity": "sha512-T04e4iTurVy7I8Sw4+c5OSN9/RkPlo1uKxAomtxQNLq8j1uPAqnsqG1bqvY3Jv7c13gyr6dui0zmh/I3+f/JaQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", "dev": true }, "@nodelib/fs.scandir": { @@ -17870,9 +16409,9 @@ } }, "@nomicfoundation/hardhat-network-helpers": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.7.tgz", - "integrity": "sha512-X+3mNvn8B7BY5hpIaLO+TrfzWq12bpux+ajGGdmdcfC78NXmYmOZkAtiz1QZx1YIZGMS1LaXzPXyBExxKFpCaw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.8.tgz", + "integrity": "sha512-MNqQbzUJZnCMIYvlniC3U+kcavz/PhhQSsY90tbEtUyMj/IQqsLwIRZa4ctjABh3Bz0KCh9OXUZ7Yk/d9hr45Q==", "dev": true, "requires": { "ethereumjs-util": "^7.1.4" @@ -18044,6 +16583,57 @@ "js-yaml": "^3.13.1", "lodash.startcase": "^4.4.0", "minimist": "^1.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "@openzeppelin/test-helpers": { @@ -18064,6 +16654,12 @@ "web3-utils": "^1.2.5" }, "dependencies": { + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -18073,9 +16669,9 @@ } }, "@openzeppelin/upgrades-core": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.20.6.tgz", - "integrity": "sha512-KWdtlahm+iunlAlzLsdpBueanwEx0LLPfAkDL1p0C4SPjMiUqHHFlyGtmmWwdiqDpJ//605vfwkd5RqfnFrHSg==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.24.0.tgz", + "integrity": "sha512-lXf1tUrCZ3Q/YmWhw0cuSSOHMp0OAsmeOg1fhSGEM6nQQ6cIVlFvq2pCV5hZMb7xkOm5pmmzV8JW1W3kfW6Lfw==", "dev": true, "requires": { "cbor": "^8.0.0", @@ -18087,13 +16683,13 @@ "solidity-ast": "^0.4.15" }, "dependencies": { - "cbor": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", - "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "nofilter": "^3.1.0" + "color-convert": "^2.0.1" } }, "chalk": { @@ -18106,11 +16702,35 @@ "supports-color": "^7.1.0" } }, - "nofilter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", - "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } } } }, @@ -18121,23 +16741,23 @@ "dev": true }, "@scure/bip32": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.0.tgz", - "integrity": "sha512-ftTW3kKX54YXLCxH6BB7oEEoJfoE2pIgw7MINKAs5PsS6nqKPuKk1haTF/EuHmYqG330t5GSrdmtRuHaY1a62Q==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz", + "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==", "dev": true, "requires": { - "@noble/hashes": "~1.1.1", - "@noble/secp256k1": "~1.6.0", + "@noble/hashes": "~1.2.0", + "@noble/secp256k1": "~1.7.0", "@scure/base": "~1.1.0" } }, "@scure/bip39": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz", - "integrity": "sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz", + "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==", "dev": true, "requires": { - "@noble/hashes": "~1.1.1", + "@noble/hashes": "~1.2.0", "@scure/base": "~1.1.0" } }, @@ -18247,14 +16867,14 @@ } }, "@truffle/abi-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@truffle/abi-utils/-/abi-utils-0.3.6.tgz", - "integrity": "sha512-61aTH2QmwVA1INaPMufRHTsS6jsEhS+GCkuCDdvBDmwctSnCKGDOr185BGt65QrpMRxYmIoH6WFBSNMYxW9GRw==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@truffle/abi-utils/-/abi-utils-0.3.8.tgz", + "integrity": "sha512-RevMtlEuQw9cE2VWzQuvLDSYPCVSZQQUOsaK8ArcwxXct6ugiJCDC7tNouhHzP1Y4ccdZtG2y/XPHQJdzgYdZQ==", "dev": true, "requires": { "change-case": "3.0.2", "fast-check": "3.1.1", - "web3-utils": "1.8.1" + "web3-utils": "1.8.2" } }, "@truffle/blockchain-utils": { @@ -18264,13 +16884,13 @@ "dev": true }, "@truffle/codec": { - "version": "0.14.11", - "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.14.11.tgz", - "integrity": "sha512-NgfMNYemgMXqoEcJA5ZsEhxChCwq33rSxtNxlececEH/1Nf0r+ryfrfmLlyPmv8f3jorVf1GWa0zI0AedGCGYQ==", + "version": "0.14.15", + "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.14.15.tgz", + "integrity": "sha512-cooWy8blmvYQQRBKgzWJnDUS6mZE9cvnmpVN15jU6TseAQkBtmfYfCH12QCKEWMq2+R4yQICP54WNCYg78g72g==", "dev": true, "requires": { - "@truffle/abi-utils": "^0.3.6", - "@truffle/compile-common": "^0.9.1", + "@truffle/abi-utils": "^0.3.8", + "@truffle/compile-common": "^0.9.3", "big.js": "^6.0.3", "bn.js": "^5.1.3", "cbor": "^5.2.0", @@ -18278,15 +16898,31 @@ "lodash": "^4.17.21", "semver": "7.3.7", "utf8": "^3.0.0", - "web3-utils": "1.8.1" + "web3-utils": "1.8.2" }, "dependencies": { + "bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "dev": true + }, "bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", "dev": true }, + "cbor": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-5.2.0.tgz", + "integrity": "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A==", + "dev": true, + "requires": { + "bignumber.js": "^9.0.1", + "nofilter": "^1.0.4" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -18296,6 +16932,12 @@ "yallist": "^4.0.0" } }, + "nofilter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz", + "integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==", + "dev": true + }, "semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -18314,41 +16956,57 @@ } }, "@truffle/compile-common": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@truffle/compile-common/-/compile-common-0.9.1.tgz", - "integrity": "sha512-mhdkX6ExZImHSBO3jGm6aAn8NpVtMTdjq50jRXY/O59/ZNC0J9WpRapxrAKUVNc+XydMdBlfeEpXoqTJg7cbXw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@truffle/compile-common/-/compile-common-0.9.3.tgz", + "integrity": "sha512-9S86H5DRC0zEj164KeClP/6jVt1M/nRd7St89h6QbuIU0JjpqSz1SXpkvqhbFoV9hhW+4ZGh0NysjrdPlk7gFw==", "dev": true, "requires": { - "@truffle/error": "^0.1.1", + "@truffle/error": "^0.2.0", "colors": "1.4.0" + }, + "dependencies": { + "@truffle/error": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@truffle/error/-/error-0.2.0.tgz", + "integrity": "sha512-Fe0/z4WWb7IP2gBnv3l6zqP87Y0kSMs7oiSLakKJq17q3GUunrHSdioKuNspdggxkXIBhEQLhi8C+LJdwmHKWQ==", + "dev": true + } } }, "@truffle/contract": { - "version": "4.6.10", - "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.10.tgz", - "integrity": "sha512-69IZSXeQKRP3EutILqe+vLY5A5gUpeXUiZhm/Fy/qHHkP238vMjtOkTZGkY6bonYqmgk+vDY7KSYSYKzDNPdCA==", + "version": "4.6.15", + "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.15.tgz", + "integrity": "sha512-3V+50z94XRh+I3axz6Zrot1wlMhRiG2O9XjR20LwR5YBnzHV7YFfOs8x6pluXVLQyxcVJBISJyqxvGxpkecrZg==", "dev": true, "requires": { "@ensdomains/ensjs": "^2.1.0", "@truffle/blockchain-utils": "^0.1.6", - "@truffle/contract-schema": "^3.4.11", - "@truffle/debug-utils": "^6.0.42", - "@truffle/error": "^0.1.1", - "@truffle/interface-adapter": "^0.5.26", + "@truffle/contract-schema": "^3.4.12", + "@truffle/debug-utils": "^6.0.46", + "@truffle/error": "^0.2.0", + "@truffle/interface-adapter": "^0.5.29", "bignumber.js": "^7.2.1", "debug": "^4.3.1", "ethers": "^4.0.32", - "web3": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-promievent": "1.8.1", - "web3-eth-abi": "1.8.1", - "web3-utils": "1.8.1" + "web3": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-utils": "1.8.2" + }, + "dependencies": { + "@truffle/error": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@truffle/error/-/error-0.2.0.tgz", + "integrity": "sha512-Fe0/z4WWb7IP2gBnv3l6zqP87Y0kSMs7oiSLakKJq17q3GUunrHSdioKuNspdggxkXIBhEQLhi8C+LJdwmHKWQ==", + "dev": true + } } }, "@truffle/contract-schema": { - "version": "3.4.11", - "resolved": "https://registry.npmjs.org/@truffle/contract-schema/-/contract-schema-3.4.11.tgz", - "integrity": "sha512-wReyVZUPyU9Zy5PSCugBLG1nnruBmRAJ/gmoirQiJ9N2n+s1iGBTY49tkDqFMz3XUUE0kplfdb9YKZJlLkTWzQ==", + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/@truffle/contract-schema/-/contract-schema-3.4.12.tgz", + "integrity": "sha512-XpMMps/bqHwiRuCyLiEEGWEAvGGzGj4u1X1+lzxrtIsrwbQhSZcdgEbXl9vGxOOJWOup3HXRCIsjlao27kS4OA==", "dev": true, "requires": { "ajv": "^6.10.0", @@ -18356,80 +17014,24 @@ } }, "@truffle/debug-utils": { - "version": "6.0.42", - "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.42.tgz", - "integrity": "sha512-9v70tj+My0Z2UZJ9OsuUlfo4Dt2AJqAQa/YWtGe28H8zsi+o9Dca0RsKWecuprdllgzrEs7ad8QUtSINhwjIlg==", + "version": "6.0.46", + "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.46.tgz", + "integrity": "sha512-MWko3c1M0NI8IT0yNh8Dh2xoDgR2R1apP8hc/9VsR/4Q7P6OE/578WRpZnvo2HWXYBiJ2PrWBDtMYBHntImrrA==", "dev": true, "requires": { - "@truffle/codec": "^0.14.11", + "@truffle/codec": "^0.14.15", "@trufflesuite/chromafi": "^3.0.0", "bn.js": "^5.1.3", "chalk": "^2.4.2", "debug": "^4.3.1", - "highlightjs-solidity": "^2.0.5" + "highlightjs-solidity": "^2.0.6" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, "bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -18440,14 +17042,14 @@ "dev": true }, "@truffle/interface-adapter": { - "version": "0.5.26", - "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.26.tgz", - "integrity": "sha512-fBhoqtT+CT4XKXcOijvw0RIMgyUi3FJg+n5i5PyGBsoRzqbLZd9cZq+oMNjOZPdf3GH68hsOFOaQO5tZH7oZow==", + "version": "0.5.29", + "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.29.tgz", + "integrity": "sha512-6UlJ+f87z7y/dWk9UfbIU+4e80iRsp8h03LEiE5B+PvZbr6cuMjLJUBtBBQZMo3+xrIcS/2u3p5hOxW8OJm8tw==", "dev": true, "requires": { "bn.js": "^5.1.3", "ethers": "^4.0.32", - "web3": "1.8.1" + "web3": "1.8.2" }, "dependencies": { "bn.js": { @@ -18474,61 +17076,11 @@ "strip-indent": "^2.0.0" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "detect-indent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", + "integrity": "sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==", "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -18615,14 +17167,6 @@ "dev": true, "requires": { "ci-info": "^3.1.0" - }, - "dependencies": { - "ci-info": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.1.tgz", - "integrity": "sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==", - "dev": true - } } }, "@types/keyv": { @@ -18653,9 +17197,9 @@ "dev": true }, "@types/node": { - "version": "18.11.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", - "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", "dev": true }, "@types/normalize-package-data": { @@ -18762,9 +17306,9 @@ } }, "acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", "dev": true }, "acorn-jsx": { @@ -18831,9 +17375,9 @@ "optional": true }, "ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true }, "ansi-escapes": { @@ -18860,18 +17404,18 @@ "dev": true }, "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "color-convert": "^2.0.1" + "color-convert": "^1.9.0" } }, "antlr4": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.7.1.tgz", - "integrity": "sha512-haHyTW7Y9joE5MVs37P2lNYfU2RWBLfcRDD8OWldcdZm5TiCE91B5Xl1oWSwiDUSd4rlExpt2pu1fksYQjRBYQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.12.0.tgz", + "integrity": "sha512-23iB5IzXJZRZeK9TigzUyrNc9pSmNqAerJRBcNq1ETrmttMWRgaYZzC561IgEO3ygKsDJTYDTozABXa4b/fTQQ==", "dev": true }, "antlr4ts": { @@ -18963,18 +17507,6 @@ "safer-buffer": "~2.1.0" } }, - "asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -18994,9 +17526,9 @@ "dev": true }, "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, "async": { @@ -19042,9 +17574,9 @@ "dev": true }, "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", "dev": true }, "balanced-match": { @@ -19158,13 +17690,13 @@ "dev": true }, "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dev": true, "requires": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -19172,7 +17704,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -19266,72 +17798,6 @@ "safe-buffer": "^5.0.1" } }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "dev": true, - "requires": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - }, - "dependencies": { - "bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - } - } - }, - "browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "dev": true, - "requires": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - } - } - }, "bs58": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", @@ -19458,32 +17924,6 @@ "get-intrinsic": "^1.0.2" } }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", - "dev": true, - "requires": { - "callsites": "^2.0.0" - }, - "dependencies": { - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", - "dev": true - } - } - }, - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", - "dev": true, - "requires": { - "caller-callsite": "^2.0.0" - } - }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -19522,12 +17962,6 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true - }, - "quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true } } }, @@ -19544,21 +17978,12 @@ "dev": true }, "cbor": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-5.2.0.tgz", - "integrity": "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", + "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", "dev": true, "requires": { - "bignumber.js": "^9.0.1", - "nofilter": "^1.0.4" - }, - "dependencies": { - "bignumber.js": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", - "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", - "dev": true - } + "nofilter": "^3.1.0" } }, "chai": { @@ -19584,13 +18009,14 @@ "requires": {} }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, "change-case": { @@ -19689,9 +18115,9 @@ "dev": true }, "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", "dev": true }, "cids": { @@ -19754,15 +18180,6 @@ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, "cli-table3": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", @@ -19774,12 +18191,6 @@ "string-width": "^2.1.1" } }, - "cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", - "dev": true - }, "cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -19847,18 +18258,18 @@ "dev": true }, "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "requires": { - "color-name": "~1.1.4" + "color-name": "1.1.3" } }, "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, "colors": { @@ -19883,9 +18294,9 @@ "dev": true }, "commander": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz", - "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz", + "integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==", "dev": true }, "compare-versions": { @@ -19913,9 +18324,9 @@ }, "dependencies": { "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -19975,9 +18386,9 @@ } }, "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true }, "cookie": { @@ -20009,32 +18420,31 @@ } }, "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.0.0.tgz", + "integrity": "sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ==", "dev": true, "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" }, "dependencies": { - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" + "argparse": "^2.0.1" } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", - "dev": true } } }, @@ -20044,16 +18454,6 @@ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", "dev": true }, - "create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, "create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -20088,6 +18488,17 @@ "dev": true, "requires": { "node-fetch": "2.6.7" + }, + "dependencies": { + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + } } }, "cross-spawn": { @@ -20122,25 +18533,6 @@ "sha3": "^2.1.1" } }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, "crypto-js": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", @@ -20237,9 +18629,9 @@ } }, "decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true }, "decamelize-keys": { @@ -20252,12 +18644,6 @@ "map-obj": "^1.0.0" }, "dependencies": { - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true - }, "map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", @@ -20320,9 +18706,9 @@ "dev": true }, "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", "dev": true, "requires": { "has-property-descriptors": "^1.0.0", @@ -20341,16 +18727,6 @@ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true }, - "des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, "destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -20358,9 +18734,9 @@ "dev": true }, "detect-indent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", - "integrity": "sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", "dev": true }, "detect-port": { @@ -20379,17 +18755,6 @@ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, "difflib": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", @@ -20534,14 +18899,6 @@ "dev": true, "requires": { "ansi-colors": "^4.1.1" - }, - "dependencies": { - "ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true - } } }, "entities": { @@ -20566,27 +18923,33 @@ } }, "es-abstract": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz", - "integrity": "sha512-7h8MM2EQhsCA7pU/Nv78qOXFpD8Rhqd12gYiSJVkrH9+e8VuA8JlPJK/hQjjlLv6pJvx/z1iRFKzYb0XT/RuAQ==", + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz", + "integrity": "sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==", "dev": true, "requires": { + "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", "get-intrinsic": "^1.1.3", "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", "gopd": "^1.0.1", "has": "^1.0.3", "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", + "internal-slot": "^1.0.4", + "is-array-buffer": "^3.0.1", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", "is-weakref": "^1.0.2", "object-inspect": "^1.12.2", "object-keys": "^1.1.1", @@ -20595,21 +18958,9 @@ "safe-regex-test": "^1.0.0", "string.prototype.trimend": "^1.0.6", "string.prototype.trimstart": "^1.0.6", - "unbox-primitive": "^1.0.2" - }, - "dependencies": { - "object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - } - } + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" } }, "es-array-method-boxes-properly": { @@ -20618,6 +18969,17 @@ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", "dev": true }, + "es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + } + }, "es-shim-unscopables": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", @@ -20689,9 +19051,9 @@ "dev": true }, "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, "escodegen": { @@ -20761,9 +19123,9 @@ } }, "eslint": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.31.0.tgz", - "integrity": "sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.34.0.tgz", + "integrity": "sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg==", "dev": true, "requires": { "@eslint/eslintrc": "^1.4.1", @@ -20813,6 +19175,15 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -20829,6 +19200,27 @@ "supports-color": "^7.1.0" } }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -20848,6 +19240,12 @@ "is-glob": "^4.0.3" } }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -20892,6 +19290,15 @@ "requires": { "ansi-regex": "^5.0.1" } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } } } }, @@ -20953,9 +19360,9 @@ "dev": true }, "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz", + "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -21041,43 +19448,12 @@ "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "chokidar": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", @@ -21105,21 +19481,6 @@ "wrap-ansi": "^5.1.0" } }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -21129,12 +19490,6 @@ "ms": "^2.1.1" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true - }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -21147,22 +19502,16 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, "ethereum-cryptography": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.1.2.tgz", - "integrity": "sha512-XDSJlg4BD+hq9N2FjvotwUET9Tfxpxc3kWGE2AqUG5vcbeunnbImVk3cj6e/xT3phdW21mE8R5IugU4fspQDcQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", "dev": true, "requires": { - "@noble/hashes": "1.1.2", - "@noble/secp256k1": "1.6.3", - "@scure/bip32": "1.1.0", - "@scure/bip39": "1.1.0" + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" } }, "find-up": { @@ -21204,12 +19553,6 @@ "path-is-absolute": "^1.0.0" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, "js-yaml": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", @@ -21295,6 +19638,18 @@ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, "p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -21783,6 +20138,26 @@ "vary": "~1.1.2" }, "dependencies": { + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, "cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -21803,6 +20178,18 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } } } }, @@ -21907,23 +20294,6 @@ "reusify": "^1.0.4" } }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - }, - "dependencies": { - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - } - } - }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -22154,9 +20524,9 @@ "dev": true }, "get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", "dev": true, "requires": { "function-bind": "^1.1.1", @@ -22203,70 +20573,12 @@ "requires": { "chalk": "^2.4.2", "node-emoji": "^1.10.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } } }, "glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -22286,9 +20598,9 @@ } }, "minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -22347,44 +20659,35 @@ } }, "globals": { - "version": "13.19.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz", - "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", "dev": true, "requires": { "type-fest": "^0.20.2" } }, - "globby": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", - "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "requires": { - "@types/glob": "^7.1.1", "array-union": "^2.1.0", "dir-glob": "^3.0.1", - "fast-glob": "^3.0.3", - "glob": "^7.1.3", - "ignore": "^5.1.1", - "merge2": "^1.2.3", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", "slash": "^3.0.0" - }, - "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } } }, "gopd": { @@ -22488,9 +20791,9 @@ "dev": true }, "hardhat": { - "version": "2.12.5", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.12.5.tgz", - "integrity": "sha512-f/t7+hLlhsnQZ6LDXyV+8rHGRZFZY1sgFvgrwr9fBjMdGp1Bu6hHq1KXS4/VFZfZcVdL1DAWWEkryinZhqce+A==", + "version": "2.12.7", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.12.7.tgz", + "integrity": "sha512-voWoN6zn5d8BOEaczSyK/1PyfdeOeI3SbGCFb36yCHTJUt6OIqLb+ZDX30VhA1UsYKzLqG7UnWl3fKJUuANc6A==", "dev": true, "requires": { "@ethersproject/abi": "^5.1.2", @@ -22540,44 +20843,15 @@ "source-map-support": "^0.5.13", "stacktrace-parser": "^0.1.10", "tsort": "0.0.1", - "undici": "^5.4.0", + "undici": "^5.14.0", "uuid": "^8.3.2", "ws": "^7.4.6" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, "commander": { @@ -22586,22 +20860,16 @@ "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", "dev": true }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, "ethereum-cryptography": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.1.2.tgz", - "integrity": "sha512-XDSJlg4BD+hq9N2FjvotwUET9Tfxpxc3kWGE2AqUG5vcbeunnbImVk3cj6e/xT3phdW21mE8R5IugU4fspQDcQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", "dev": true, "requires": { - "@noble/hashes": "1.1.2", - "@noble/secp256k1": "1.6.3", - "@scure/bip32": "1.1.0", - "@scure/bip39": "1.1.0" + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" } }, "find-up": { @@ -22627,12 +20895,6 @@ "path-is-absolute": "^1.0.0" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, "jsonfile": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", @@ -22740,22 +21002,13 @@ "dev": true } } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, "hardhat-exposed": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.1.tgz", - "integrity": "sha512-qyHXdS3NmzrtXF+XL547BMsTAK+IEMW9OOYMH4d362DlPn4L2B2KXKG6OpuJczxYjgMFJ0LunLxNgu6jtRvjRg==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.2.tgz", + "integrity": "sha512-qhi60I2bfjoPZwKgrY7BIpuUiBE7aC/bHN2MzHxPcZdxaeFnjKJ50n59LE7yK3GK2qYzE8DMjzqfnH6SlKPUjw==", "dev": true, "requires": { "micromatch": "^4.0.4", @@ -22774,9 +21027,9 @@ } }, "hardhat-ignore-warnings": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/hardhat-ignore-warnings/-/hardhat-ignore-warnings-0.2.6.tgz", - "integrity": "sha512-GQgvjprONI8VF8b85+QJ8H9v3L9TCCtQvUx+9QaRL+sCPw1cOZHfhlEz9V6Lq7GNCQMqBORVzNzUzoP/tKAEQQ==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/hardhat-ignore-warnings/-/hardhat-ignore-warnings-0.2.8.tgz", + "integrity": "sha512-vPX94rJyTzYsCOzGIYdOcJgn3iQI6qa+CI9ZZfgDhdXJpda8ljpOT7bdUKAYC4LyoP0Z5fWTmupXoPaQrty0gw==", "dev": true, "requires": { "minimatch": "^5.1.0", @@ -22794,9 +21047,9 @@ } }, "minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -22820,9 +21073,9 @@ "dev": true }, "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true }, "has-property-descriptors": { @@ -22834,6 +21087,12 @@ "get-intrinsic": "^1.1.1" } }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true + }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", @@ -22899,9 +21158,9 @@ "dev": true }, "highlightjs-solidity": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-2.0.5.tgz", - "integrity": "sha512-ReXxQSGQkODMUgHcWzVSnfDCDrL2HshOYgw3OlIYmfHeRzUPkfJTUIp95pK4CmbiNG2eMTOmNLpfCz9Zq7Cwmg==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-2.0.6.tgz", + "integrity": "sha512-DySXWfQghjm2l6a/flF+cteroJqD4gI8GSdL4PtvxZSsAHie8m3yVe2JFoRg03ROKT6hp2Lc/BxXkqerNmtQYg==", "dev": true }, "hmac-drbg": { @@ -23006,6 +21265,14 @@ "requires": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" + }, + "dependencies": { + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + } } }, "https-proxy-agent": { @@ -23055,9 +21322,9 @@ "dev": true }, "immutable": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.1.tgz", - "integrity": "sha512-7WYV7Q5BTs0nlQm7tl92rDYYoyELLKHoDMBKhrxEoiV4mrfVdRz8hzPiYOzH7yWjzoVEamxRuAqhxL2PLRwZYQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz", + "integrity": "sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w==", "dev": true }, "import-fresh": { @@ -23068,6 +21335,14 @@ "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } } }, "imurmurhash": { @@ -23104,113 +21379,13 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, - "inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "internal-slot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz", - "integrity": "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", "dev": true, "requires": { - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "has": "^1.0.3", "side-channel": "^1.0.4" } @@ -23252,6 +21427,17 @@ "has-tostringtag": "^1.0.0" } }, + "is-array-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz", + "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-typed-array": "^1.1.10" + } + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -23305,14 +21491,6 @@ "dev": true, "requires": { "ci-info": "^3.2.0" - }, - "dependencies": { - "ci-info": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.1.tgz", - "integrity": "sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==", - "dev": true - } } }, "is-date-object": { @@ -23324,12 +21502,6 @@ "has-tostringtag": "^1.0.0" } }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", - "dev": true - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -23409,9 +21581,9 @@ "dev": true }, "is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true }, "is-port-reachable": { @@ -23540,9 +21712,9 @@ "dev": true }, "js-sdsl": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", - "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", "dev": true }, "js-sha3": { @@ -23579,12 +21751,6 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -23802,6 +21968,15 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } } } }, @@ -23815,14 +21990,6 @@ "js-yaml": "^3.13.0", "pify": "^4.0.1", "strip-bom": "^3.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true - } } }, "locate-path": { @@ -23864,6 +22031,12 @@ "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", "dev": true }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, "lodash.zip": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", @@ -23880,6 +22053,15 @@ "is-unicode-supported": "^0.1.0" }, "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -23889,6 +22071,36 @@ "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } } } }, @@ -24008,84 +22220,11 @@ "yargs-parser": "^18.1.3" }, "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, "type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, @@ -24138,16 +22277,6 @@ "picomatch": "^2.3.1" } }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - } - }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -24169,12 +22298,6 @@ "mime-db": "1.52.0" } }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -24218,9 +22341,9 @@ } }, "minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, "minimist-options": { @@ -24232,14 +22355,6 @@ "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", "kind-of": "^6.0.3" - }, - "dependencies": { - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true - } } }, "minipass": { @@ -24262,9 +22377,9 @@ } }, "mixme": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.4.tgz", - "integrity": "sha512-3KYa4m4Vlqx98GPdOHghxSdNtTvcP8E0kkaJ5Dlh+h2DRzF7zpuVVcA8B0QpKd11YJeP9QQ7ASkKzOeu195Wzw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.5.tgz", + "integrity": "sha512-/6IupbRx32s7jjEwHcycXikJwFD5UujbVNuJFkeKLYje+92OvtuPniF6JhnFm5JCTDUhS+kYK3W/4BWYQYXz7w==", "dev": true }, "mkdirp": { @@ -24352,6 +22467,12 @@ "wrap-ansi": "^7.0.0" } }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -24387,6 +22508,12 @@ } } }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -24498,6 +22625,12 @@ "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true } } }, @@ -24561,12 +22694,6 @@ } } }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", - "dev": true - }, "nano-base32": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/nano-base32/-/nano-base32-1.0.1.tgz", @@ -24615,12 +22742,6 @@ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "dev": true }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, "no-case": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", @@ -24664,18 +22785,18 @@ } }, "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", "dev": true, "requires": { "whatwg-url": "^5.0.0" } }, "node-gyp-build": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz", - "integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", "dev": true }, "node-interval-tree": { @@ -24688,9 +22809,9 @@ } }, "nofilter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz", - "integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", "dev": true }, "nopt": { @@ -24780,9 +22901,9 @@ "dev": true }, "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", "dev": true }, "object-keys": { @@ -24792,15 +22913,15 @@ "dev": true }, "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" } }, "object.getownpropertydescriptors": { @@ -24848,15 +22969,6 @@ "wrappy": "1" } }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -24972,19 +23084,6 @@ "callsites": "^3.0.0" } }, - "parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "dev": true, - "requires": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, "parse-cache-control": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", @@ -24998,13 +23097,15 @@ "dev": true }, "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "requires": { + "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" } }, "parse5": { @@ -25063,12 +23164,6 @@ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", - "dev": true - }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -25154,6 +23249,12 @@ "find-up": "^4.0.0" } }, + "pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true + }, "preferred-pm": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.0.3.tgz", @@ -25212,45 +23313,30 @@ "dev": true }, "prettier": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz", - "integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", + "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", "dev": true }, "prettier-plugin-solidity": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.1.tgz", - "integrity": "sha512-uD24KO26tAHF+zMN2nt1OUzfknzza5AgxjogQQrMLZc7j8xiQrDoNWNeOlfFC0YLTwo12CLD10b9niLyP6AqXg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.2.tgz", + "integrity": "sha512-KC5oNbFJfyBaFiO0kl56J6AXnDmr9tUlBV1iqo864x4KQrKYKaBZvW9jhT2oC0NHoNp7/GoMJNxqL8pp8k7C/g==", "dev": true, "requires": { - "@solidity-parser/parser": "^0.14.5", + "@solidity-parser/parser": "^0.15.0", "semver": "^7.3.8", "solidity-comments-extractor": "^0.0.7" }, "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "@solidity-parser/parser": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.15.0.tgz", + "integrity": "sha512-5UFJJTzWi1hgFk6aGCZ5rxG2DJkCJOzJ74qg7UkWSNCDSigW+CJLoYUb5bLiKrtI34Nr9rpFSUNHfkqtlL+N/w==", "dev": true, "requires": { - "yallist": "^4.0.0" + "antlr4ts": "^0.5.0-alpha.4" } - }, - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true } } }, @@ -25266,12 +23352,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, "promise": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", @@ -25314,20 +23394,6 @@ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -25377,9 +23443,9 @@ "dev": true }, "quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true }, "randombytes": { @@ -25391,16 +23457,6 @@ "safe-buffer": "^5.1.0" } }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -25408,9 +23464,9 @@ "dev": true }, "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, "requires": { "bytes": "3.1.2", @@ -25420,63 +23476,41 @@ } }, "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", "dev": true, "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" }, "dependencies": { - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", "dev": true } } }, "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", "dev": true, "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" }, "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -25490,20 +23524,12 @@ "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true - } } }, "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", + "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", "dev": true, "requires": { "inherits": "^2.0.3", @@ -25704,9 +23730,9 @@ "dev": true }, "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true }, "responselike": { @@ -25726,16 +23752,6 @@ } } }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, "retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -25806,12 +23822,6 @@ } } }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -25836,15 +23846,6 @@ "integrity": "sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==", "dev": true }, - "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -26239,38 +24240,44 @@ "dev": true }, "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, "dependencies": { "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" } }, "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "color-name": "1.1.3" + "color-name": "~1.1.4" } }, "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true } } @@ -26295,11 +24302,14 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } }, "cliui": { "version": "6.0.0", @@ -26312,10 +24322,19 @@ "wrap-ansi": "^6.2.0" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "is-fullwidth-code-point": { @@ -26379,16 +24398,6 @@ "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, @@ -26437,11 +24446,15 @@ "wrap-ansi": "^2.0.0" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } }, "fs-extra": { "version": "0.30.0", @@ -26494,6 +24507,53 @@ "graceful-fs": "^4.1.6" } }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, "require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", @@ -26592,304 +24652,99 @@ } }, "solhint": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/solhint/-/solhint-3.3.7.tgz", - "integrity": "sha512-NjjjVmXI3ehKkb3aNtRJWw55SUVJ8HMKKodwe0HnejA+k0d2kmhw7jvpa+MCTbcEgt8IWSwx0Hu6aCo/iYOZzQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/solhint/-/solhint-3.4.0.tgz", + "integrity": "sha512-FYEs/LoTxMsWFP/OGsEqR1CBDn3Bn7hrTWsgtjai17MzxITgearIdlo374KKZjjIycu8E2xBcJ+RSWeoBvQmkw==", "dev": true, "requires": { - "@solidity-parser/parser": "^0.14.1", - "ajv": "^6.6.1", - "antlr4": "4.7.1", - "ast-parents": "0.0.1", - "chalk": "^2.4.2", - "commander": "2.18.0", - "cosmiconfig": "^5.0.7", - "eslint": "^5.6.0", - "fast-diff": "^1.1.2", - "glob": "^7.1.3", - "ignore": "^4.0.6", - "js-yaml": "^3.12.0", - "lodash": "^4.17.11", - "prettier": "^1.14.3", - "semver": "^6.3.0" + "@solidity-parser/parser": "^0.15.0", + "ajv": "^6.12.6", + "antlr4": "^4.11.0", + "ast-parents": "^0.0.1", + "chalk": "^4.1.2", + "commander": "^10.0.0", + "cosmiconfig": "^8.0.0", + "fast-diff": "^1.2.0", + "glob": "^8.0.3", + "ignore": "^5.2.4", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "pluralize": "^8.0.0", + "prettier": "^2.8.3", + "semver": "^6.3.0", + "strip-ansi": "^6.0.1", + "table": "^6.8.1", + "text-table": "^0.2.0" }, "dependencies": { - "acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "@solidity-parser/parser": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.15.0.tgz", + "integrity": "sha512-5UFJJTzWi1hgFk6aGCZ5rxG2DJkCJOzJ74qg7UkWSNCDSigW+CJLoYUb5bLiKrtI34Nr9rpFSUNHfkqtlL+N/w==", + "dev": true, + "requires": { + "antlr4ts": "^0.5.0-alpha.4" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" } }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "color-name": "1.1.3" + "color-name": "~1.1.4" } }, "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "eslint": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", - "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.9.1", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^4.0.3", - "eslint-utils": "^1.3.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^5.0.1", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.7.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^6.2.2", - "js-yaml": "^3.13.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.11", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^5.5.1", - "strip-ansi": "^4.0.0", - "strip-json-comments": "^2.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - }, - "espree": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", - "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", - "dev": true, - "requires": { - "acorn": "^6.0.7", - "acorn-jsx": "^5.0.0", - "eslint-visitor-keys": "^1.0.0" - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "requires": { - "flat-cache": "^2.0.1" - } - }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - } - }, - "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true - }, - "prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", - "dev": true, - "optional": true - }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" + "argparse": "^2.0.1" } }, "semver": { @@ -26898,60 +24753,30 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "shebang-regex": "^1.0.0" + "ansi-regex": "^5.0.1" } }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true - }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { - "has-flag": "^3.0.0" - } - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" + "has-flag": "^4.0.0" } } } }, "solidity-ast": { - "version": "0.4.40", - "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.40.tgz", - "integrity": "sha512-M8uLBT2jgFB7B0iVAC5a2l71J8vim7aEm03AZkaHbDqyrl1pE+i5PriMEw6WlwGfHp3/Ym7cn9BqvVLQgRk+Yw==", + "version": "0.4.46", + "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.46.tgz", + "integrity": "sha512-MlPZQfPhjWXqh7YxWcBGDXaPZIfMYCOHYoLEhGDWulNwEPIQQZuB7mA9eP17CU0jY/bGR4avCEUVVpvHtT2gbA==", "dev": true }, "solidity-comments": { @@ -27088,32 +24913,12 @@ "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, "chokidar": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", @@ -27141,21 +24946,6 @@ "wrap-ansi": "^5.1.0" } }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -27165,12 +24955,6 @@ "ms": "^2.1.1" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true - }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -27183,12 +24967,6 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -27226,24 +25004,34 @@ "optional": true }, "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true + "globby": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", + "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + } }, "js-yaml": { "version": "3.13.1", @@ -27274,15 +25062,6 @@ "chalk": "^2.4.2" } }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", @@ -27324,13 +25103,27 @@ "yargs-unparser": "1.6.0" }, "dependencies": { - "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" } } } @@ -27341,6 +25134,18 @@ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, "p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -27392,9 +25197,9 @@ "dev": true }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", "dev": true, "requires": { "has-flag": "^3.0.0" @@ -27732,13 +25537,10 @@ } }, "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true }, "strip-hex-prefix": { "version": "1.0.0", @@ -27762,12 +25564,12 @@ "dev": true }, "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { - "has-flag": "^4.0.0" + "has-flag": "^3.0.0" } }, "swap-case": { @@ -27865,6 +25667,12 @@ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "dev": true + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true } } }, @@ -27889,47 +25697,72 @@ } }, "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", "dev": true, "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" }, "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.1" } } } @@ -27994,12 +25827,6 @@ } } }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, "timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", @@ -28051,9 +25878,9 @@ }, "dependencies": { "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true } } @@ -28109,6 +25936,15 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -28119,6 +25955,27 @@ "supports-color": "^7.1.0" } }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -28127,6 +25984,15 @@ "requires": { "ansi-regex": "^5.0.1" } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } } } }, @@ -28188,6 +26054,17 @@ "mime-types": "~2.1.24" } }, + "typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -28235,9 +26112,9 @@ "dev": true }, "undici": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.14.0.tgz", - "integrity": "sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.20.0.tgz", + "integrity": "sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==", "dev": true, "requires": { "busboy": "^1.6.0" @@ -28374,60 +26251,46 @@ } }, "web3": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.8.1.tgz", - "integrity": "sha512-tAqFsQhGv340C9OgRJIuoScN7f7wa1tUvsnnDUMt9YE6J4gcm7TV2Uwv+KERnzvV+xgdeuULYpsioRRNKrUvoQ==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.8.2.tgz", + "integrity": "sha512-92h0GdEHW9wqDICQQKyG4foZBYi0OQkyg4CRml2F7XBl/NG+fu9o6J19kzfFXzSBoA4DnJXbyRgj/RHZv5LRiw==", "dev": true, "requires": { - "web3-bzz": "1.8.1", - "web3-core": "1.8.1", - "web3-eth": "1.8.1", - "web3-eth-personal": "1.8.1", - "web3-net": "1.8.1", - "web3-shh": "1.8.1", - "web3-utils": "1.8.1" + "web3-bzz": "1.8.2", + "web3-core": "1.8.2", + "web3-eth": "1.8.2", + "web3-eth-personal": "1.8.2", + "web3-net": "1.8.2", + "web3-shh": "1.8.2", + "web3-utils": "1.8.2" } }, "web3-bzz": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.8.1.tgz", - "integrity": "sha512-dJJHS84nvpoxv6ijTMkdUSlRr5beCXNtx4UZcrFLHBva8dT63QEtKdLyDt2AyMJJdVzTCk78uir/6XtVWrdS6w==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.8.2.tgz", + "integrity": "sha512-1EEnxjPnFnvNWw3XeeKuTR8PBxYd0+XWzvaLK7OJC/Go9O8llLGxrxICbKV+8cgIE0sDRBxiYx02X+6OhoAQ9w==", "dev": true, "requires": { "@types/node": "^12.12.6", "got": "12.1.0", "swarm-js": "^0.1.40" - }, - "dependencies": { - "@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - } } }, "web3-core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.8.1.tgz", - "integrity": "sha512-LbRZlJH2N6nS3n3Eo9Y++25IvzMY7WvYnp4NM/Ajhh97dAdglYs6rToQ2DbL2RLvTYmTew4O/y9WmOk4nq9COw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.8.2.tgz", + "integrity": "sha512-DJTVEAYcNqxkqruJE+Rxp3CIv0y5AZMwPHQmOkz/cz+MM75SIzMTc0AUdXzGyTS8xMF8h3YWMQGgGEy8SBf1PQ==", "dev": true, "requires": { "@types/bn.js": "^5.1.0", "@types/node": "^12.12.6", "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-core-requestmanager": "1.8.1", - "web3-utils": "1.8.1" + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-requestmanager": "1.8.2", + "web3-utils": "1.8.2" }, "dependencies": { - "@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - }, "bignumber.js": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", @@ -28437,107 +26300,106 @@ } }, "web3-core-helpers": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.8.1.tgz", - "integrity": "sha512-ClzNO6T1S1gifC+BThw0+GTfcsjLEY8T1qUp6Ly2+w4PntAdNtKahxWKApWJ0l9idqot/fFIDXwO3Euu7I0Xqw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.8.2.tgz", + "integrity": "sha512-6B1eLlq9JFrfealZBomd1fmlq1o4A09vrCVQSa51ANoib/jllT3atZrRDr0zt1rfI7TSZTZBXdN/aTdeN99DWw==", "dev": true, "requires": { - "web3-eth-iban": "1.8.1", - "web3-utils": "1.8.1" + "web3-eth-iban": "1.8.2", + "web3-utils": "1.8.2" } }, "web3-core-method": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.8.1.tgz", - "integrity": "sha512-oYGRodktfs86NrnFwaWTbv2S38JnpPslFwSSARwFv4W9cjbGUW3LDeA5MKD/dRY+ssZ5OaekeMsUCLoGhX68yA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.8.2.tgz", + "integrity": "sha512-1qnr5mw5wVyULzLOrk4B+ryO3gfGjGd/fx8NR+J2xCGLf1e6OSjxT9vbfuQ3fErk/NjSTWWreieYWLMhaogcRA==", "dev": true, "requires": { "@ethersproject/transactions": "^5.6.2", - "web3-core-helpers": "1.8.1", - "web3-core-promievent": "1.8.1", - "web3-core-subscriptions": "1.8.1", - "web3-utils": "1.8.1" + "web3-core-helpers": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-utils": "1.8.2" } }, "web3-core-promievent": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.8.1.tgz", - "integrity": "sha512-9mxqHlgB0MrZI4oUIRFkuoJMNj3E7btjrMv3sMer/Z9rYR1PfoSc1aAokw4rxKIcAh+ylVtd/acaB2HKB7aRPg==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.8.2.tgz", + "integrity": "sha512-nvkJWDVgoOSsolJldN33tKW6bKKRJX3MCPDYMwP5SUFOA/mCzDEoI88N0JFofDTXkh1k7gOqp1pvwi9heuaxGg==", "dev": true, "requires": { "eventemitter3": "4.0.4" } }, "web3-core-requestmanager": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.8.1.tgz", - "integrity": "sha512-x+VC2YPPwZ1khvqA6TA69LvfFCOZXsoUVOxmTx/vIN22PrY9KzKhxcE7pBSiGhmab1jtmRYXUbcQSVpAXqL8cw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.8.2.tgz", + "integrity": "sha512-p1d090RYs5Mu7DK1yyc3GCBVZB/03rBtFhYFoS2EruGzOWs/5Q0grgtpwS/DScdRAm8wB8mYEBhY/RKJWF6B2g==", "dev": true, "requires": { - "util": "^0.12.0", - "web3-core-helpers": "1.8.1", - "web3-providers-http": "1.8.1", - "web3-providers-ipc": "1.8.1", - "web3-providers-ws": "1.8.1" + "util": "^0.12.5", + "web3-core-helpers": "1.8.2", + "web3-providers-http": "1.8.2", + "web3-providers-ipc": "1.8.2", + "web3-providers-ws": "1.8.2" } }, "web3-core-subscriptions": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.8.1.tgz", - "integrity": "sha512-bmCMq5OeA3E2vZUh8Js1HcJbhwtsE+yeMqGC4oIZB3XsL5SLqyKLB/pU+qUYqQ9o4GdcrFTDPhPg1bgvf7p1Pw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.8.2.tgz", + "integrity": "sha512-vXQogHDmAIQcKpXvGiMddBUeP9lnKgYF64+yQJhPNE5PnWr1sAibXuIPV7mIPihpFr/n/DORRj6Wh1pUv9zaTw==", "dev": true, "requires": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.1" + "web3-core-helpers": "1.8.2" } }, "web3-eth": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.8.1.tgz", - "integrity": "sha512-LgyzbhFqiFRd8M8sBXoFN4ztzOnkeckl3H/9lH5ek7AdoRMhBg7tYpYRP3E5qkhd/q+yiZmcUgy1AF6NHrC1wg==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.8.2.tgz", + "integrity": "sha512-JoTiWWc4F4TInpbvDUGb0WgDYJsFhuIjJlinc5ByjWD88Gvh+GKLsRjjFdbqe5YtwIGT4NymwoC5LQd1K6u/QQ==", "dev": true, "requires": { - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-core-subscriptions": "1.8.1", - "web3-eth-abi": "1.8.1", - "web3-eth-accounts": "1.8.1", - "web3-eth-contract": "1.8.1", - "web3-eth-ens": "1.8.1", - "web3-eth-iban": "1.8.1", - "web3-eth-personal": "1.8.1", - "web3-net": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-eth-accounts": "1.8.2", + "web3-eth-contract": "1.8.2", + "web3-eth-ens": "1.8.2", + "web3-eth-iban": "1.8.2", + "web3-eth-personal": "1.8.2", + "web3-net": "1.8.2", + "web3-utils": "1.8.2" } }, "web3-eth-abi": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.8.1.tgz", - "integrity": "sha512-0mZvCRTIG0UhDhJwNQJgJxu4b4DyIpuMA0GTfqxqeuqzX4Q/ZvmoNurw0ExTfXaGPP82UUmmdkRi6FdZOx+C6w==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.8.2.tgz", + "integrity": "sha512-Om9g3kaRNjqiNPAgKwGT16y+ZwtBzRe4ZJFGjLiSs6v5I7TPNF+rRMWuKnR6jq0azQZDj6rblvKFMA49/k48Og==", "dev": true, "requires": { "@ethersproject/abi": "^5.6.3", - "web3-utils": "1.8.1" + "web3-utils": "1.8.2" } }, "web3-eth-accounts": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.8.1.tgz", - "integrity": "sha512-mgzxSYgN54/NsOFBO1Fq1KkXp1S5KlBvI/DlgvajU72rupoFMq6Cu6Yp9GUaZ/w2ij9PzEJuFJk174XwtfMCmg==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.8.2.tgz", + "integrity": "sha512-c367Ij63VCz9YdyjiHHWLFtN85l6QghgwMQH2B1eM/p9Y5lTlTX7t/Eg/8+f1yoIStXbk2w/PYM2lk+IkbqdLA==", "dev": true, "requires": { "@ethereumjs/common": "2.5.0", "@ethereumjs/tx": "3.3.2", - "crypto-browserify": "3.12.0", "eth-lib": "0.2.8", - "ethereumjs-util": "^7.0.10", + "ethereumjs-util": "^7.1.5", "scrypt-js": "^3.0.1", "uuid": "^9.0.0", - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-utils": "1.8.2" }, "dependencies": { "eth-lib": { @@ -28560,45 +26422,45 @@ } }, "web3-eth-contract": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.8.1.tgz", - "integrity": "sha512-1wphnl+/xwCE2io44JKnN+ti3oa47BKRiVzvWd42icwRbcpFfRxH9QH+aQX3u8VZIISNH7dAkTWpGIIJgGFTmg==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.8.2.tgz", + "integrity": "sha512-ID5A25tHTSBNwOPjiXSVzxruz006ULRIDbzWTYIFTp7NJ7vXu/kynKK2ag/ObuTqBpMbobP8nXcA9b5EDkIdQA==", "dev": true, "requires": { "@types/bn.js": "^5.1.0", - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-core-promievent": "1.8.1", - "web3-core-subscriptions": "1.8.1", - "web3-eth-abi": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-utils": "1.8.2" } }, "web3-eth-ens": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.8.1.tgz", - "integrity": "sha512-FT8xTI9uN8RxeBQa/W8pLa2aoFh4+EE34w7W2271LICKzla1dtLyb6XSdn48vsUcPmhWsTVk9mO9RTU0l4LGQQ==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.8.2.tgz", + "integrity": "sha512-PWph7C/CnqdWuu1+SH4U4zdrK4t2HNt0I4XzPYFdv9ugE8EuojselioPQXsVGvjql+Nt3jDLvQvggPqlMbvwRw==", "dev": true, "requires": { "content-hash": "^2.5.2", "eth-ens-namehash": "2.0.8", - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-promievent": "1.8.1", - "web3-eth-abi": "1.8.1", - "web3-eth-contract": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-eth-contract": "1.8.2", + "web3-utils": "1.8.2" } }, "web3-eth-iban": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.8.1.tgz", - "integrity": "sha512-DomoQBfvIdtM08RyMGkMVBOH0vpOIxSSQ+jukWk/EkMLGMWJtXw/K2c2uHAeq3L/VPWNB7zXV2DUEGV/lNE2Dg==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.8.2.tgz", + "integrity": "sha512-h3vNblDWkWMuYx93Q27TAJz6lhzpP93EiC3+45D6xoz983p6si773vntoQ+H+5aZhwglBtoiBzdh7PSSOnP/xQ==", "dev": true, "requires": { "bn.js": "^5.2.1", - "web3-utils": "1.8.1" + "web3-utils": "1.8.2" }, "dependencies": { "bn.js": { @@ -28610,87 +26472,79 @@ } }, "web3-eth-personal": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.8.1.tgz", - "integrity": "sha512-myIYMvj7SDIoV9vE5BkVdon3pya1WinaXItugoii2VoTcQNPOtBxmYVH+XS5ErzCJlnxzphpQrkywyY64bbbCA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.8.2.tgz", + "integrity": "sha512-Vg4HfwCr7doiUF/RC+Jz0wT4+cYaXcOWMAW2AHIjHX6Z7Xwa8nrURIeQgeEE62qcEHAzajyAdB1u6bJyTfuCXw==", "dev": true, "requires": { "@types/node": "^12.12.6", - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-net": "1.8.1", - "web3-utils": "1.8.1" - }, - "dependencies": { - "@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - } + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-net": "1.8.2", + "web3-utils": "1.8.2" } }, "web3-net": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.8.1.tgz", - "integrity": "sha512-LyEJAwogdFo0UAXZqoSJGFjopdt+kLw0P00FSZn2yszbgcoI7EwC+nXiOsEe12xz4LqpYLOtbR7+gxgiTVjjHQ==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.8.2.tgz", + "integrity": "sha512-1itkDMGmbgb83Dg9nporFes9/fxsU7smJ3oRXlFkg4ZHn8YJyP1MSQFPJWWwSc+GrcCFt4O5IrUTvEkHqE3xag==", "dev": true, "requires": { - "web3-core": "1.8.1", - "web3-core-method": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.8.2", + "web3-core-method": "1.8.2", + "web3-utils": "1.8.2" } }, "web3-providers-http": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.8.1.tgz", - "integrity": "sha512-1Zyts4O9W/UNEPkp+jyL19Jc3D15S4yp8xuLTjVhcUEAlHo24NDWEKxtZGUuHk4HrKL2gp8OlsDbJ7MM+ESDgg==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.8.2.tgz", + "integrity": "sha512-2xY94IIEQd16+b+vIBF4IC1p7GVaz9q4EUFscvMUjtEq4ru4Atdzjs9GP+jmcoo49p70II0UV3bqQcz0TQfVyQ==", "dev": true, "requires": { "abortcontroller-polyfill": "^1.7.3", "cross-fetch": "^3.1.4", "es6-promise": "^4.2.8", - "web3-core-helpers": "1.8.1" + "web3-core-helpers": "1.8.2" } }, "web3-providers-ipc": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.8.1.tgz", - "integrity": "sha512-nw/W5nclvi+P2z2dYkLWReKLnocStflWqFl+qjtv0xn3MrUTyXMzSF0+61i77+16xFsTgzo4wS/NWIOVkR0EFA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.8.2.tgz", + "integrity": "sha512-p6fqKVGFg+WiXGHWnB1hu43PbvPkDHTz4RgoEzbXugv5rtv5zfYLqm8Ba6lrJOS5ks9kGKR21a0y3NzE3u7V4w==", "dev": true, "requires": { "oboe": "2.1.5", - "web3-core-helpers": "1.8.1" + "web3-core-helpers": "1.8.2" } }, "web3-providers-ws": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.8.1.tgz", - "integrity": "sha512-TNefIDAMpdx57+YdWpYZ/xdofS0P+FfKaDYXhn24ie/tH9G+AB+UBSOKnjN0KSadcRSCMBwGPRiEmNHPavZdsA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.8.2.tgz", + "integrity": "sha512-3s/4K+wHgbiN+Zrp9YjMq2eqAF6QGABw7wFftPdx+m5hWImV27/MoIx57c6HffNRqZXmCHnfWWFCNHHsi7wXnA==", "dev": true, "requires": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.1", + "web3-core-helpers": "1.8.2", "websocket": "^1.0.32" } }, "web3-shh": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.8.1.tgz", - "integrity": "sha512-sqHgarnfcY2Qt3PYS4R6YveHrDy7hmL09yeLLHHCI+RKirmjLVqV0rc5LJWUtlbYI+kDoa5gbgde489M9ZAC0g==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.8.2.tgz", + "integrity": "sha512-uZ+3MAoNcaJsXXNCDnizKJ5viBNeHOFYsCbFhV755Uu52FswzTOw6DtE7yK9nYXMtIhiSgi7nwl1RYzP8pystw==", "dev": true, "requires": { - "web3-core": "1.8.1", - "web3-core-method": "1.8.1", - "web3-core-subscriptions": "1.8.1", - "web3-net": "1.8.1" + "web3-core": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-net": "1.8.2" } }, "web3-utils": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.1.tgz", - "integrity": "sha512-LgnM9p6V7rHHUGfpMZod+NST8cRfGzJ1BTXAyNo7A9cJX9LczBfSRxJp+U/GInYe9mby40t3v22AJdlELibnsQ==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", + "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", "dev": true, "requires": { "bn.js": "^5.2.1", @@ -28859,6 +26713,30 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -28893,15 +26771,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, "ws": { "version": "7.5.9", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", @@ -28976,9 +26845,9 @@ "dev": true }, "yargs": { - "version": "17.6.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", - "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", "dev": true, "requires": { "cliui": "^8.0.1", @@ -29031,10 +26900,22 @@ } }, "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } }, "yargs-unparser": { "version": "2.0.0", @@ -29053,6 +26934,18 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true } } }, From 8b47e96af1f3fe06fb581820bbb28d51a0abd4f6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 24 Feb 2023 21:29:49 +0100 Subject: [PATCH 060/197] Add SafeERC20.forceApprove() (#4067) --- .changeset/small-terms-sleep.md | 5 + .../mocks/token/ERC20ForceApproveMock.sol | 13 ++ contracts/mocks/token/ERC20NoReturnMock.sol | 31 ++-- .../mocks/token/ERC20PermitNoRevertMock.sol | 4 +- .../mocks/token/ERC20ReturnFalseMock.sol | 18 +- contracts/mocks/token/ERC20ReturnTrueMock.sol | 27 --- contracts/token/ERC20/utils/SafeERC20.sol | 64 ++++++- package-lock.json | 2 +- package.json | 2 +- test/token/ERC20/utils/SafeERC20.test.js | 164 +++++++++++++----- 10 files changed, 226 insertions(+), 104 deletions(-) create mode 100644 .changeset/small-terms-sleep.md create mode 100644 contracts/mocks/token/ERC20ForceApproveMock.sol delete mode 100644 contracts/mocks/token/ERC20ReturnTrueMock.sol diff --git a/.changeset/small-terms-sleep.md b/.changeset/small-terms-sleep.md new file mode 100644 index 000000000..ed184a1c4 --- /dev/null +++ b/.changeset/small-terms-sleep.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`SafeERC20`: Add a `forceApprove` function to improve compatibility with tokens behaving like USDT. diff --git a/contracts/mocks/token/ERC20ForceApproveMock.sol b/contracts/mocks/token/ERC20ForceApproveMock.sol new file mode 100644 index 000000000..955224bcf --- /dev/null +++ b/contracts/mocks/token/ERC20ForceApproveMock.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../../token/ERC20/ERC20.sol"; + +// contract that replicate USDT (0xdac17f958d2ee523a2206206994597c13d831ec7) approval beavior +abstract contract ERC20ForceApproveMock is ERC20 { + function approve(address spender, uint256 amount) public virtual override returns (bool) { + require(amount == 0 || allowance(msg.sender, spender) == 0, "USDT approval failure"); + return super.approve(spender, amount); + } +} diff --git a/contracts/mocks/token/ERC20NoReturnMock.sol b/contracts/mocks/token/ERC20NoReturnMock.sol index 023dab2cb..348c0d6bb 100644 --- a/contracts/mocks/token/ERC20NoReturnMock.sol +++ b/contracts/mocks/token/ERC20NoReturnMock.sol @@ -2,20 +2,27 @@ pragma solidity ^0.8.0; -contract ERC20NoReturnMock { - mapping(address => uint256) private _allowances; +import "../../token/ERC20/ERC20.sol"; - function transfer(address, uint256) public {} - - function transferFrom(address, address, uint256) public {} - - function approve(address, uint256) public {} - - function setAllowance(address account, uint256 allowance_) public { - _allowances[account] = allowance_; +abstract contract ERC20NoReturnMock is ERC20 { + function transfer(address to, uint256 amount) public override returns (bool) { + super.transfer(to, amount); + assembly { + return(0, 0) + } } - function allowance(address owner, address) public view returns (uint256) { - return _allowances[owner]; + function transferFrom(address from, address to, uint256 amount) public override returns (bool) { + super.transferFrom(from, to, amount); + assembly { + return(0, 0) + } + } + + function approve(address spender, uint256 amount) public override returns (bool) { + super.approve(spender, amount); + assembly { + return(0, 0) + } } } diff --git a/contracts/mocks/token/ERC20PermitNoRevertMock.sol b/contracts/mocks/token/ERC20PermitNoRevertMock.sol index 2b6e2503b..2176c51ae 100644 --- a/contracts/mocks/token/ERC20PermitNoRevertMock.sol +++ b/contracts/mocks/token/ERC20PermitNoRevertMock.sol @@ -5,9 +5,7 @@ pragma solidity ^0.8.0; import "../../token/ERC20/ERC20.sol"; import "../../token/ERC20/extensions/draft-ERC20Permit.sol"; -contract ERC20PermitNoRevertMock is ERC20, ERC20Permit { - constructor() ERC20("ERC20PermitNoRevertMock", "ERC20PermitNoRevertMock") ERC20Permit("ERC20PermitNoRevertMock") {} - +abstract contract ERC20PermitNoRevertMock is ERC20Permit { function permitThatMayRevert( address owner, address spender, diff --git a/contracts/mocks/token/ERC20ReturnFalseMock.sol b/contracts/mocks/token/ERC20ReturnFalseMock.sol index 3f8f64037..c4dc6921f 100644 --- a/contracts/mocks/token/ERC20ReturnFalseMock.sol +++ b/contracts/mocks/token/ERC20ReturnFalseMock.sol @@ -2,26 +2,18 @@ pragma solidity ^0.8.0; -contract ERC20ReturnFalseMock { - mapping(address => uint256) private _allowances; +import "../../token/ERC20/ERC20.sol"; - function transfer(address, uint256) public pure returns (bool) { +abstract contract ERC20ReturnFalseMock is ERC20 { + function transfer(address, uint256) public pure override returns (bool) { return false; } - function transferFrom(address, address, uint256) public pure returns (bool) { + function transferFrom(address, address, uint256) public pure override returns (bool) { return false; } - function approve(address, uint256) public pure returns (bool) { + function approve(address, uint256) public pure override returns (bool) { return false; } - - function setAllowance(address account, uint256 allowance_) public { - _allowances[account] = allowance_; - } - - function allowance(address owner, address) public view returns (uint256) { - return _allowances[owner]; - } } diff --git a/contracts/mocks/token/ERC20ReturnTrueMock.sol b/contracts/mocks/token/ERC20ReturnTrueMock.sol deleted file mode 100644 index 8953aad64..000000000 --- a/contracts/mocks/token/ERC20ReturnTrueMock.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -contract ERC20ReturnTrueMock { - mapping(address => uint256) private _allowances; - - function transfer(address, uint256) public pure returns (bool) { - return true; - } - - function transferFrom(address, address, uint256) public pure returns (bool) { - return true; - } - - function approve(address, uint256) public pure returns (bool) { - return true; - } - - function setAllowance(address account, uint256 allowance_) public { - _allowances[account] = allowance_; - } - - function allowance(address owner, address) public view returns (uint256) { - return _allowances[owner]; - } -} diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index 028711ddf..2f76386d5 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -19,10 +19,18 @@ import "../../../utils/Address.sol"; library SafeERC20 { using Address for address; + /** + * @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. + */ function safeTransfer(IERC20 token, address to, uint256 value) internal { _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); } + /** + * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the + * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful. + */ function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); } @@ -45,20 +53,45 @@ library SafeERC20 { _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); } + /** + * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value, + * non-reverting calls are assumed to be successful. + */ function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { - uint256 newAllowance = token.allowance(address(this), spender) + value; - _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); + uint256 oldAllowance = token.allowance(address(this), spender); + _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance + value)); } + /** + * @dev Decrease the calling contract's allowance toward `spender` by `value`. If `token` returns no value, + * non-reverting calls are assumed to be successful. + */ function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal { unchecked { uint256 oldAllowance = token.allowance(address(this), spender); require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); - uint256 newAllowance = oldAllowance - value; - _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); + _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance - value)); } } + /** + * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value, + * non-reverting calls are assumed to be successful. Compatible with tokens that require the approval to be set to + * 0 before setting it to a non-zero value. + */ + function forceApprove(IERC20 token, address spender, uint256 value) internal { + bytes memory approvalCall = abi.encodeWithSelector(token.approve.selector, spender, value); + + if (!_callOptionalReturnBool(token, approvalCall)) { + _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, 0)); + _callOptionalReturn(token, approvalCall); + } + } + + /** + * @dev Use a ERC-2612 signature to set the `owner` approval toward `spender` on `token`. + * Revert on invalid signature. + */ function safePermit( IERC20Permit token, address owner, @@ -87,9 +120,24 @@ library SafeERC20 { // 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"); - if (returndata.length > 0) { - // Return data is optional - require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); - } + require(returndata.length == 0 || abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + * + * This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead. + */ + function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false + // and not revert is the subcall reverts. + + (bool success, bytes memory returndata) = address(token).call(data); + return + success && (returndata.length == 0 || abi.decode(returndata, (bool))) && Address.isContract(address(token)); } } diff --git a/package-lock.json b/package-lock.json index 87fd3a795..fc3918807 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "glob": "^8.0.3", "graphlib": "^2.1.8", "hardhat": "^2.9.1", - "hardhat-exposed": "^0.3.1", + "hardhat-exposed": "^0.3.2", "hardhat-gas-reporter": "^1.0.4", "hardhat-ignore-warnings": "^0.2.0", "keccak256": "^1.0.2", diff --git a/package.json b/package.json index 66d44888a..f0ee95ab0 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "glob": "^8.0.3", "graphlib": "^2.1.8", "hardhat": "^2.9.1", - "hardhat-exposed": "^0.3.1", + "hardhat-exposed": "^0.3.2", "hardhat-gas-reporter": "^1.0.4", "hardhat-ignore-warnings": "^0.2.0", "keccak256": "^1.0.2", diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index a6b10fae3..d4981dd30 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -1,10 +1,11 @@ -const { constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const SafeERC20 = artifacts.require('$SafeERC20'); -const ERC20ReturnFalseMock = artifacts.require('ERC20ReturnFalseMock'); -const ERC20ReturnTrueMock = artifacts.require('ERC20ReturnTrueMock'); -const ERC20NoReturnMock = artifacts.require('ERC20NoReturnMock'); -const ERC20PermitNoRevertMock = artifacts.require('ERC20PermitNoRevertMock'); +const ERC20ReturnFalseMock = artifacts.require('$ERC20ReturnFalseMock'); +const ERC20ReturnTrueMock = artifacts.require('$ERC20'); // default implementation returns true +const ERC20NoReturnMock = artifacts.require('$ERC20NoReturnMock'); +const ERC20PermitNoRevertMock = artifacts.require('$ERC20PermitNoRevertMock'); +const ERC20ForceApproveMock = artifacts.require('$ERC20ForceApproveMock'); const { getDomain, domainType, Permit } = require('../../../helpers/eip712'); @@ -12,6 +13,9 @@ const { fromRpcSig } = require('ethereumjs-util'); const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; +const name = 'ERC20Mock'; +const symbol = 'ERC20Mock'; + contract('SafeERC20', function (accounts) { const [hasNoCode] = accounts; @@ -24,31 +28,31 @@ contract('SafeERC20', function (accounts) { this.token = { address: hasNoCode }; }); - shouldRevertOnAllCalls('Address: call to non-contract'); + shouldRevertOnAllCalls(accounts, 'Address: call to non-contract'); }); describe('with token that returns false on all calls', function () { beforeEach(async function () { - this.token = await ERC20ReturnFalseMock.new(); + this.token = await ERC20ReturnFalseMock.new(name, symbol); }); - shouldRevertOnAllCalls('SafeERC20: ERC20 operation did not succeed'); + shouldRevertOnAllCalls(accounts, 'SafeERC20: ERC20 operation did not succeed'); }); describe('with token that returns true on all calls', function () { beforeEach(async function () { - this.token = await ERC20ReturnTrueMock.new(); + this.token = await ERC20ReturnTrueMock.new(name, symbol); }); - shouldOnlyRevertOnErrors(); + shouldOnlyRevertOnErrors(accounts); }); describe('with token that returns no boolean values', function () { beforeEach(async function () { - this.token = await ERC20NoReturnMock.new(); + this.token = await ERC20NoReturnMock.new(name, symbol); }); - shouldOnlyRevertOnErrors(); + shouldOnlyRevertOnErrors(accounts); }); describe("with token that doesn't revert on invalid permit", function () { @@ -57,7 +61,7 @@ contract('SafeERC20', function (accounts) { const spender = hasNoCode; beforeEach(async function () { - this.token = await ERC20PermitNoRevertMock.new(); + this.token = await ERC20PermitNoRevertMock.new(name, symbol, name); this.data = await getDomain(this.token).then(domain => ({ primaryType: 'Permit', @@ -165,65 +169,134 @@ contract('SafeERC20', function (accounts) { ); }); }); + + describe('with usdt approval beaviour', function () { + const spender = hasNoCode; + + beforeEach(async function () { + this.token = await ERC20ForceApproveMock.new(name, symbol); + }); + + describe('with initial approval', function () { + beforeEach(async function () { + await this.token.$_approve(this.mock.address, spender, 100); + }); + + it('safeApprove fails to update approval to non-zero', async function () { + await expectRevert( + this.mock.$safeApprove(this.token.address, spender, 200), + 'SafeERC20: approve from non-zero to non-zero allowance', + ); + }); + + it('safeApprove can update approval to zero', async function () { + await this.mock.$safeApprove(this.token.address, spender, 0); + }); + + it('safeApprove can increase approval', async function () { + await expectRevert(this.mock.$safeIncreaseAllowance(this.token.address, spender, 10), 'USDT approval failure'); + }); + + it('safeApprove can decrease approval', async function () { + await expectRevert(this.mock.$safeDecreaseAllowance(this.token.address, spender, 10), 'USDT approval failure'); + }); + + it('forceApprove works', async function () { + await this.mock.$forceApprove(this.token.address, spender, 200); + }); + }); + }); }); -function shouldRevertOnAllCalls(reason) { +function shouldRevertOnAllCalls([receiver, spender], reason) { it('reverts on transfer', async function () { - await expectRevert(this.mock.$safeTransfer(this.token.address, constants.ZERO_ADDRESS, 0), reason); + await expectRevert(this.mock.$safeTransfer(this.token.address, receiver, 0), reason); }); it('reverts on transferFrom', async function () { - await expectRevert( - this.mock.$safeTransferFrom(this.token.address, this.mock.address, constants.ZERO_ADDRESS, 0), - reason, - ); + await expectRevert(this.mock.$safeTransferFrom(this.token.address, this.mock.address, receiver, 0), reason); }); it('reverts on approve', async function () { - await expectRevert(this.mock.$safeApprove(this.token.address, constants.ZERO_ADDRESS, 0), reason); + await expectRevert(this.mock.$safeApprove(this.token.address, spender, 0), reason); }); it('reverts on increaseAllowance', async function () { // [TODO] make sure it's reverting for the right reason - await expectRevert.unspecified(this.mock.$safeIncreaseAllowance(this.token.address, constants.ZERO_ADDRESS, 0)); + await expectRevert.unspecified(this.mock.$safeIncreaseAllowance(this.token.address, spender, 0)); }); it('reverts on decreaseAllowance', async function () { // [TODO] make sure it's reverting for the right reason - await expectRevert.unspecified(this.mock.$safeDecreaseAllowance(this.token.address, constants.ZERO_ADDRESS, 0)); + await expectRevert.unspecified(this.mock.$safeDecreaseAllowance(this.token.address, spender, 0)); + }); + + it('reverts on forceApprove', async function () { + await expectRevert(this.mock.$forceApprove(this.token.address, spender, 0), reason); }); } -function shouldOnlyRevertOnErrors() { - it("doesn't revert on transfer", async function () { - await this.mock.$safeTransfer(this.token.address, constants.ZERO_ADDRESS, 0); - }); +function shouldOnlyRevertOnErrors([owner, receiver, spender]) { + describe('transfers', function () { + beforeEach(async function () { + await this.token.$_mint(owner, 100); + await this.token.$_mint(this.mock.address, 100); + await this.token.approve(this.mock.address, constants.MAX_UINT256, { from: owner }); + }); - it("doesn't revert on transferFrom", async function () { - await this.mock.$safeTransferFrom(this.token.address, this.mock.address, constants.ZERO_ADDRESS, 0); + it("doesn't revert on transfer", async function () { + const { tx } = await this.mock.$safeTransfer(this.token.address, receiver, 10); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: this.mock.address, + to: receiver, + value: '10', + }); + }); + + it("doesn't revert on transferFrom", async function () { + const { tx } = await this.mock.$safeTransferFrom(this.token.address, owner, receiver, 10); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: owner, + to: receiver, + value: '10', + }); + }); }); describe('approvals', function () { context('with zero allowance', function () { beforeEach(async function () { - await this.token.setAllowance(this.mock.address, 0); + await this.token.$_approve(this.mock.address, spender, 0); }); it("doesn't revert when approving a non-zero allowance", async function () { - await this.mock.$safeApprove(this.token.address, constants.ZERO_ADDRESS, 100); + await this.mock.$safeApprove(this.token.address, spender, 100); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('100'); }); it("doesn't revert when approving a zero allowance", async function () { - await this.mock.$safeApprove(this.token.address, constants.ZERO_ADDRESS, 0); + await this.mock.$safeApprove(this.token.address, spender, 0); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0'); + }); + + it("doesn't revert when force approving a non-zero allowance", async function () { + await this.mock.$forceApprove(this.token.address, spender, 100); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('100'); + }); + + it("doesn't revert when force approving a zero allowance", async function () { + await this.mock.$forceApprove(this.token.address, spender, 0); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0'); }); it("doesn't revert when increasing the allowance", async function () { - await this.mock.$safeIncreaseAllowance(this.token.address, constants.ZERO_ADDRESS, 10); + await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('10'); }); it('reverts when decreasing the allowance', async function () { await expectRevert( - this.mock.$safeDecreaseAllowance(this.token.address, constants.ZERO_ADDRESS, 10), + this.mock.$safeDecreaseAllowance(this.token.address, spender, 10), 'SafeERC20: decreased allowance below zero', ); }); @@ -231,31 +304,44 @@ function shouldOnlyRevertOnErrors() { context('with non-zero allowance', function () { beforeEach(async function () { - await this.token.setAllowance(this.mock.address, 100); + await this.token.$_approve(this.mock.address, spender, 100); }); it('reverts when approving a non-zero allowance', async function () { await expectRevert( - this.mock.$safeApprove(this.token.address, constants.ZERO_ADDRESS, 20), + this.mock.$safeApprove(this.token.address, spender, 20), 'SafeERC20: approve from non-zero to non-zero allowance', ); }); it("doesn't revert when approving a zero allowance", async function () { - await this.mock.$safeApprove(this.token.address, constants.ZERO_ADDRESS, 0); + await this.mock.$safeApprove(this.token.address, spender, 0); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0'); + }); + + it("doesn't revert when force approving a non-zero allowance", async function () { + await this.mock.$forceApprove(this.token.address, spender, 20); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('20'); + }); + + it("doesn't revert when force approving a zero allowance", async function () { + await this.mock.$forceApprove(this.token.address, spender, 0); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0'); }); it("doesn't revert when increasing the allowance", async function () { - await this.mock.$safeIncreaseAllowance(this.token.address, constants.ZERO_ADDRESS, 10); + await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('110'); }); it("doesn't revert when decreasing the allowance to a positive value", async function () { - await this.mock.$safeDecreaseAllowance(this.token.address, constants.ZERO_ADDRESS, 50); + await this.mock.$safeDecreaseAllowance(this.token.address, spender, 50); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('50'); }); it('reverts when decreasing the allowance to a negative value', async function () { await expectRevert( - this.mock.$safeDecreaseAllowance(this.token.address, constants.ZERO_ADDRESS, 200), + this.mock.$safeDecreaseAllowance(this.token.address, spender, 200), 'SafeERC20: decreased allowance below zero', ); }); From 3f3774c5df70a7d2399810872056bb5338a09b4c Mon Sep 17 00:00:00 2001 From: Vittorio Minacori Date: Fri, 24 Feb 2023 22:28:37 +0100 Subject: [PATCH 061/197] Improve ERC1363 documentation (#3993) Co-authored-by: Francisco --- contracts/interfaces/IERC1363.sol | 31 ++++++++++++++--------- contracts/interfaces/IERC1363Receiver.sol | 11 +++++--- contracts/interfaces/IERC1363Spender.sol | 11 +++++--- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/contracts/interfaces/IERC1363.sol b/contracts/interfaces/IERC1363.sol index 1517197fe..63d87b962 100644 --- a/contracts/interfaces/IERC1363.sol +++ b/contracts/interfaces/IERC1363.sol @@ -6,6 +6,13 @@ pragma solidity ^0.8.0; import "./IERC20.sol"; import "./IERC165.sol"; +/** + * @dev Interface of an ERC1363 compliant contract, as defined in the + * https://eips.ethereum.org/EIPS/eip-1363[EIP]. + * + * Defines a interface for ERC20 tokens that supports executing recipient + * code after `transfer` or `transferFrom`, or spender code after `approve`. + */ interface IERC1363 is IERC165, IERC20 { /* * Note: the ERC-165 identifier for this interface is 0xb0202a11. @@ -21,53 +28,53 @@ interface IERC1363 is IERC165, IERC20 { /** * @dev Transfer tokens from `msg.sender` to another address and then call `onTransferReceived` on receiver * @param to address The address which you want to transfer to - * @param value uint256 The amount of tokens to be transferred + * @param amount uint256 The amount of tokens to be transferred * @return true unless throwing */ - function transferAndCall(address to, uint256 value) external returns (bool); + function transferAndCall(address to, uint256 amount) external returns (bool); /** * @dev Transfer tokens from `msg.sender` to another address and then call `onTransferReceived` on receiver * @param to address The address which you want to transfer to - * @param value uint256 The amount of tokens to be transferred + * @param amount uint256 The amount of tokens to be transferred * @param data bytes Additional data with no specified format, sent in call to `to` * @return true unless throwing */ - function transferAndCall(address to, uint256 value, bytes memory data) external returns (bool); + function transferAndCall(address to, uint256 amount, bytes memory data) external returns (bool); /** * @dev Transfer tokens from one address to another and then call `onTransferReceived` on receiver * @param from address The address which you want to send tokens from * @param to address The address which you want to transfer to - * @param value uint256 The amount of tokens to be transferred + * @param amount uint256 The amount of tokens to be transferred * @return true unless throwing */ - function transferFromAndCall(address from, address to, uint256 value) external returns (bool); + function transferFromAndCall(address from, address to, uint256 amount) external returns (bool); /** * @dev Transfer tokens from one address to another and then call `onTransferReceived` on receiver * @param from address The address which you want to send tokens from * @param to address The address which you want to transfer to - * @param value uint256 The amount of tokens to be transferred + * @param amount uint256 The amount of tokens to be transferred * @param data bytes Additional data with no specified format, sent in call to `to` * @return true unless throwing */ - function transferFromAndCall(address from, address to, uint256 value, bytes memory data) external returns (bool); + function transferFromAndCall(address from, address to, uint256 amount, bytes memory data) external returns (bool); /** * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender * and then call `onApprovalReceived` on spender. * @param spender address The address which will spend the funds - * @param value uint256 The amount of tokens to be spent + * @param amount uint256 The amount of tokens to be spent */ - function approveAndCall(address spender, uint256 value) external returns (bool); + function approveAndCall(address spender, uint256 amount) external returns (bool); /** * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender * and then call `onApprovalReceived` on spender. * @param spender address The address which will spend the funds - * @param value uint256 The amount of tokens to be spent + * @param amount uint256 The amount of tokens to be spent * @param data bytes Additional data with no specified format, sent in call to `spender` */ - function approveAndCall(address spender, uint256 value, bytes memory data) external returns (bool); + function approveAndCall(address spender, uint256 amount, bytes memory data) external returns (bool); } diff --git a/contracts/interfaces/IERC1363Receiver.sol b/contracts/interfaces/IERC1363Receiver.sol index bc5eaddb0..f5e7a0c28 100644 --- a/contracts/interfaces/IERC1363Receiver.sol +++ b/contracts/interfaces/IERC1363Receiver.sol @@ -3,6 +3,10 @@ pragma solidity ^0.8.0; +/** + * @dev Interface for any contract that wants to support {IERC1363-transferAndCall} + * or {IERC1363-transferFromAndCall} from {ERC1363} token contracts. + */ interface IERC1363Receiver { /* * Note: the ERC-165 identifier for this interface is 0x88a7ca5c. @@ -18,15 +22,14 @@ interface IERC1363Receiver { * Note: the token contract address is always the message sender. * @param operator address The address which called `transferAndCall` or `transferFromAndCall` function * @param from address The address which are token transferred from - * @param value uint256 The amount of tokens transferred + * @param amount uint256 The amount of tokens transferred * @param data bytes Additional data with no specified format - * @return `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))` - * unless throwing + * @return `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))` unless throwing */ function onTransferReceived( address operator, address from, - uint256 value, + uint256 amount, bytes memory data ) external returns (bytes4); } diff --git a/contracts/interfaces/IERC1363Spender.sol b/contracts/interfaces/IERC1363Spender.sol index 28775e140..16dd5e0fe 100644 --- a/contracts/interfaces/IERC1363Spender.sol +++ b/contracts/interfaces/IERC1363Spender.sol @@ -3,6 +3,10 @@ pragma solidity ^0.8.0; +/** + * @dev Interface for any contract that wants to support {IERC1363-approveAndCall} + * from {ERC1363} token contracts. + */ interface IERC1363Spender { /* * Note: the ERC-165 identifier for this interface is 0x7b04a2d0. @@ -17,10 +21,9 @@ interface IERC1363Spender { * transaction being reverted. * Note: the token contract address is always the message sender. * @param owner address The address which called `approveAndCall` function - * @param value uint256 The amount of tokens to be spent + * @param amount uint256 The amount of tokens to be spent * @param data bytes Additional data with no specified format - * @return `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))` - * unless throwing + * @return `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))`unless throwing */ - function onApprovalReceived(address owner, uint256 value, bytes memory data) external returns (bytes4); + function onApprovalReceived(address owner, uint256 amount, bytes memory data) external returns (bytes4); } From 2c69f9f9f5d0e59bba030f6eba5495f7a6991162 Mon Sep 17 00:00:00 2001 From: Clayton Rabenda Date: Sat, 25 Feb 2023 05:31:54 +0800 Subject: [PATCH 062/197] Fix comment on ERC777._beforeTokenTransfer (#3982) --- contracts/token/ERC777/ERC777.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/token/ERC777/ERC777.sol b/contracts/token/ERC777/ERC777.sol index 0e5af5d08..aa95b1843 100644 --- a/contracts/token/ERC777/ERC777.sol +++ b/contracts/token/ERC777/ERC777.sol @@ -498,7 +498,7 @@ contract ERC777 is Context, IERC777, IERC20 { /** * @dev Hook that is called before any token transfer. This includes - * calls to {send}, {transfer}, {operatorSend}, minting and burning. + * calls to {send}, {transfer}, {operatorSend}, {transferFrom}, minting and burning. * * Calling conditions: * From dad73159df3d3053c72b5e430fa8164330f18068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 24 Feb 2023 14:48:08 -0700 Subject: [PATCH 063/197] Add AccessControlDefaultAdminRules (#4009) Co-authored-by: Hadrien Croubois Co-authored-by: Francisco --- .changeset/silent-dancers-type.md | 5 + contracts/access/AccessControl.sol | 3 +- .../access/AccessControlDefaultAdminRules.sol | 240 +++++++++++++++ .../IAccessControlDefaultAdminRules.sol | 73 +++++ contracts/access/Ownable.sol | 4 +- contracts/access/README.adoc | 2 + contracts/interfaces/README.adoc | 2 + docs/modules/ROOT/pages/access-control.adoc | 2 + .../ROOT/pages/extending-contracts.adoc | 2 + test/access/AccessControl.behavior.js | 284 ++++++++++++++++++ .../AccessControlDefaultAdminRules.test.js | 18 ++ test/access/Ownable.test.js | 10 +- test/access/Ownable2Step.test.js | 22 +- .../SupportsInterface.behavior.js | 9 + 14 files changed, 666 insertions(+), 10 deletions(-) create mode 100644 .changeset/silent-dancers-type.md create mode 100644 contracts/access/AccessControlDefaultAdminRules.sol create mode 100644 contracts/access/IAccessControlDefaultAdminRules.sol create mode 100644 test/access/AccessControlDefaultAdminRules.test.js diff --git a/.changeset/silent-dancers-type.md b/.changeset/silent-dancers-type.md new file mode 100644 index 000000000..74ecf500d --- /dev/null +++ b/.changeset/silent-dancers-type.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`AccessControlDefaultAdminRules`: Add an extension of `AccessControl` with additional security rules for the `DEFAULT_ADMIN_ROLE`. diff --git a/contracts/access/AccessControl.sol b/contracts/access/AccessControl.sol index 5f2829e74..3a73de78b 100644 --- a/contracts/access/AccessControl.sol +++ b/contracts/access/AccessControl.sol @@ -44,7 +44,8 @@ import "../utils/introspection/ERC165.sol"; * * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to * grant and revoke this role. Extra precautions should be taken to secure - * accounts that have been granted it. + * accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules} + * to enforce additional security measures for this role. */ abstract contract AccessControl is Context, IAccessControl, ERC165 { struct RoleData { diff --git a/contracts/access/AccessControlDefaultAdminRules.sol b/contracts/access/AccessControlDefaultAdminRules.sol new file mode 100644 index 000000000..e3bce6b75 --- /dev/null +++ b/contracts/access/AccessControlDefaultAdminRules.sol @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.8.0) (access/AccessControlDefaultAdminRules.sol) + +pragma solidity ^0.8.0; + +import "./AccessControl.sol"; +import "./IAccessControlDefaultAdminRules.sol"; +import "../utils/math/SafeCast.sol"; +import "../interfaces/IERC5313.sol"; + +/** + * @dev Extension of {AccessControl} that allows specifying special rules to manage + * the `DEFAULT_ADMIN_ROLE` holder, which is a sensitive role with special permissions + * over other roles that may potentially have privileged rights in the system. + * + * If a specific role doesn't have an admin role assigned, the holder of the + * `DEFAULT_ADMIN_ROLE` will have the ability to grant it and revoke it. + * + * This contract implements the following risk mitigations on top of {AccessControl}: + * + * * Only one account holds the `DEFAULT_ADMIN_ROLE` since deployment until it's potentially renounced. + * * Enforce a 2-step process to transfer the `DEFAULT_ADMIN_ROLE` to another account. + * * Enforce a configurable delay between the two steps, with the ability to cancel in between. + * - Even after the timer has passed to avoid locking it forever. + * * It is not possible to use another role to manage the `DEFAULT_ADMIN_ROLE`. + * + * Example usage: + * + * ```solidity + * contract MyToken is AccessControlDefaultAdminRules { + * constructor() AccessControlDefaultAdminRules( + * 3 days, + * msg.sender // Explicit initial `DEFAULT_ADMIN_ROLE` holder + * ) {} + *} + * ``` + * + * NOTE: The `delay` can only be set in the constructor and is fixed thereafter. + * + * _Available since v4.9._ + */ +abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRules, IERC5313, AccessControl { + uint48 private immutable _defaultAdminDelay; + + address private _currentDefaultAdmin; + address private _pendingDefaultAdmin; + + uint48 private _defaultAdminTransferDelayedUntil; + + /** + * @dev Sets the initial values for {defaultAdminDelay} in seconds and {defaultAdmin}. + * + * The `defaultAdminDelay` value is immutable. It can only be set at the constructor. + */ + constructor(uint48 defaultAdminDelay_, address initialDefaultAdmin) { + _defaultAdminDelay = defaultAdminDelay_; + _grantRole(DEFAULT_ADMIN_ROLE, initialDefaultAdmin); + } + + /** + * @dev See {IERC5313-owner}. + */ + function owner() public view virtual returns (address) { + return defaultAdmin(); + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function defaultAdminDelay() public view virtual returns (uint48) { + return _defaultAdminDelay; + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function defaultAdmin() public view virtual returns (address) { + return _currentDefaultAdmin; + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function pendingDefaultAdmin() public view virtual returns (address) { + return _pendingDefaultAdmin; + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function defaultAdminTransferDelayedUntil() public view virtual returns (uint48) { + return _defaultAdminTransferDelayedUntil; + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IAccessControlDefaultAdminRules).interfaceId || super.supportsInterface(interfaceId); + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function beginDefaultAdminTransfer(address newAdmin) public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _beginDefaultAdminTransfer(newAdmin); + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function acceptDefaultAdminTransfer() public virtual { + require(_msgSender() == pendingDefaultAdmin(), "AccessControl: pending admin must accept"); + _acceptDefaultAdminTransfer(); + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function cancelDefaultAdminTransfer() public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _resetDefaultAdminTransfer(); + } + + /** + * @dev Revokes `role` from the calling account. + * + * For `DEFAULT_ADMIN_ROLE`, only allows renouncing in two steps, so it's required + * that the {defaultAdminTransferDelayedUntil} has passed and the pending default admin is the zero address. + * After its execution, it will not be possible to call `onlyRole(DEFAULT_ADMIN_ROLE)` + * functions. + * + * For other roles, see {AccessControl-renounceRole}. + * + * NOTE: Renouncing `DEFAULT_ADMIN_ROLE` will leave the contract without a defaultAdmin, + * thereby disabling any functionality that is only available to the default admin, and the + * possibility of reassigning a non-administrated role. + */ + function renounceRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { + if (role == DEFAULT_ADMIN_ROLE) { + require( + pendingDefaultAdmin() == address(0) && _hasDefaultAdminTransferDelayPassed(), + "AccessControl: only can renounce in two delayed steps" + ); + } + super.renounceRole(role, account); + } + + /** + * @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"); + super.grantRole(role, account); + } + + /** + * @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"); + super.revokeRole(role, account); + } + + /** + * @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"); + super._setRoleAdmin(role, adminRole); + } + + /** + * @dev Grants `role` to `account`. + * + * For `DEFAULT_ADMIN_ROLE`, it only allows granting if there isn't already a role's holder + * or if the role has been previously renounced. + * + * For other roles, see {AccessControl-renounceRole}. + * + * NOTE: Exposing this function through another mechanism may make the + * `DEFAULT_ADMIN_ROLE` assignable again. Make sure to guarantee this is + * the expected behavior in your implementation. + */ + function _grantRole(bytes32 role, address account) internal virtual override { + if (role == DEFAULT_ADMIN_ROLE) { + require(defaultAdmin() == address(0), "AccessControl: default admin already granted"); + _currentDefaultAdmin = account; + } + super._grantRole(role, account); + } + + /** + * @dev See {acceptDefaultAdminTransfer}. + * + * Internal function without access restriction. + */ + function _acceptDefaultAdminTransfer() internal virtual { + require(_hasDefaultAdminTransferDelayPassed(), "AccessControl: transfer delay not passed"); + _revokeRole(DEFAULT_ADMIN_ROLE, defaultAdmin()); + _grantRole(DEFAULT_ADMIN_ROLE, pendingDefaultAdmin()); + _resetDefaultAdminTransfer(); + } + + /** + * @dev See {beginDefaultAdminTransfer}. + * + * Internal function without access restriction. + */ + function _beginDefaultAdminTransfer(address newAdmin) internal virtual { + _defaultAdminTransferDelayedUntil = SafeCast.toUint48(block.timestamp) + defaultAdminDelay(); + _pendingDefaultAdmin = newAdmin; + emit DefaultAdminRoleChangeStarted(pendingDefaultAdmin(), defaultAdminTransferDelayedUntil()); + } + + /** + * @dev See {AccessControl-_revokeRole}. + */ + function _revokeRole(bytes32 role, address account) internal virtual override { + if (role == DEFAULT_ADMIN_ROLE) { + delete _currentDefaultAdmin; + } + super._revokeRole(role, account); + } + + /** + * @dev Resets the pending default admin and delayed until. + */ + function _resetDefaultAdminTransfer() private { + delete _pendingDefaultAdmin; + delete _defaultAdminTransferDelayedUntil; + } + + /** + * @dev Checks if a {defaultAdminTransferDelayedUntil} has been set and passed. + */ + function _hasDefaultAdminTransferDelayPassed() private view returns (bool) { + uint48 delayedUntil = defaultAdminTransferDelayedUntil(); + return delayedUntil > 0 && delayedUntil < block.timestamp; + } +} diff --git a/contracts/access/IAccessControlDefaultAdminRules.sol b/contracts/access/IAccessControlDefaultAdminRules.sol new file mode 100644 index 000000000..753c7c802 --- /dev/null +++ b/contracts/access/IAccessControlDefaultAdminRules.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.9.0 (access/IAccessControlDefaultAdminRules.sol) + +pragma solidity ^0.8.0; + +import "./IAccessControl.sol"; + +/** + * @dev External interface of AccessControlDefaultAdminRules declared to support ERC165 detection. + * + * _Available since v4.9._ + */ +interface IAccessControlDefaultAdminRules is IAccessControl { + /** + * @dev Emitted when a `DEFAULT_ADMIN_ROLE` transfer is started, setting `newDefaultAdmin` + * as the next default admin, which will have rights to claim the `DEFAULT_ADMIN_ROLE` + * after `defaultAdminTransferDelayedUntil` has passed. + */ + event DefaultAdminRoleChangeStarted(address indexed newDefaultAdmin, uint48 defaultAdminTransferDelayedUntil); + + /** + * @dev Returns the delay between each `DEFAULT_ADMIN_ROLE` transfer. + */ + function defaultAdminDelay() external view returns (uint48); + + /** + * @dev Returns the address of the current `DEFAULT_ADMIN_ROLE` holder. + */ + function defaultAdmin() external view returns (address); + + /** + * @dev Returns the address of the pending `DEFAULT_ADMIN_ROLE` holder. + */ + function pendingDefaultAdmin() external view returns (address); + + /** + * @dev Returns the timestamp after which the pending default admin can claim the `DEFAULT_ADMIN_ROLE`. + */ + function defaultAdminTransferDelayedUntil() external view returns (uint48); + + /** + * @dev Starts a `DEFAULT_ADMIN_ROLE` transfer by setting a pending default admin + * and a timer to pass. + * + * Requirements: + * + * - Only can be called by the current `DEFAULT_ADMIN_ROLE` holder. + * + * Emits a {DefaultAdminRoleChangeStarted}. + */ + function beginDefaultAdminTransfer(address newAdmin) external; + + /** + * @dev Completes a `DEFAULT_ADMIN_ROLE` transfer. + * + * Requirements: + * + * - Caller should be the pending default admin. + * - `DEFAULT_ADMIN_ROLE` should be granted to the caller. + * - `DEFAULT_ADMIN_ROLE` should be revoked from the previous holder. + */ + function acceptDefaultAdminTransfer() external; + + /** + * @dev Cancels a `DEFAULT_ADMIN_ROLE` transfer. + * + * Requirements: + * + * - Can be called even after the timer has passed. + * - Can only be called by the current `DEFAULT_ADMIN_ROLE` holder. + */ + function cancelDefaultAdminTransfer() external; +} diff --git a/contracts/access/Ownable.sol b/contracts/access/Ownable.sol index 6d4e866f4..1378ffb43 100644 --- a/contracts/access/Ownable.sol +++ b/contracts/access/Ownable.sol @@ -53,10 +53,10 @@ abstract contract Ownable is Context { /** * @dev Leaves the contract without owner. It will not be possible to call - * `onlyOwner` functions anymore. Can only be called by the current owner. + * `onlyOwner` functions. Can only be called by the current owner. * * NOTE: Renouncing ownership will leave the contract without an owner, - * thereby removing any functionality that is only available to the owner. + * thereby disabling any functionality that is only available to the owner. */ function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); diff --git a/contracts/access/README.adoc b/contracts/access/README.adoc index 888d0e99e..80ca0020f 100644 --- a/contracts/access/README.adoc +++ b/contracts/access/README.adoc @@ -23,3 +23,5 @@ This directory provides ways to restrict who can access the functions of a contr {{IAccessControlEnumerable}} {{AccessControlEnumerable}} + +{{AccessControlDefaultAdminRules}} diff --git a/contracts/interfaces/README.adoc b/contracts/interfaces/README.adoc index 710c078e8..4525bc9a2 100644 --- a/contracts/interfaces/README.adoc +++ b/contracts/interfaces/README.adoc @@ -64,6 +64,8 @@ are useful to interact with third party contracts that implement them. {{IERC4626}} +{{IERC5313}} + {{IERC5267}} {{IERC5805}} diff --git a/docs/modules/ROOT/pages/access-control.adoc b/docs/modules/ROOT/pages/access-control.adoc index f3ddb6234..56d91e1c1 100644 --- a/docs/modules/ROOT/pages/access-control.adoc +++ b/docs/modules/ROOT/pages/access-control.adoc @@ -131,6 +131,8 @@ Every role has an associated admin role, which grants permission to call the `gr This mechanism can be used to create complex permissioning structures resembling organizational charts, but it also provides an easy way to manage simpler applications. `AccessControl` includes a special role, called `DEFAULT_ADMIN_ROLE`, which acts as the **default admin role for all roles**. An account with this role will be able to manage any other role, unless `_setRoleAdmin` is used to select a new admin role. +Since it is the admin for all roles by default, and in fact it is also its own admin, this role carries significant risk. To mitigate this risk we provide xref:api:access.adoc#AccessControlDefaultAdminRules[`AccessControlDefaultAdminRules`], a recommended extension of `AccessControl` that adds a number of enforced security measures for this role: the admin is restricted to a single account, with a 2-step transfer procedure with a delay in between steps. + Let's take a look at the ERC20 token example, this time taking advantage of the default admin role: [source,solidity] diff --git a/docs/modules/ROOT/pages/extending-contracts.adoc b/docs/modules/ROOT/pages/extending-contracts.adoc index a440f4067..d12ef7585 100644 --- a/docs/modules/ROOT/pages/extending-contracts.adoc +++ b/docs/modules/ROOT/pages/extending-contracts.adoc @@ -66,6 +66,8 @@ contract ModifiedAccessControl is AccessControl { The `super.revokeRole` statement at the end will invoke ``AccessControl``'s original version of `revokeRole`, the same code that would've run if there were no overrides in place. +NOTE: The same rule is implemented and extended in xref:api:access.adoc#AccessControlDefaultAdminRules[`AccessControlDefaultAdminRules`], an extension that also adds enforced security measures for the `DEFAULT_ADMIN_ROLE`. + [[using-hooks]] == Using Hooks diff --git a/test/access/AccessControl.behavior.js b/test/access/AccessControl.behavior.js index a93560983..e04c5a165 100644 --- a/test/access/AccessControl.behavior.js +++ b/test/access/AccessControl.behavior.js @@ -1,5 +1,7 @@ const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { ZERO_ADDRESS } = require('@openzeppelin/test-helpers/src/constants'); const { expect } = require('chai'); +const { time } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); @@ -210,8 +212,290 @@ function shouldBehaveLikeAccessControlEnumerable(errorPrefix, admin, authorized, }); } +function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defaultAdmin, newDefaultAdmin, other) { + shouldSupportInterfaces(['AccessControlDefaultAdminRules']); + + it('has a default disabled delayed until', async function () { + expect(await this.accessControl.defaultAdminTransferDelayedUntil()).to.be.bignumber.equal(web3.utils.toBN(0)); + }); + + it('has a default pending default admin', async function () { + expect(await this.accessControl.pendingDefaultAdmin()).to.equal(ZERO_ADDRESS); + }); + + it('has a default current owner set to the initial default admin', async function () { + const owner = await this.accessControl.owner(); + expect(owner).to.equal(defaultAdmin); + expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, owner)).to.be.true; + }); + + it('should revert if granting default admin role', async function () { + await expectRevert( + this.accessControl.grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }), + `${errorPrefix}: can't directly grant default admin role`, + ); + }); + + it('should revert if revoking default admin role', async function () { + await expectRevert( + this.accessControl.revokeRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }), + `${errorPrefix}: can't directly revoke default admin role`, + ); + }); + + it("should revert if defaultAdmin's admin is changed", async function () { + await expectRevert( + this.accessControl.$_setRoleAdmin(DEFAULT_ADMIN_ROLE, defaultAdmin), + `${errorPrefix}: can't violate default admin rules`, + ); + }); + + it('should not grant the default admin role twice', async function () { + await expectRevert( + this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin), + `${errorPrefix}: default admin already granted`, + ); + }); + + describe('begins transfer of default admin', function () { + let receipt; + let defaultAdminTransferDelayedUntil; + + beforeEach('begins admin transfer', async function () { + receipt = await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); + defaultAdminTransferDelayedUntil = web3.utils.toBN(await time.latest()).add(delay); + }); + + it('should set pending default admin and delayed until', async function () { + expect(await this.accessControl.pendingDefaultAdmin()).to.equal(newDefaultAdmin); + expect(await this.accessControl.defaultAdminTransferDelayedUntil()).to.be.bignumber.equal( + defaultAdminTransferDelayedUntil, + ); + expectEvent(receipt, 'DefaultAdminRoleChangeStarted', { + newDefaultAdmin, + defaultAdminTransferDelayedUntil, + }); + }); + + it('should be able to begin a transfer again before delay pass', async function () { + // Time passes just before delay + await time.setNextBlockTimestamp(defaultAdminTransferDelayedUntil.subn(1)); + + // defaultAdmin changes its mind and begin again to another address + await this.accessControl.beginDefaultAdminTransfer(other, { from: defaultAdmin }); + const newDelayedUntil = web3.utils.toBN(await time.latest()).add(delay); + expect(await this.accessControl.pendingDefaultAdmin()).to.equal(other); + expect(await this.accessControl.defaultAdminTransferDelayedUntil()).to.be.bignumber.equal(newDelayedUntil); + }); + + it('should be able to begin a transfer again after delay pass if not accepted', async function () { + // Time passes after delay without acceptance + await time.setNextBlockTimestamp(defaultAdminTransferDelayedUntil.addn(1)); + + // defaultAdmin changes its mind and begin again to another address + await this.accessControl.beginDefaultAdminTransfer(other, { from: defaultAdmin }); + const newDelayedUntil = web3.utils.toBN(await time.latest()).add(delay); + expect(await this.accessControl.pendingDefaultAdmin()).to.equal(other); + expect(await this.accessControl.defaultAdminTransferDelayedUntil()).to.be.bignumber.equal(newDelayedUntil); + }); + + it('should revert if it is called by non-admin accounts', async function () { + await expectRevert( + this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: other }), + `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + ); + }); + }); + + describe('accepts transfer admin', function () { + let delayPassed; + + beforeEach(async function () { + await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); + delayPassed = web3.utils + .toBN(await time.latest()) + .add(delay) + .addn(1); + }); + + describe('caller is pending default admin and delay has passed', function () { + let from; + + beforeEach(async function () { + await time.setNextBlockTimestamp(delayPassed); + from = newDefaultAdmin; + }); + + it('accepts a transfer and changes default admin', async function () { + const receipt = await this.accessControl.acceptDefaultAdminTransfer({ from }); + + // Storage changes + expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, defaultAdmin)).to.be.false; + expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, newDefaultAdmin)).to.be.true; + expect(await this.accessControl.owner()).to.equal(newDefaultAdmin); + + // Emit events + expectEvent(receipt, 'RoleRevoked', { + role: DEFAULT_ADMIN_ROLE, + account: defaultAdmin, + }); + expectEvent(receipt, 'RoleGranted', { + role: DEFAULT_ADMIN_ROLE, + account: newDefaultAdmin, + }); + + // Resets pending default admin and delayed until + expect(await this.accessControl.defaultAdminTransferDelayedUntil()).to.be.bignumber.equal(web3.utils.toBN(0)); + expect(await this.accessControl.pendingDefaultAdmin()).to.equal(ZERO_ADDRESS); + }); + }); + + it('should revert if caller is not pending default admin', async function () { + await time.setNextBlockTimestamp(delayPassed); + await expectRevert( + this.accessControl.acceptDefaultAdminTransfer({ from: other }), + `${errorPrefix}: pending admin must accept`, + ); + }); + + describe('delayedUntil not passed', function () { + let delayNotPassed; + + beforeEach(function () { + delayNotPassed = delayPassed.subn(1); + }); + + it('should revert if block.timestamp is equal to delayed until', async function () { + await time.setNextBlockTimestamp(delayNotPassed); + await expectRevert( + this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }), + `${errorPrefix}: transfer delay not passed`, + ); + }); + + it('should revert if block.timestamp is less than delayed until', async function () { + await expectRevert( + this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }), + `${errorPrefix}: transfer delay not passed`, + ); + }); + }); + }); + + describe('cancel transfer default admin', function () { + let delayPassed; + + beforeEach(async function () { + await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); + delayPassed = web3.utils + .toBN(await time.latest()) + .add(delay) + .addn(1); + }); + + it('resets pending default admin and delayed until', async function () { + await this.accessControl.cancelDefaultAdminTransfer({ from: defaultAdmin }); + expect(await this.accessControl.defaultAdminTransferDelayedUntil()).to.be.bignumber.equal(web3.utils.toBN(0)); + expect(await this.accessControl.pendingDefaultAdmin()).to.equal(ZERO_ADDRESS); + + // Advance until passed delay + await time.setNextBlockTimestamp(delayPassed); + + // Previous pending default admin should not be able to accept after cancellation. + await expectRevert( + this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }), + `${errorPrefix}: pending admin must accept`, + ); + }); + + it('cancels even after delay has passed', async function () { + await this.accessControl.cancelDefaultAdminTransfer({ from: defaultAdmin }); + await time.setNextBlockTimestamp(delayPassed); + expect(await this.accessControl.defaultAdminTransferDelayedUntil()).to.be.bignumber.equal(web3.utils.toBN(0)); + expect(await this.accessControl.pendingDefaultAdmin()).to.equal(ZERO_ADDRESS); + }); + + it('reverts if called by non default admin accounts', async function () { + await expectRevert( + this.accessControl.cancelDefaultAdminTransfer({ from: other }), + `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + ); + }); + }); + + describe('renouncing admin', function () { + let delayPassed; + let from = defaultAdmin; + + beforeEach(async function () { + await this.accessControl.beginDefaultAdminTransfer(ZERO_ADDRESS, { from }); + delayPassed = web3.utils + .toBN(await time.latest()) + .add(delay) + .addn(1); + }); + + it('it renounces role', async function () { + await time.setNextBlockTimestamp(delayPassed); + const receipt = await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, from, { from }); + + expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, defaultAdmin)).to.be.false; + expect(await this.accessControl.hasRole(ZERO_ADDRESS, defaultAdmin)).to.be.false; + expectEvent(receipt, 'RoleRevoked', { + role: DEFAULT_ADMIN_ROLE, + account: from, + }); + expect(await this.accessControl.owner()).to.equal(ZERO_ADDRESS); + }); + + it('allows to recover access using the internal _grantRole', async function () { + await time.setNextBlockTimestamp(delayPassed); + await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, from, { from }); + + const grantRoleReceipt = await this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, other); + expectEvent(grantRoleReceipt, 'RoleGranted', { + role: DEFAULT_ADMIN_ROLE, + account: other, + }); + }); + + it('reverts if caller is not default admin', async function () { + await time.setNextBlockTimestamp(delayPassed); + await expectRevert( + this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, other, { from }), + `${errorPrefix}: can only renounce roles for self`, + ); + }); + + describe('delayed until not passed', function () { + let delayNotPassed; + + beforeEach(function () { + delayNotPassed = delayPassed.subn(1); + }); + + it('reverts if block.timestamp is equal to delayed until', async function () { + await time.setNextBlockTimestamp(delayNotPassed); + await expectRevert( + this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from }), + `${errorPrefix}: only can renounce in two delayed steps`, + ); + }); + + it('reverts if block.timestamp is less than delayed until', async function () { + await time.setNextBlockTimestamp(delayNotPassed.subn(1)); + await expectRevert( + this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from }), + `${errorPrefix}: only can renounce in two delayed steps`, + ); + }); + }); + }); +} + module.exports = { DEFAULT_ADMIN_ROLE, shouldBehaveLikeAccessControl, shouldBehaveLikeAccessControlEnumerable, + shouldBehaveLikeAccessControlDefaultAdminRules, }; diff --git a/test/access/AccessControlDefaultAdminRules.test.js b/test/access/AccessControlDefaultAdminRules.test.js new file mode 100644 index 000000000..2a23d3b6d --- /dev/null +++ b/test/access/AccessControlDefaultAdminRules.test.js @@ -0,0 +1,18 @@ +const { time } = require('@openzeppelin/test-helpers'); +const { + shouldBehaveLikeAccessControl, + shouldBehaveLikeAccessControlDefaultAdminRules, +} = require('./AccessControl.behavior.js'); + +const AccessControlDefaultAdminRules = artifacts.require('$AccessControlDefaultAdminRules'); + +contract('AccessControlDefaultAdminRules', function (accounts) { + const delay = web3.utils.toBN(time.duration.days(10)); + + beforeEach(async function () { + this.accessControl = await AccessControlDefaultAdminRules.new(delay, accounts[0], { from: accounts[0] }); + }); + + shouldBehaveLikeAccessControl('AccessControl', ...accounts); + shouldBehaveLikeAccessControlDefaultAdminRules('AccessControl', delay, ...accounts); +}); diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index dc308f98f..109150874 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -37,7 +37,7 @@ contract('Ownable', function (accounts) { }); describe('renounce ownership', function () { - it('loses owner after renouncement', async function () { + it('loses ownership after renouncement', async function () { const receipt = await this.ownable.renounceOwnership({ from: owner }); expectEvent(receipt, 'OwnershipTransferred'); @@ -47,5 +47,13 @@ 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'); }); + + it('allows to recover access using the internal _transferOwnership', async function () { + await this.ownable.renounceOwnership({ from: owner }); + const receipt = await this.ownable.$_transferOwnership(other); + expectEvent(receipt, 'OwnershipTransferred'); + + expect(await this.ownable.owner()).to.equal(other); + }); }); }); diff --git a/test/access/Ownable2Step.test.js b/test/access/Ownable2Step.test.js index 64d432762..ce043057b 100644 --- a/test/access/Ownable2Step.test.js +++ b/test/access/Ownable2Step.test.js @@ -27,6 +27,16 @@ contract('Ownable2Step', function (accounts) { expect(await this.ownable2Step.pendingOwner()).to.not.equal(accountA); }); + it('guards transfer against invalid user', async function () { + await this.ownable2Step.transferOwnership(accountA, { from: owner }); + await expectRevert( + this.ownable2Step.acceptOwnership({ from: accountB }), + 'Ownable2Step: caller is not the new owner', + ); + }); + }); + + it('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 ... @@ -46,12 +56,12 @@ contract('Ownable2Step', function (accounts) { ); }); - it('guards transfer against invalid user', async function () { - await this.ownable2Step.transferOwnership(accountA, { from: owner }); - await expectRevert( - this.ownable2Step.acceptOwnership({ from: accountB }), - 'Ownable2Step: caller is not the new owner', - ); + it('allows to recover access using the internal _transferOwnership', async function () { + await this.ownable.renounceOwnership({ from: owner }); + const receipt = await this.ownable.$_transferOwnership(accountA); + expectEvent(receipt, 'OwnershipTransferred'); + + expect(await this.ownable.owner()).to.equal(accountA); }); }); }); diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index 02d147884..5ffe242ed 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -37,6 +37,15 @@ const INTERFACES = { 'renounceRole(bytes32,address)', ], AccessControlEnumerable: ['getRoleMember(bytes32,uint256)', 'getRoleMemberCount(bytes32)'], + AccessControlDefaultAdminRules: [ + 'defaultAdminDelay()', + 'defaultAdmin()', + 'defaultAdminTransferDelayedUntil()', + 'pendingDefaultAdmin()', + 'beginDefaultAdminTransfer(address)', + 'acceptDefaultAdminTransfer()', + 'cancelDefaultAdminTransfer()', + ], Governor: [ 'name()', 'version()', From e1a77ab15f9e311edffe855cf5f5a2f078cdd1ea Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 1 Mar 2023 19:35:15 +0100 Subject: [PATCH 064/197] Fix an upgrade replay bug in Governor.propose (#4082) --- contracts/governance/Governor.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index 1483da6d2..12fde79de 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -276,7 +276,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive 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].proposer == address(0), "Governor: proposal already exists"); + require(_proposals[proposalId].voteStart == 0, "Governor: proposal already exists"); uint256 snapshot = currentTimepoint + votingDelay(); uint256 deadline = snapshot + votingPeriod(); From 0ebc6e35295aa276cf5dcc23e871ce087c20d447 Mon Sep 17 00:00:00 2001 From: qedk <1994constant@gmail.com> Date: Thu, 2 Mar 2023 13:40:36 +0400 Subject: [PATCH 065/197] Fix grammar in docs (#4085) --- contracts/governance/Governor.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index 12fde79de..a2911bf98 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -21,7 +21,7 @@ import "./IGovernor.sol"; * * - A counting module must implement {quorum}, {_quorumReached}, {_voteSucceeded} and {_countVote} * - A voting module must implement {_getVotes} - * - Additionally, the {votingPeriod} must also be implemented + * - Additionally, {votingPeriod} must also be implemented * * _Available since v4.3._ */ From 8ba26f388f4c1959a071310b4823a7889498e28c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 2 Mar 2023 23:41:28 +0100 Subject: [PATCH 066/197] Merge pull request from GHSA-878m-3g6q-594q * Test batch minting of 1 * Fix balance tracking * fix lint * add changeset * rename UNSAFE -> unsafe * fix docs * fix changeset * grammar * add explanation of preserved invariant * add fuzz tests * rename variable * improve property definition * add burn * add test ownership multiple batches * refactor fuzz tests * change ownership test for better probability * typo * reorder comment * update changelog notes * edit changelog * lint * Update CHANGELOG.md --------- Co-authored-by: Francisco Giordano --- .changeset/brave-olives-laugh.md | 5 + CHANGELOG.md | 4 + contracts/token/ERC721/ERC721.sol | 28 ++-- .../ERC721/extensions/ERC721Consecutive.sol | 5 + .../ERC721/extensions/ERC721Consecutive.t.sol | 120 ++++++++++++++++++ .../extensions/ERC721Consecutive.test.js | 3 +- 6 files changed, 149 insertions(+), 16 deletions(-) create mode 100644 .changeset/brave-olives-laugh.md create mode 100644 test/token/ERC721/extensions/ERC721Consecutive.t.sol diff --git a/.changeset/brave-olives-laugh.md b/.changeset/brave-olives-laugh.md new file mode 100644 index 000000000..8cb416323 --- /dev/null +++ b/.changeset/brave-olives-laugh.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ERC721Consecutive`: Fixed a bug when `_mintConsecutive` is used for batches of size 1 that could lead to balance overflow. Refer to the breaking changes section in the changelog for a note on the behavior of `ERC721._beforeTokenTransfer`. diff --git a/CHANGELOG.md b/CHANGELOG.md index e882fd516..1c3a41dd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ - `ERC777`: The `ERC777` token standard is no longer supported by OpenZeppelin. Our implementation is now deprecated and will be removed in the next major release. The corresponding standard interfaces remain available. ([#4066](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4066)) - `ERC1820Implementer`: The `ERC1820` pseudo-introspection mechanism is no longer supported by OpenZeppelin. Our implementation is now deprecated and will be removed in the next major release. The corresponding standard interfaces remain available. ([#4066](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4066)) +### Breaking changes + +- `ERC721`: The internal function `_beforeTokenTransfer` no longer updates balances, which it previously did when `batchSize` was greater than 1. This change has no consequence unless a custom ERC721 extension is explicitly invoking `_beforeTokenTransfer`. Balance updates in extensions must now be done explicitly using `__unsafe_increaseBalance`, with a name that indicates that there is an invariant that has to be manually verified. + ## 4.8.1 (2023-01-12) - `ERC4626`: Use staticcall instead of call when fetching underlying ERC-20 decimals. ([#3943](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3943)) diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 6bf620b4f..ea3fffe3f 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -434,21 +434,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */ - function _beforeTokenTransfer( - address from, - address to, - uint256 /* firstTokenId */, - uint256 batchSize - ) internal virtual { - if (batchSize > 1) { - if (from != address(0)) { - _balances[from] -= batchSize; - } - if (to != address(0)) { - _balances[to] += batchSize; - } - } - } + function _beforeTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal virtual {} /** * @dev Hook that is called after any token transfer. This includes minting and burning. If {ERC721Consecutive} is @@ -465,4 +451,16 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */ function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal virtual {} + + /** + * @dev Unsafe write access to the balances, used by extensions that "mint" tokens using an {ownerOf} override. + * + * WARNING: Anyone calling this MUST ensure that the balances remain consistent with the ownership. The invariant + * being that for any address `a` the value returned by `balanceOf(a)` must be equal to the number of tokens such + * that `ownerOf(tokenId)` is `a`. + */ + // solhint-disable-next-line func-name-mixedcase + function __unsafe_increaseBalance(address account, uint256 amount) internal { + _balances[account] += amount; + } } diff --git a/contracts/token/ERC721/extensions/ERC721Consecutive.sol b/contracts/token/ERC721/extensions/ERC721Consecutive.sol index e6843f1fa..e8610fdf8 100644 --- a/contracts/token/ERC721/extensions/ERC721Consecutive.sol +++ b/contracts/token/ERC721/extensions/ERC721Consecutive.sol @@ -96,6 +96,11 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { // push an ownership checkpoint & emit event uint96 last = first + batchSize - 1; _sequentialOwnership.push(last, uint160(to)); + + // The invariant required by this function is preserved because the new sequentialOwnership checkpoint + // is attributing ownership of `batchSize` new tokens to account `to`. + __unsafe_increaseBalance(to, batchSize); + emit ConsecutiveTransfer(first, last, address(0), to); // hook after diff --git a/test/token/ERC721/extensions/ERC721Consecutive.t.sol b/test/token/ERC721/extensions/ERC721Consecutive.t.sol new file mode 100644 index 000000000..fc6d3c79f --- /dev/null +++ b/test/token/ERC721/extensions/ERC721Consecutive.t.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../../../../contracts/token/ERC721/extensions/ERC721Consecutive.sol"; +import "forge-std/Test.sol"; + +function toSingleton(address account) pure returns (address[] memory) { + address[] memory accounts = new address[](1); + accounts[0] = account; + return accounts; +} + +contract ERC721ConsecutiveTarget is StdUtils, ERC721Consecutive { + uint256 public totalMinted = 0; + + constructor(address[] memory receivers, uint256[] memory batches) ERC721("", "") { + for (uint256 i = 0; i < batches.length; i++) { + address receiver = receivers[i % receivers.length]; + uint96 batchSize = uint96(bound(batches[i], 0, _maxBatchSize())); + _mintConsecutive(receiver, batchSize); + totalMinted += batchSize; + } + } + + function burn(uint256 tokenId) public { + _burn(tokenId); + } +} + +contract ERC721ConsecutiveTest is Test { + function test_balance(address receiver, uint256[] calldata batches) public { + vm.assume(receiver != address(0)); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches); + + assertEq(token.balanceOf(receiver), token.totalMinted()); + } + + function test_ownership(address receiver, uint256[] calldata batches, uint256[2] calldata unboundedTokenId) public { + vm.assume(receiver != address(0)); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches); + + if (token.totalMinted() > 0) { + uint256 validTokenId = bound(unboundedTokenId[0], 0, token.totalMinted() - 1); + assertEq(token.ownerOf(validTokenId), receiver); + } + + uint256 invalidTokenId = bound(unboundedTokenId[1], token.totalMinted(), type(uint256).max); + vm.expectRevert(); + token.ownerOf(invalidTokenId); + } + + function test_burn(address receiver, uint256[] calldata batches, uint256 unboundedTokenId) public { + vm.assume(receiver != address(0)); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches); + + // only test if we minted at least one token + uint256 supply = token.totalMinted(); + vm.assume(supply > 0); + + // burn a token in [0; supply[ + uint256 tokenId = bound(unboundedTokenId, 0, supply - 1); + token.burn(tokenId); + + // balance should have decreased + assertEq(token.balanceOf(receiver), supply - 1); + + // token should be burnt + vm.expectRevert(); + token.ownerOf(tokenId); + } + + function test_transfer( + address[2] calldata accounts, + uint256[2] calldata unboundedBatches, + uint256[2] calldata unboundedTokenId + ) public { + vm.assume(accounts[0] != address(0)); + vm.assume(accounts[1] != address(0)); + vm.assume(accounts[0] != accounts[1]); + + address[] memory receivers = new address[](2); + receivers[0] = accounts[0]; + receivers[1] = accounts[1]; + + // We assume _maxBatchSize is 5000 (the default). This test will break otherwise. + uint256[] memory batches = new uint256[](2); + batches[0] = bound(unboundedBatches[0], 1, 5000); + batches[1] = bound(unboundedBatches[1], 1, 5000); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(receivers, batches); + + uint256 tokenId0 = bound(unboundedTokenId[0], 0, batches[0] - 1); + uint256 tokenId1 = bound(unboundedTokenId[1], 0, batches[1] - 1) + batches[0]; + + assertEq(token.ownerOf(tokenId0), accounts[0]); + assertEq(token.ownerOf(tokenId1), accounts[1]); + assertEq(token.balanceOf(accounts[0]), batches[0]); + assertEq(token.balanceOf(accounts[1]), batches[1]); + + vm.prank(accounts[0]); + token.transferFrom(accounts[0], accounts[1], tokenId0); + + assertEq(token.ownerOf(tokenId0), accounts[1]); + assertEq(token.ownerOf(tokenId1), accounts[1]); + assertEq(token.balanceOf(accounts[0]), batches[0] - 1); + assertEq(token.balanceOf(accounts[1]), batches[1] + 1); + + vm.prank(accounts[1]); + token.transferFrom(accounts[1], accounts[0], tokenId1); + + assertEq(token.ownerOf(tokenId0), accounts[1]); + assertEq(token.ownerOf(tokenId1), accounts[0]); + assertEq(token.balanceOf(accounts[0]), batches[0]); + assertEq(token.balanceOf(accounts[1]), batches[1]); + } +} diff --git a/test/token/ERC721/extensions/ERC721Consecutive.test.js b/test/token/ERC721/extensions/ERC721Consecutive.test.js index 3d4922846..a92aef480 100644 --- a/test/token/ERC721/extensions/ERC721Consecutive.test.js +++ b/test/token/ERC721/extensions/ERC721Consecutive.test.js @@ -12,7 +12,8 @@ contract('ERC721Consecutive', function (accounts) { const symbol = 'NFT'; const batches = [ { receiver: user1, amount: 0 }, - { receiver: user1, amount: 3 }, + { receiver: user1, amount: 1 }, + { receiver: user1, amount: 2 }, { receiver: user2, amount: 5 }, { receiver: user3, amount: 0 }, { receiver: user1, amount: 7 }, From e58c6d8ff4fd2ddac0320f9b8b7f4fca14f65192 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 2 Mar 2023 19:47:41 -0300 Subject: [PATCH 067/197] Fix linter error --- test/token/ERC721/extensions/ERC721Consecutive.t.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/token/ERC721/extensions/ERC721Consecutive.t.sol b/test/token/ERC721/extensions/ERC721Consecutive.t.sol index fc6d3c79f..3cc868929 100644 --- a/test/token/ERC721/extensions/ERC721Consecutive.t.sol +++ b/test/token/ERC721/extensions/ERC721Consecutive.t.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.0; +// solhint-disable func-name-mixedcase + import "../../../../contracts/token/ERC721/extensions/ERC721Consecutive.sol"; import "forge-std/Test.sol"; From eedca5d873a559140d79cc7ec674d0e28b2b6ebd Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 2 Mar 2023 21:13:25 -0300 Subject: [PATCH 068/197] Merge release-v4.8 branch --- .changeset/brave-olives-laugh.md | 5 ----- CHANGELOG.md | 4 ++++ contracts/governance/TimelockController.sol | 2 +- contracts/package.json | 2 +- contracts/proxy/utils/Initializable.sol | 2 +- contracts/token/ERC1155/extensions/ERC1155Pausable.sol | 2 +- contracts/token/ERC20/extensions/ERC20Pausable.sol | 2 +- contracts/token/ERC20/extensions/ERC20Votes.sol | 2 +- contracts/token/ERC20/extensions/ERC4626.sol | 2 +- contracts/token/ERC721/ERC721.sol | 2 +- contracts/token/ERC721/extensions/ERC721Consecutive.sol | 2 +- contracts/token/ERC721/extensions/ERC721Pausable.sol | 2 +- contracts/utils/Checkpoints.sol | 2 +- contracts/utils/introspection/ERC165Checker.sol | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 16 files changed, 19 insertions(+), 20 deletions(-) delete mode 100644 .changeset/brave-olives-laugh.md diff --git a/.changeset/brave-olives-laugh.md b/.changeset/brave-olives-laugh.md deleted file mode 100644 index 8cb416323..000000000 --- a/.changeset/brave-olives-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`ERC721Consecutive`: Fixed a bug when `_mintConsecutive` is used for batches of size 1 that could lead to balance overflow. Refer to the breaking changes section in the changelog for a note on the behavior of `ERC721._beforeTokenTransfer`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c3a41dd9..333b6d4a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ - `ERC777`: The `ERC777` token standard is no longer supported by OpenZeppelin. Our implementation is now deprecated and will be removed in the next major release. The corresponding standard interfaces remain available. ([#4066](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4066)) - `ERC1820Implementer`: The `ERC1820` pseudo-introspection mechanism is no longer supported by OpenZeppelin. Our implementation is now deprecated and will be removed in the next major release. The corresponding standard interfaces remain available. ([#4066](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4066)) +## 4.8.2 (2023-03-02) + +- `ERC721Consecutive`: Fixed a bug when `_mintConsecutive` is used for batches of size 1 that could lead to balance overflow. Refer to the breaking changes section in the changelog for a note on the behavior of `ERC721._beforeTokenTransfer`. + ### Breaking changes - `ERC721`: The internal function `_beforeTokenTransfer` no longer updates balances, which it previously did when `batchSize` was greater than 1. This change has no consequence unless a custom ERC721 extension is explicitly invoking `_beforeTokenTransfer`. Balance updates in extensions must now be done explicitly using `__unsafe_increaseBalance`, with a name that indicates that there is an invariant that has to be manually verified. diff --git a/contracts/governance/TimelockController.sol b/contracts/governance/TimelockController.sol index 615b4c0e3..18ca81e5f 100644 --- a/contracts/governance/TimelockController.sol +++ b/contracts/governance/TimelockController.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (governance/TimelockController.sol) +// OpenZeppelin Contracts (last updated v4.8.2) (governance/TimelockController.sol) pragma solidity ^0.8.0; diff --git a/contracts/package.json b/contracts/package.json index e3e608341..55e70b179 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,7 +1,7 @@ { "name": "@openzeppelin/contracts", "description": "Secure Smart Contract library for Solidity", - "version": "4.8.0", + "version": "4.8.2", "files": [ "**/*.sol", "/build/contracts/*.json", diff --git a/contracts/proxy/utils/Initializable.sol b/contracts/proxy/utils/Initializable.sol index 638dbe353..3c898ec5a 100644 --- a/contracts/proxy/utils/Initializable.sol +++ b/contracts/proxy/utils/Initializable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (proxy/utils/Initializable.sol) +// OpenZeppelin Contracts (last updated v4.8.1) (proxy/utils/Initializable.sol) pragma solidity ^0.8.2; diff --git a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol index 66ad55e7d..07bb3901a 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (token/ERC1155/extensions/ERC1155Pausable.sol) +// OpenZeppelin Contracts (last updated v4.8.2) (token/ERC1155/extensions/ERC1155Pausable.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/extensions/ERC20Pausable.sol b/contracts/token/ERC20/extensions/ERC20Pausable.sol index e011f08c0..36cc30ce4 100644 --- a/contracts/token/ERC20/extensions/ERC20Pausable.sol +++ b/contracts/token/ERC20/extensions/ERC20Pausable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/ERC20Pausable.sol) +// OpenZeppelin Contracts (last updated v4.8.2) (token/ERC20/extensions/ERC20Pausable.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index 273bb8c50..f78938e6d 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/extensions/ERC20Votes.sol) +// OpenZeppelin Contracts (last updated v4.8.1) (token/ERC20/extensions/ERC20Votes.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index 3846cbbbb..40e9cf2b3 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/extensions/ERC4626.sol) +// OpenZeppelin Contracts (last updated v4.8.1) (token/ERC20/extensions/ERC4626.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index ea3fffe3f..428338d90 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/ERC721.sol) +// OpenZeppelin Contracts (last updated v4.8.2) (token/ERC721/ERC721.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC721/extensions/ERC721Consecutive.sol b/contracts/token/ERC721/extensions/ERC721Consecutive.sol index e8610fdf8..9451c8c59 100644 --- a/contracts/token/ERC721/extensions/ERC721Consecutive.sol +++ b/contracts/token/ERC721/extensions/ERC721Consecutive.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/extensions/ERC721Consecutive.sol) +// OpenZeppelin Contracts (last updated v4.8.2) (token/ERC721/extensions/ERC721Consecutive.sol) pragma solidity ^0.8.0; diff --git a/contracts/token/ERC721/extensions/ERC721Pausable.sol b/contracts/token/ERC721/extensions/ERC721Pausable.sol index cdd729b26..e8eb4b103 100644 --- a/contracts/token/ERC721/extensions/ERC721Pausable.sol +++ b/contracts/token/ERC721/extensions/ERC721Pausable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/extensions/ERC721Pausable.sol) +// OpenZeppelin Contracts (last updated v4.8.2) (token/ERC721/extensions/ERC721Pausable.sol) pragma solidity ^0.8.0; diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index 025b13d0a..7d7905fd1 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/Checkpoints.sol) +// OpenZeppelin Contracts (last updated v4.8.1) (utils/Checkpoints.sol) // This file was procedurally generated from scripts/generate/templates/Checkpoints.js. pragma solidity ^0.8.0; diff --git a/contracts/utils/introspection/ERC165Checker.sol b/contracts/utils/introspection/ERC165Checker.sol index db6939d83..fd51159cd 100644 --- a/contracts/utils/introspection/ERC165Checker.sol +++ b/contracts/utils/introspection/ERC165Checker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/introspection/ERC165Checker.sol) +// OpenZeppelin Contracts (last updated v4.8.2) (utils/introspection/ERC165Checker.sol) pragma solidity ^0.8.0; diff --git a/package-lock.json b/package-lock.json index fc3918807..f3081ffa9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openzeppelin-solidity", - "version": "4.8.0", + "version": "4.8.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "openzeppelin-solidity", - "version": "4.8.0", + "version": "4.8.2", "license": "MIT", "bin": { "openzeppelin-contracts-migrate-imports": "scripts/migrate-imports.js" diff --git a/package.json b/package.json index f0ee95ab0..282547535 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openzeppelin-solidity", "description": "Secure Smart Contract library for Solidity", - "version": "4.8.0", + "version": "4.8.2", "files": [ "/contracts/**/*.sol", "/build/contracts/*.json", From 7f028d69593342673492b0a0b1679e2a898cf1cf Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 3 Mar 2023 22:45:52 +0100 Subject: [PATCH 069/197] Fix empty short string encoding (#4088) Co-authored-by: Francisco --- contracts/utils/ShortStrings.sol | 13 ++++++++++--- test/utils/ShortStrings.test.js | 14 +++++++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/contracts/utils/ShortStrings.sol b/contracts/utils/ShortStrings.sol index 9f253df82..2f891aaa3 100644 --- a/contracts/utils/ShortStrings.sol +++ b/contracts/utils/ShortStrings.sol @@ -32,7 +32,10 @@ type ShortString is bytes32; * ``` */ library ShortStrings { + bytes32 private constant _FALLBACK_SENTINEL = 0x00000000000000000000000000000000000000000000000000000000000000FF; + error StringTooLong(string str); + error InvalidShortString(); /** * @dev Encode a string of at most 31 chars into a `ShortString`. @@ -66,7 +69,11 @@ library ShortStrings { * @dev Return the length of a `ShortString`. */ function length(ShortString sstr) internal pure returns (uint256) { - return uint256(ShortString.unwrap(sstr)) & 0xFF; + uint256 result = uint256(ShortString.unwrap(sstr)) & 0xFF; + if (result > 31) { + revert InvalidShortString(); + } + return result; } /** @@ -77,7 +84,7 @@ library ShortStrings { return toShortString(value); } else { StorageSlot.getStringSlot(store).value = value; - return ShortString.wrap(0); + return ShortString.wrap(_FALLBACK_SENTINEL); } } @@ -85,7 +92,7 @@ library ShortStrings { * @dev Decode a string that was encoded to `ShortString` or written to storage using {setWithFallback}. */ function toStringWithFallback(ShortString value, string storage store) internal pure returns (string memory) { - if (length(value) > 0) { + if (ShortString.unwrap(value) != _FALLBACK_SENTINEL) { return toString(value); } else { return store; diff --git a/test/utils/ShortStrings.test.js b/test/utils/ShortStrings.test.js index f41084a7e..27d52a5d7 100644 --- a/test/utils/ShortStrings.test.js +++ b/test/utils/ShortStrings.test.js @@ -3,9 +3,12 @@ const { expectRevertCustomError } = require('../helpers/customError'); const ShortStrings = artifacts.require('$ShortStrings'); +function length(sstr) { + return parseInt(sstr.slice(64), 16); +} + function decode(sstr) { - const length = parseInt(sstr.slice(64), 16); - return web3.utils.toUtf8(sstr).slice(0, length); + return web3.utils.toUtf8(sstr).slice(0, length(sstr)); } contract('ShortStrings', function () { @@ -34,7 +37,12 @@ contract('ShortStrings', function () { const { logs } = await this.mock.$toShortStringWithFallback(str, 0); const { ret0 } = logs.find(({ event }) => event == 'return$toShortStringWithFallback').args; - expect(await this.mock.$toString(ret0)).to.be.equal(str.length < 32 ? str : ''); + const promise = this.mock.$toString(ret0); + if (str.length < 32) { + expect(await promise).to.be.equal(str); + } else { + await expectRevertCustomError(promise, 'InvalidShortString()'); + } const recovered = await this.mock.$toStringWithFallback(ret0, 0); expect(recovered).to.be.equal(str); From de520fe25a75fc9c1e51c4988bc508d8a58da192 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 6 Mar 2023 21:44:58 +0100 Subject: [PATCH 070/197] Add `byteLengthWithFallback` to ShortStrings (#4089) Co-authored-by: Francisco --- contracts/utils/ShortStrings.sol | 18 ++++++++++++++++-- test/utils/ShortStrings.test.js | 5 ++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/contracts/utils/ShortStrings.sol b/contracts/utils/ShortStrings.sol index 2f891aaa3..450c7985e 100644 --- a/contracts/utils/ShortStrings.sol +++ b/contracts/utils/ShortStrings.sol @@ -54,7 +54,7 @@ library ShortStrings { * @dev Decode a `ShortString` back to a "normal" string. */ function toString(ShortString sstr) internal pure returns (string memory) { - uint256 len = length(sstr); + uint256 len = byteLength(sstr); // using `new string(len)` would work locally but is not memory safe. string memory str = new string(32); /// @solidity memory-safe-assembly @@ -68,7 +68,7 @@ library ShortStrings { /** * @dev Return the length of a `ShortString`. */ - function length(ShortString sstr) internal pure returns (uint256) { + function byteLength(ShortString sstr) internal pure returns (uint256) { uint256 result = uint256(ShortString.unwrap(sstr)) & 0xFF; if (result > 31) { revert InvalidShortString(); @@ -98,4 +98,18 @@ library ShortStrings { return store; } } + + /** + * @dev Return the length of a string that was encoded to `ShortString` or written to storage using {setWithFallback}. + * + * WARNING: This will return the "byte length" of the string. This may not reflect the actual length in terms of + * actual characters as the UTF-8 encoding of a single character can span over multiple bytes. + */ + function byteLengthWithFallback(ShortString value, string storage store) internal view returns (uint256) { + if (ShortString.unwrap(value) != _FALLBACK_SENTINEL) { + return byteLength(value); + } else { + return bytes(store).length; + } + } } diff --git a/test/utils/ShortStrings.test.js b/test/utils/ShortStrings.test.js index 27d52a5d7..f5cd82fbd 100644 --- a/test/utils/ShortStrings.test.js +++ b/test/utils/ShortStrings.test.js @@ -23,7 +23,7 @@ contract('ShortStrings', function () { const encoded = await this.mock.$toShortString(str); expect(decode(encoded)).to.be.equal(str); - const length = await this.mock.$length(encoded); + const length = await this.mock.$byteLength(encoded); expect(length.toNumber()).to.be.equal(str.length); const decoded = await this.mock.$toString(encoded); @@ -44,6 +44,9 @@ contract('ShortStrings', function () { await expectRevertCustomError(promise, 'InvalidShortString()'); } + const length = await this.mock.$byteLengthWithFallback(ret0, 0); + expect(length.toNumber()).to.be.equal(str.length); + const recovered = await this.mock.$toStringWithFallback(ret0, 0); expect(recovered).to.be.equal(str); }); From 4fb6833e325658946c2185862b8e57e32f3683bc Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 6 Mar 2023 22:31:48 +0100 Subject: [PATCH 071/197] Formal verification using Certora (#4084) Co-authored-by: Francisco Giordano --- .github/workflows/formal-verification.yml | 53 +++ certora/.gitignore | 1 + certora/Makefile | 58 ++- certora/applyHarness.patch | 101 ----- certora/harnesses/AccessControlHarness.sol | 7 + certora/harnesses/ERC20FlashMintHarness.sol | 36 ++ certora/harnesses/ERC20PermitHarness.sol | 18 + certora/harnesses/ERC20VotesHarness.sol | 28 -- .../harnesses/ERC3156FlashBorrowerHarness.sol | 13 + .../harnesses/WizardControlFirstPriority.sol | 150 ------- certora/harnesses/WizardFirstTry.sol | 141 ------ certora/munged/.gitignore | 2 - certora/run.js | 109 +++++ certora/scripts/Governor.sh | 10 - .../GovernorCountingSimple-counting.sh | 10 - certora/scripts/WizardControlFirstPriority.sh | 12 - certora/scripts/WizardFirstTry.sh | 10 - certora/scripts/sanity.sh | 14 - certora/scripts/verifyAll.sh | 39 -- certora/specs.json | 22 + certora/specs/AccessControl.spec | 114 +++++ certora/specs/ERC20.spec | 414 ++++++++++++++++++ certora/specs/ERC20FlashMint.spec | 48 ++ certora/specs/GovernorBase.spec | 333 -------------- certora/specs/GovernorCountingSimple.spec | 221 ---------- certora/specs/RulesInProgress.spec | 139 ------ certora/specs/helpers.spec | 1 + certora/specs/methods/IERC20.spec | 11 + certora/specs/methods/IERC2612.spec | 5 + certora/specs/methods/IERC3156.spec | 5 + certora/specs/sanity.spec | 14 - package-lock.json | 189 ++++---- package.json | 1 + requirements.txt | 1 + 34 files changed, 1011 insertions(+), 1319 deletions(-) create mode 100644 .github/workflows/formal-verification.yml create mode 100644 certora/.gitignore delete mode 100644 certora/applyHarness.patch create mode 100644 certora/harnesses/AccessControlHarness.sol create mode 100644 certora/harnesses/ERC20FlashMintHarness.sol create mode 100644 certora/harnesses/ERC20PermitHarness.sol delete mode 100644 certora/harnesses/ERC20VotesHarness.sol create mode 100644 certora/harnesses/ERC3156FlashBorrowerHarness.sol delete mode 100644 certora/harnesses/WizardControlFirstPriority.sol delete mode 100644 certora/harnesses/WizardFirstTry.sol delete mode 100644 certora/munged/.gitignore create mode 100644 certora/run.js delete mode 100755 certora/scripts/Governor.sh delete mode 100644 certora/scripts/GovernorCountingSimple-counting.sh delete mode 100644 certora/scripts/WizardControlFirstPriority.sh delete mode 100644 certora/scripts/WizardFirstTry.sh delete mode 100644 certora/scripts/sanity.sh delete mode 100644 certora/scripts/verifyAll.sh create mode 100644 certora/specs.json create mode 100644 certora/specs/AccessControl.spec create mode 100644 certora/specs/ERC20.spec create mode 100644 certora/specs/ERC20FlashMint.spec delete mode 100644 certora/specs/GovernorBase.spec delete mode 100644 certora/specs/GovernorCountingSimple.spec delete mode 100644 certora/specs/RulesInProgress.spec create mode 100644 certora/specs/helpers.spec create mode 100644 certora/specs/methods/IERC20.spec create mode 100644 certora/specs/methods/IERC2612.spec create mode 100644 certora/specs/methods/IERC3156.spec delete mode 100644 certora/specs/sanity.spec create mode 100644 requirements.txt diff --git a/.github/workflows/formal-verification.yml b/.github/workflows/formal-verification.yml new file mode 100644 index 000000000..6c9935ad4 --- /dev/null +++ b/.github/workflows/formal-verification.yml @@ -0,0 +1,53 @@ +name: formal verification + +on: + push: + branches: + - master + - release-v* + pull_request: {} + workflow_dispatch: {} + +env: + PIP_VERSION: '3.10' + JAVA_VERSION: '11' + SOLC_VERSION: '0.8.19' + +jobs: + apply-diff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Apply patches + run: make -C certora apply + + verify: + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'formal-verification') + steps: + - uses: actions/checkout@v3 + - name: Set up environment + uses: ./.github/actions/setup + - name: Install python + uses: actions/setup-python@v4 + with: + python-version: ${{ env.PIP_VERSION }} + cache: 'pip' + - name: Install python packages + run: pip install -r requirements.txt + - name: Install java + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + - name: Install solc + run: | + wget https://github.com/ethereum/solidity/releases/download/v${{ env.SOLC_VERSION }}/solc-static-linux + sudo mv solc-static-linux /usr/local/bin/solc + chmod +x /usr/local/bin/solc + - name: Verify specification + run: | + make -C certora apply + node certora/run.js >> "$GITHUB_STEP_SUMMARY" + env: + CERTORAKEY: ${{ secrets.CERTORAKEY }} diff --git a/certora/.gitignore b/certora/.gitignore new file mode 100644 index 000000000..893adcd31 --- /dev/null +++ b/certora/.gitignore @@ -0,0 +1 @@ +patched diff --git a/certora/Makefile b/certora/Makefile index bbbddbcab..50fd6da08 100644 --- a/certora/Makefile +++ b/certora/Makefile @@ -1,24 +1,54 @@ default: help -PATCH = applyHarness.patch -CONTRACTS_DIR = ../contracts -MUNGED_DIR = munged +SRC := ../contracts +DST := patched +DIFF := diff +SRCS := $(shell find $(SRC) -type f) +DSTS := $(shell find $(DST) -type f) +DIFFS := $(shell find $(DIFF) -type f) +############################################################################### +# Apply all patches in the $DIFF folder to the $DST folder +apply: $(DST) $(patsubst $(DIFF)/%.patch,$(DST)/%,$(subst _,/,$(DIFFS))) + +# Reset the $DST folder +$(DST): FORCE + @rm -rf $@ + @cp -r $(SRC) $@ + +# Update a solidity file in the $DST directory using the corresponding patch +$(DST)/%.sol: FORCE + @echo Applying patch to $@ + @patch -p0 -d $(DST) < $(patsubst $(DST)_%,$(DIFF)/%.patch,$(subst /,_,$@)) + +############################################################################### +# Record all difference between $SRC and $DST in patches +record: $(DIFF) $(patsubst %,$(DIFF)/%.patch,$(subst /,_,$(subst $(SRC)/,,$(SRCS)) $(subst $(DST)/,,$(DSTS)))) + +# Create the $DIFF folder +$(DIFF): FORCE + @rm -rf $@ + @mkdir $@ + +# Create the patch file by comparing the source and the destination +$(DIFF)/%.patch: FORCE + @echo Generating patch $@ + @diff -ruN \ + $(patsubst $(DIFF)/%.patch,$(SRC)/%,$(subst _,/,$@)) \ + $(patsubst $(DIFF)/%.patch,$(DST)/%,$(subst _,/,$@)) \ + | sed 's+$(SRC)/++g' \ + | sed 's+$(DST)/++g' \ + > $@ + @[ -s $@ ] || rm $@ + +############################################################################### help: @echo "usage:" + @echo " make apply: create $(DST) directory by applying the patches to $(SRC)" + @echo " make record: record the patches capturing the differences between $(SRC) and $(DST)" @echo " make clean: remove all generated files (those ignored by git)" - @echo " make $(MUNGED_DIR): create $(MUNGED_DIR) directory by applying the patch file to $(CONTRACTS_DIR)" - @echo " make record: record a new patch file capturing the differences between $(CONTRACTS_DIR) and $(MUNGED_DIR)" - -munged: $(wildcard $(CONTRACTS_DIR)/*.sol) $(PATCH) - rm -rf $@ - cp -r $(CONTRACTS_DIR) $@ - patch -p0 -d $@ < $(PATCH) - -record: - diff -ruN $(CONTRACTS_DIR) $(MUNGED_DIR) | sed 's+../contracts/++g' | sed 's+munged/++g' > $(PATCH) clean: git clean -fdX - touch $(PATCH) +FORCE: ; diff --git a/certora/applyHarness.patch b/certora/applyHarness.patch deleted file mode 100644 index 0fbe9acad..000000000 --- a/certora/applyHarness.patch +++ /dev/null @@ -1,101 +0,0 @@ -diff -ruN .gitignore .gitignore ---- .gitignore 1969-12-31 19:00:00.000000000 -0500 -+++ .gitignore 2021-12-09 14:46:33.923637220 -0500 -@@ -0,0 +1,2 @@ -+* -+!.gitignore -diff -ruN governance/compatibility/GovernorCompatibilityBravo.sol governance/compatibility/GovernorCompatibilityBravo.sol ---- governance/compatibility/GovernorCompatibilityBravo.sol 2021-12-03 15:24:56.523654357 -0500 -+++ governance/compatibility/GovernorCompatibilityBravo.sol 2021-12-09 14:46:33.923637220 -0500 -@@ -245,7 +245,7 @@ - /** - * @dev See {Governor-_quorumReached}. In this module, only forVotes count toward the quorum. - */ -- function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) { -+ function _quorumReached(uint256 proposalId) public view virtual override returns (bool) { // HARNESS: changed to public from internal - ProposalDetails storage details = _proposalDetails[proposalId]; - return quorum(proposalSnapshot(proposalId)) <= details.forVotes; - } -@@ -253,7 +253,7 @@ - /** - * @dev See {Governor-_voteSucceeded}. In this module, the forVotes must be scritly over the againstVotes. - */ -- function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) { -+ function _voteSucceeded(uint256 proposalId) public view virtual override returns (bool) { // HARNESS: changed to public from internal - ProposalDetails storage details = _proposalDetails[proposalId]; - return details.forVotes > details.againstVotes; - } -diff -ruN governance/extensions/GovernorCountingSimple.sol governance/extensions/GovernorCountingSimple.sol ---- governance/extensions/GovernorCountingSimple.sol 2021-12-03 15:24:56.523654357 -0500 -+++ governance/extensions/GovernorCountingSimple.sol 2021-12-09 14:46:33.923637220 -0500 -@@ -64,7 +64,7 @@ - /** - * @dev See {Governor-_quorumReached}. - */ -- function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) { -+ function _quorumReached(uint256 proposalId) public view virtual override returns (bool) { - ProposalVote storage proposalvote = _proposalVotes[proposalId]; - - return quorum(proposalSnapshot(proposalId)) <= proposalvote.forVotes + proposalvote.abstainVotes; -@@ -73,7 +73,7 @@ - /** - * @dev See {Governor-_voteSucceeded}. In this module, the forVotes must be strictly over the againstVotes. - */ -- function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) { -+ function _voteSucceeded(uint256 proposalId) public view virtual override returns (bool) { - ProposalVote storage proposalvote = _proposalVotes[proposalId]; - - return proposalvote.forVotes > proposalvote.againstVotes; -diff -ruN governance/extensions/GovernorTimelockControl.sol governance/extensions/GovernorTimelockControl.sol ---- governance/extensions/GovernorTimelockControl.sol 2021-12-03 15:24:56.523654357 -0500 -+++ governance/extensions/GovernorTimelockControl.sol 2021-12-09 14:46:33.923637220 -0500 -@@ -111,7 +111,7 @@ - bytes[] memory calldatas, - bytes32 descriptionHash - ) internal virtual override { -- _timelock.executeBatch{value: msg.value}(targets, values, calldatas, 0, descriptionHash); -+ _timelock.executeBatch{value: msg.value}(targets, values, calldatas, 0, descriptionHash); - } - - /** -diff -ruN governance/Governor.sol governance/Governor.sol ---- governance/Governor.sol 2021-12-03 15:24:56.523654357 -0500 -+++ governance/Governor.sol 2021-12-09 14:46:56.411503587 -0500 -@@ -38,8 +38,8 @@ - - string private _name; - -- mapping(uint256 => ProposalCore) private _proposals; -- -+ mapping(uint256 => ProposalCore) public _proposals; -+ - /** - * @dev Restrict access to governor executing address. Some module might override the _executor function to make - * sure this modifier is consistent with the execution model. -@@ -167,12 +167,12 @@ - /** - * @dev Amount of votes already cast passes the threshold limit. - */ -- function _quorumReached(uint256 proposalId) internal view virtual returns (bool); -+ function _quorumReached(uint256 proposalId) public view virtual returns (bool); // HARNESS: changed to public from internal - - /** - * @dev Is the proposal successful or not. - */ -- function _voteSucceeded(uint256 proposalId) internal view virtual returns (bool); -+ function _voteSucceeded(uint256 proposalId) public view virtual returns (bool); // HARNESS: changed to public from internal - - /** - * @dev Register a vote with a given support and voting weight. -diff -ruN token/ERC20/extensions/ERC20Votes.sol token/ERC20/extensions/ERC20Votes.sol ---- token/ERC20/extensions/ERC20Votes.sol 2021-12-03 15:24:56.527654330 -0500 -+++ token/ERC20/extensions/ERC20Votes.sol 2021-12-09 14:46:33.927637196 -0500 -@@ -84,7 +84,7 @@ - * - * - `blockNumber` must have been already mined - */ -- function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) { -+ function getPastVotes(address account, uint256 blockNumber) public view virtual returns (uint256) { - require(blockNumber < block.number, "ERC20Votes: block not yet mined"); - return _checkpointsLookup(_checkpoints[account], blockNumber); - } diff --git a/certora/harnesses/AccessControlHarness.sol b/certora/harnesses/AccessControlHarness.sol new file mode 100644 index 000000000..0cb1e55d4 --- /dev/null +++ b/certora/harnesses/AccessControlHarness.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../patched/access/AccessControl.sol"; + +contract AccessControlHarness is AccessControl {} diff --git a/certora/harnesses/ERC20FlashMintHarness.sol b/certora/harnesses/ERC20FlashMintHarness.sol new file mode 100644 index 000000000..119eb4768 --- /dev/null +++ b/certora/harnesses/ERC20FlashMintHarness.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../patched/token/ERC20/ERC20.sol"; +import "../patched/token/ERC20/extensions/ERC20Permit.sol"; +import "../patched/token/ERC20/extensions/ERC20FlashMint.sol"; + +contract ERC20FlashMintHarness is ERC20, ERC20Permit, ERC20FlashMint { + uint256 someFee; + address someFeeReceiver; + + constructor(string memory name, string memory symbol) ERC20(name, symbol) ERC20Permit(name) {} + + function mint(address account, uint256 amount) external { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external { + _burn(account, amount); + } + + // public accessor + function flashFeeReceiver() public view returns (address) { + return someFeeReceiver; + } + + // internal hook + function _flashFee(address, uint256) internal view override returns (uint256) { + return someFee; + } + + function _flashFeeReceiver() internal view override returns (address) { + return someFeeReceiver; + } +} diff --git a/certora/harnesses/ERC20PermitHarness.sol b/certora/harnesses/ERC20PermitHarness.sol new file mode 100644 index 000000000..cc66358b2 --- /dev/null +++ b/certora/harnesses/ERC20PermitHarness.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../patched/token/ERC20/ERC20.sol"; +import "../patched/token/ERC20/extensions/ERC20Permit.sol"; + +contract ERC20PermitHarness is ERC20, ERC20Permit { + constructor(string memory name, string memory symbol) ERC20(name, symbol) ERC20Permit(name) {} + + function mint(address account, uint256 amount) external { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external { + _burn(account, amount); + } +} diff --git a/certora/harnesses/ERC20VotesHarness.sol b/certora/harnesses/ERC20VotesHarness.sol deleted file mode 100644 index 5067ecfba..000000000 --- a/certora/harnesses/ERC20VotesHarness.sol +++ /dev/null @@ -1,28 +0,0 @@ -import "../munged/token/ERC20/extensions/ERC20Votes.sol"; - -contract ERC20VotesHarness is ERC20Votes { - constructor(string memory name, string memory symbol) ERC20Permit(name) ERC20(name, symbol) {} - - mapping(address => mapping(uint256 => uint256)) public _getPastVotes; - - function _afterTokenTransfer( - address from, - address to, - uint256 amount - ) internal virtual override { - super._afterTokenTransfer(from, to, amount); - _getPastVotes[from][block.number] -= amount; - _getPastVotes[to][block.number] += amount; - } - - /** - * @dev Change delegation for `delegator` to `delegatee`. - * - * Emits events {DelegateChanged} and {DelegateVotesChanged}. - */ - function _delegate(address delegator, address delegatee) internal virtual override{ - super._delegate(delegator, delegatee); - _getPastVotes[delegator][block.number] -= balanceOf(delegator); - _getPastVotes[delegatee][block.number] += balanceOf(delegator); - } -} diff --git a/certora/harnesses/ERC3156FlashBorrowerHarness.sol b/certora/harnesses/ERC3156FlashBorrowerHarness.sol new file mode 100644 index 000000000..0ad29a16e --- /dev/null +++ b/certora/harnesses/ERC3156FlashBorrowerHarness.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +import "../patched/interfaces/IERC3156FlashBorrower.sol"; + +pragma solidity ^0.8.0; + +contract ERC3156FlashBorrowerHarness is IERC3156FlashBorrower { + bytes32 somethingToReturn; + + function onFlashLoan(address, address, uint256, uint256, bytes calldata) external view override returns (bytes32) { + return somethingToReturn; + } +} diff --git a/certora/harnesses/WizardControlFirstPriority.sol b/certora/harnesses/WizardControlFirstPriority.sol deleted file mode 100644 index 5ae7fe066..000000000 --- a/certora/harnesses/WizardControlFirstPriority.sol +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "../munged/governance/Governor.sol"; -import "../munged/governance/extensions/GovernorCountingSimple.sol"; -import "../munged/governance/extensions/GovernorVotes.sol"; -import "../munged/governance/extensions/GovernorVotesQuorumFraction.sol"; -import "../munged/governance/extensions/GovernorTimelockControl.sol"; -import "../munged/governance/extensions/GovernorProposalThreshold.sol"; - -/* -Wizard options: -ProposalThreshhold = 10 -ERC20Votes -TimelockController -*/ - -contract WizardControlFirstPriority is Governor, GovernorProposalThreshold, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl { - constructor(ERC20Votes _token, TimelockController _timelock, string memory name, uint256 quorumFraction) - Governor(name) - GovernorVotes(_token) - GovernorVotesQuorumFraction(quorumFraction) - GovernorTimelockControl(_timelock) - {} - - //HARNESS - - function isExecuted(uint256 proposalId) public view returns (bool) { - return _proposals[proposalId].executed; - } - - function isCanceled(uint256 proposalId) public view returns (bool) { - return _proposals[proposalId].canceled; - } - - uint256 _votingDelay; - - uint256 _votingPeriod; - - uint256 _proposalThreshold; - - mapping(uint256 => uint256) public ghost_sum_vote_power_by_id; - - function _castVote( - uint256 proposalId, - address account, - uint8 support, - string memory reason - ) internal override virtual returns (uint256) { - - uint256 deltaWeight = super._castVote(proposalId, account, support, reason); //HARNESS - ghost_sum_vote_power_by_id[proposalId] += deltaWeight; - - return deltaWeight; - } - - function snapshot(uint256 proposalId) public view returns (uint64) { - return _proposals[proposalId].voteStart._deadline; - } - - - function getExecutor() public view returns (address){ - return _executor(); - } - - // original code, harnessed - - function votingDelay() public view override returns (uint256) { // HARNESS: pure -> view - return _votingDelay; // HARNESS: parametric - } - - function votingPeriod() public view override returns (uint256) { // HARNESS: pure -> view - return _votingPeriod; // HARNESS: parametric - } - - function proposalThreshold() public view override returns (uint256) { // HARNESS: pure -> view - return _proposalThreshold; // HARNESS: parametric - } - - // original code, not harnessed - // The following functions are overrides required by Solidity. - - function quorum(uint256 blockNumber) - public - view - override(IGovernor, GovernorVotesQuorumFraction) - returns (uint256) - { - return super.quorum(blockNumber); - } - - function getVotes(address account, uint256 blockNumber) - public - view - override(IGovernor, GovernorVotes) - returns (uint256) - { - return super.getVotes(account, blockNumber); - } - - function state(uint256 proposalId) - public - view - override(Governor, GovernorTimelockControl) - returns (ProposalState) - { - return super.state(proposalId); - } - - function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) - public - override(Governor, GovernorProposalThreshold, IGovernor) - returns (uint256) - { - return super.propose(targets, values, calldatas, description); - } - - function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) - internal - override(Governor, GovernorTimelockControl) - { - super._execute(proposalId, targets, values, calldatas, descriptionHash); - } - - function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) - internal - override(Governor, GovernorTimelockControl) - returns (uint256) - { - return super._cancel(targets, values, calldatas, descriptionHash); - } - - function _executor() - internal - view - override(Governor, GovernorTimelockControl) - returns (address) - { - return super._executor(); - } - - function supportsInterface(bytes4 interfaceId) - public - view - override(Governor, GovernorTimelockControl) - returns (bool) - { - return super.supportsInterface(interfaceId); - } -} diff --git a/certora/harnesses/WizardFirstTry.sol b/certora/harnesses/WizardFirstTry.sol deleted file mode 100644 index 83fece04f..000000000 --- a/certora/harnesses/WizardFirstTry.sol +++ /dev/null @@ -1,141 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "../munged/governance/Governor.sol"; -import "../munged/governance/extensions/GovernorCountingSimple.sol"; -import "../munged/governance/extensions/GovernorVotes.sol"; -import "../munged/governance/extensions/GovernorVotesQuorumFraction.sol"; -import "../munged/governance/extensions/GovernorTimelockCompound.sol"; - -/* -Wizard options: -ERC20Votes -TimelockCompound -*/ - -contract WizardFirstTry is Governor, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockCompound { - constructor(ERC20Votes _token, ICompoundTimelock _timelock, string memory name, uint256 quorumFraction) - Governor(name) - GovernorVotes(_token) - GovernorVotesQuorumFraction(quorumFraction) - GovernorTimelockCompound(_timelock) - {} - - //HARNESS - - function isExecuted(uint256 proposalId) public view returns (bool) { - return _proposals[proposalId].executed; - } - - function isCanceled(uint256 proposalId) public view returns (bool) { - return _proposals[proposalId].canceled; - } - - function snapshot(uint256 proposalId) public view returns (uint64) { - return _proposals[proposalId].voteStart._deadline; - } - - function getExecutor() public view returns (address){ - return _executor(); - } - - uint256 _votingDelay; - - uint256 _votingPeriod; - - mapping(uint256 => uint256) public ghost_sum_vote_power_by_id; - - function _castVote( - uint256 proposalId, - address account, - uint8 support, - string memory reason - ) internal override virtual returns (uint256) { - - uint256 deltaWeight = super._castVote(proposalId, account, support, reason); //HARNESS - ghost_sum_vote_power_by_id[proposalId] += deltaWeight; - - return deltaWeight; - } - - // original code, harnessed - - function votingDelay() public view override virtual returns (uint256) { // HARNESS: pure -> view - return _votingDelay; // HARNESS: parametric - } - - function votingPeriod() public view override virtual returns (uint256) { // HARNESS: pure -> view - return _votingPeriod; // HARNESS: parametric - } - - // original code, not harnessed - // The following functions are overrides required by Solidity. - - function quorum(uint256 blockNumber) - public - view - override(IGovernor, GovernorVotesQuorumFraction) - returns (uint256) - { - return super.quorum(blockNumber); - } - - function getVotes(address account, uint256 blockNumber) - public - view - override(IGovernor, GovernorVotes) - returns (uint256) - { - return super.getVotes(account, blockNumber); - } - - function state(uint256 proposalId) - public - view - override(Governor, GovernorTimelockCompound) - returns (ProposalState) - { - return super.state(proposalId); - } - - function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) - public - override(Governor, IGovernor) - returns (uint256) - { - return super.propose(targets, values, calldatas, description); - } - - function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) - internal - override(Governor, GovernorTimelockCompound) - { - super._execute(proposalId, targets, values, calldatas, descriptionHash); - } - - function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) - internal - override(Governor, GovernorTimelockCompound) - returns (uint256) - { - return super._cancel(targets, values, calldatas, descriptionHash); - } - - function _executor() - internal - view - override(Governor, GovernorTimelockCompound) - returns (address) - { - return super._executor(); - } - - function supportsInterface(bytes4 interfaceId) - public - view - override(Governor, GovernorTimelockCompound) - returns (bool) - { - return super.supportsInterface(interfaceId); - } -} diff --git a/certora/munged/.gitignore b/certora/munged/.gitignore deleted file mode 100644 index d6b7ef32c..000000000 --- a/certora/munged/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/certora/run.js b/certora/run.js new file mode 100644 index 000000000..1da238d4f --- /dev/null +++ b/certora/run.js @@ -0,0 +1,109 @@ +#!/usr/bin/env node + +// USAGE: +// node certora/run.js [[CONTRACT_NAME:]SPEC_NAME] [OPTIONS...] +// EXAMPLES: +// node certora/run.js AccessControl +// node certora/run.js AccessControlHarness:AccessControl + +const MAX_PARALLEL = 4; + +const specs = require(__dirname + '/specs.json'); + +const proc = require('child_process'); +const { PassThrough } = require('stream'); +const events = require('events'); +const limit = require('p-limit')(MAX_PARALLEL); + +let [, , request = '', ...extraOptions] = process.argv; +if (request.startsWith('-')) { + extraOptions.unshift(request); + request = ''; +} +const [reqSpec, reqContract] = request.split(':').reverse(); + +for (const { spec, contract, files, options = [] } of Object.values(specs)) { + if ((!reqSpec || reqSpec === spec) && (!reqContract || reqContract === contract)) { + limit(runCertora, spec, contract, files, [...options, ...extraOptions]); + } +} + +// Run certora, aggregate the output and print it at the end +async function runCertora(spec, contract, files, options = []) { + const args = [...files, '--verify', `${contract}:certora/specs/${spec}.spec`, ...options]; + const child = proc.spawn('certoraRun', args); + + const stream = new PassThrough(); + const output = collect(stream); + + child.stdout.pipe(stream, { end: false }); + child.stderr.pipe(stream, { end: false }); + + // as soon as we have a jobStatus link, print it + stream.on('data', function logStatusUrl(data) { + const urls = data.toString('utf8').match(/https?:\S*/g); + for (const url of urls ?? []) { + if (url.includes('/jobStatus/')) { + console.error(`[${spec}] ${url}`); + stream.off('data', logStatusUrl); + break; + } + } + }); + + // wait for process end + const [code, signal] = await events.once(child, 'exit'); + + // error + if (code || signal) { + console.error(`[${spec}] Exited with code ${code || signal}`); + process.exitCode = 1; + } + + // get all output + stream.end(); + + // write results in markdown format + writeEntry(spec, contract, code || signal, (await output).match(/https:\S*/)[0]); + + // write all details + console.error(`+ certoraRun ${args.join(' ')}\n` + (await output)); +} + +// Collects stream data into a string +async function collect(stream) { + const buffers = []; + for await (const data of stream) { + const buf = Buffer.isBuffer(data) ? data : Buffer.from(data); + buffers.push(buf); + } + return Buffer.concat(buffers).toString('utf8'); +} + +// Formatting +let hasHeader = false; + +function formatRow(...array) { + return ['', ...array, ''].join(' | '); +} + +function writeHeader() { + console.log(formatRow('spec', 'contract', 'result', 'status', 'output')); + console.log(formatRow('-', '-', '-', '-', '-')); +} + +function writeEntry(spec, contract, success, url) { + if (!hasHeader) { + hasHeader = true; + writeHeader(); + } + console.log( + formatRow( + spec, + contract, + success ? ':x:' : ':heavy_check_mark:', + `[link](${url})`, + `[link](${url.replace('/jobStatus/', '/output/')})`, + ), + ); +} diff --git a/certora/scripts/Governor.sh b/certora/scripts/Governor.sh deleted file mode 100755 index 53ade5060..000000000 --- a/certora/scripts/Governor.sh +++ /dev/null @@ -1,10 +0,0 @@ -make -C certora munged - -certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/GovernorHarness.sol \ - --verify GovernorHarness:certora/specs/GovernorBase.spec \ - --solc solc8.0 \ - --staging shelly/forSasha \ - --optimistic_loop \ - --settings -copyLoopUnroll=4 \ - --rule voteStartBeforeVoteEnd \ - --msg "$1" diff --git a/certora/scripts/GovernorCountingSimple-counting.sh b/certora/scripts/GovernorCountingSimple-counting.sh deleted file mode 100644 index 9ed8fe34c..000000000 --- a/certora/scripts/GovernorCountingSimple-counting.sh +++ /dev/null @@ -1,10 +0,0 @@ -make -C certora munged - -certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/GovernorBasicHarness.sol \ - --verify GovernorBasicHarness:certora/specs/GovernorCountingSimple.spec \ - --solc solc8.2 \ - --staging shelly/forSasha \ - --optimistic_loop \ - --settings -copyLoopUnroll=4 \ - --rule hasVotedCorrelation \ - --msg "$1" diff --git a/certora/scripts/WizardControlFirstPriority.sh b/certora/scripts/WizardControlFirstPriority.sh deleted file mode 100644 index b815986ee..000000000 --- a/certora/scripts/WizardControlFirstPriority.sh +++ /dev/null @@ -1,12 +0,0 @@ -make -C certora munged - -certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/WizardControlFirstPriority.sol \ - --link WizardControlFirstPriority:token=ERC20VotesHarness \ - --verify WizardControlFirstPriority:certora/specs/GovernorBase.spec \ - --solc solc8.2 \ - --disableLocalTypeChecking \ - --staging shelly/forSasha \ - --optimistic_loop \ - --settings -copyLoopUnroll=4 \ - --rule canVoteDuringVotingPeriod \ - --msg "$1" diff --git a/certora/scripts/WizardFirstTry.sh b/certora/scripts/WizardFirstTry.sh deleted file mode 100644 index fd5a32ab4..000000000 --- a/certora/scripts/WizardFirstTry.sh +++ /dev/null @@ -1,10 +0,0 @@ -make -C certora munged - -certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/WizardFirstTry.sol \ - --verify WizardFirstTry:certora/specs/GovernorBase.spec \ - --solc solc8.2 \ - --staging shelly/forSasha \ - --optimistic_loop \ - --disableLocalTypeChecking \ - --settings -copyLoopUnroll=4 \ - --msg "$1" diff --git a/certora/scripts/sanity.sh b/certora/scripts/sanity.sh deleted file mode 100644 index 1c42bf67b..000000000 --- a/certora/scripts/sanity.sh +++ /dev/null @@ -1,14 +0,0 @@ -make -C certora munged - -for f in certora/harnesses/Wizard*.sol -do - echo "Processing $f" - file="$(basename $f)" - echo ${file%.*} - certoraRun certora/harnesses/$file \ - --verify ${file%.*}:certora/specs/sanity.spec "$@" \ - --solc solc8.2 --staging shelly/forSasha \ - --optimistic_loop \ - --msg "checking sanity on ${file%.*}" - --settings -copyLoopUnroll=4 -done diff --git a/certora/scripts/verifyAll.sh b/certora/scripts/verifyAll.sh deleted file mode 100644 index 9b2f11c8c..000000000 --- a/certora/scripts/verifyAll.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -make -C certora munged - -for contract in certora/harnesses/Wizard*.sol; -do - for spec in certora/specs/*.spec; - do - contractFile="$(basename $contract)" - specFile="$(basename $spec)" - if [[ "${specFile%.*}" != "RulesInProgress" ]]; - then - echo "Processing ${contractFile%.*} with $specFile" - if [[ "${contractFile%.*}" = *"WizardControl"* ]]; - then - certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/$contractFile \ - --link ${contractFile%.*}:token=ERC20VotesHarness \ - --verify ${contractFile%.*}:certora/specs/$specFile "$@" \ - --solc solc8.2 \ - --staging shelly/forSasha \ - --disableLocalTypeChecking \ - --optimistic_loop \ - --settings -copyLoopUnroll=4 \ - --send_only \ - --msg "checking $specFile on ${contractFile%.*}" - else - certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/$contractFile \ - --verify ${contractFile%.*}:certora/specs/$specFile "$@" \ - --solc solc8.2 \ - --staging shelly/forSasha \ - --disableLocalTypeChecking \ - --optimistic_loop \ - --settings -copyLoopUnroll=4 \ - --send_only \ - --msg "checking $specFile on ${contractFile%.*}" - fi - fi - done -done diff --git a/certora/specs.json b/certora/specs.json new file mode 100644 index 000000000..4d3e1fb90 --- /dev/null +++ b/certora/specs.json @@ -0,0 +1,22 @@ +[ + { + "spec": "AccessControl", + "contract": "AccessControlHarness", + "files": ["certora/harnesses/AccessControlHarness.sol"] + }, + { + "spec": "ERC20", + "contract": "ERC20PermitHarness", + "files": ["certora/harnesses/ERC20PermitHarness.sol"], + "options": ["--optimistic_loop"] + }, + { + "spec": "ERC20FlashMint", + "contract": "ERC20FlashMintHarness", + "files": [ + "certora/harnesses/ERC20FlashMintHarness.sol", + "certora/harnesses/ERC3156FlashBorrowerHarness.sol" + ], + "options": ["--optimistic_loop"] + } +] diff --git a/certora/specs/AccessControl.spec b/certora/specs/AccessControl.spec new file mode 100644 index 000000000..170d56fd4 --- /dev/null +++ b/certora/specs/AccessControl.spec @@ -0,0 +1,114 @@ +import "helpers.spec" + +methods { + hasRole(bytes32, address) returns(bool) envfree + getRoleAdmin(bytes32) returns(bytes32) envfree + grantRole(bytes32, address) + revokeRole(bytes32, address) + renounceRole(bytes32, address) +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Identify entrypoints: only grantRole, revokeRole and renounceRole can alter permissions │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyGrantCanGrant(env e, bytes32 role, address account) { + method f; calldataarg args; + + bool hasRoleBefore = hasRole(role, account); + f(e, args); + bool hasRoleAfter = hasRole(role, account); + + assert ( + !hasRoleBefore && + hasRoleAfter + ) => ( + f.selector == grantRole(bytes32, address).selector + ); + + assert ( + hasRoleBefore && + !hasRoleAfter + ) => ( + f.selector == revokeRole(bytes32, address).selector || + f.selector == renounceRole(bytes32, address).selector + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: grantRole only affects the specified user/role combo │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule grantRoleEffect(env e) { + require nonpayable(e); + + bytes32 role1; bytes32 role2; + address account1; address account2; + + bool isCallerAdmin = hasRole(getRoleAdmin(role2), e.msg.sender); + bool hasRoleBefore = hasRole(role1, account1); + + grantRole@withrevert(e, role2, account2); + assert !lastReverted <=> isCallerAdmin; + + bool hasRoleAfter = hasRole(role1, account1); + + assert ( + hasRoleBefore != hasRoleAfter + ) => ( + hasRoleAfter == true && role1 == role2 && account1 == account2 + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: revokeRole only affects the specified user/role combo │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule revokeRoleEffect(env e) { + require nonpayable(e); + + bytes32 role1; bytes32 role2; + address account1; address account2; + + bool isCallerAdmin = hasRole(getRoleAdmin(role2), e.msg.sender); + bool hasRoleBefore = hasRole(role1, account1); + + revokeRole@withrevert(e, role2, account2); + assert !lastReverted <=> isCallerAdmin; + + bool hasRoleAfter = hasRole(role1, account1); + + assert ( + hasRoleBefore != hasRoleAfter + ) => ( + hasRoleAfter == false && role1 == role2 && account1 == account2 + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: renounceRole only affects the specified user/role combo │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule renounceRoleEffect(env e) { + require nonpayable(e); + + bytes32 role1; bytes32 role2; + address account1; address account2; + + bool hasRoleBefore = hasRole(role1, account1); + + renounceRole@withrevert(e, role2, account2); + assert !lastReverted <=> account2 == e.msg.sender; + + bool hasRoleAfter = hasRole(role1, account1); + + assert ( + hasRoleBefore != hasRoleAfter + ) => ( + hasRoleAfter == false && role1 == role2 && account1 == account2 + ); +} diff --git a/certora/specs/ERC20.spec b/certora/specs/ERC20.spec new file mode 100644 index 000000000..85f95e706 --- /dev/null +++ b/certora/specs/ERC20.spec @@ -0,0 +1,414 @@ +import "helpers.spec" +import "methods/IERC20.spec" +import "methods/IERC2612.spec" + +methods { + // non standard ERC20 functions + increaseAllowance(address,uint256) returns (bool) + decreaseAllowance(address,uint256) returns (bool) + + // exposed for FV + mint(address,uint256) + burn(address,uint256) +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Ghost & hooks: sum of all balances │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +ghost sumOfBalances() returns uint256 { + init_state axiom sumOfBalances() == 0; +} + +hook Sstore _balances[KEY address addr] uint256 newValue (uint256 oldValue) STORAGE { + havoc sumOfBalances assuming sumOfBalances@new() == sumOfBalances@old() + newValue - oldValue; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: totalSupply is the sum of all balances │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant totalSupplyIsSumOfBalances() + totalSupply() == sumOfBalances() + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: balance of address(0) is 0 │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant zeroAddressNoBalance() + balanceOf(0) == 0 + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: only mint and burn can change total supply │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noChangeTotalSupply(env e) { + requireInvariant totalSupplyIsSumOfBalances(); + + method f; + calldataarg args; + + uint256 totalSupplyBefore = totalSupply(); + f(e, args); + uint256 totalSupplyAfter = totalSupply(); + + assert totalSupplyAfter > totalSupplyBefore => f.selector == mint(address,uint256).selector; + assert totalSupplyAfter < totalSupplyBefore => f.selector == burn(address,uint256).selector; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: only the token holder or an approved third party can reduce an account's balance │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyAuthorizedCanTransfer(env e) { + requireInvariant totalSupplyIsSumOfBalances(); + + method f; + calldataarg args; + address account; + + uint256 allowanceBefore = allowance(account, e.msg.sender); + uint256 balanceBefore = balanceOf(account); + f(e, args); + uint256 balanceAfter = balanceOf(account); + + assert ( + balanceAfter < balanceBefore + ) => ( + f.selector == burn(address,uint256).selector || + e.msg.sender == account || + balanceBefore - balanceAfter <= allowanceBefore + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: only the token holder (or a permit) can increase allowance. The spender can decrease it by using it │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyHolderOfSpenderCanChangeAllowance(env e) { + requireInvariant totalSupplyIsSumOfBalances(); + + method f; + calldataarg args; + address holder; + address spender; + + uint256 allowanceBefore = allowance(holder, spender); + f(e, args); + uint256 allowanceAfter = allowance(holder, spender); + + assert ( + allowanceAfter > allowanceBefore + ) => ( + (f.selector == approve(address,uint256).selector && e.msg.sender == holder) || + (f.selector == increaseAllowance(address,uint256).selector && e.msg.sender == holder) || + (f.selector == permit(address,address,uint256,uint256,uint8,bytes32,bytes32).selector) + ); + + assert ( + allowanceAfter < allowanceBefore + ) => ( + (f.selector == transferFrom(address,address,uint256).selector && e.msg.sender == spender) || + (f.selector == approve(address,uint256).selector && e.msg.sender == holder ) || + (f.selector == decreaseAllowance(address,uint256).selector && e.msg.sender == holder ) || + (f.selector == permit(address,address,uint256,uint256,uint8,bytes32,bytes32).selector) + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: mint behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule mint(env e) { + requireInvariant totalSupplyIsSumOfBalances(); + require nonpayable(e); + + address to; + address other; + uint256 amount; + + // cache state + uint256 toBalanceBefore = balanceOf(to); + uint256 otherBalanceBefore = balanceOf(other); + uint256 totalSupplyBefore = totalSupply(); + + // run transaction + mint@withrevert(e, to, amount); + + // check outcome + if (lastReverted) { + assert to == 0 || totalSupplyBefore + amount > max_uint256; + } else { + // updates balance and totalSupply + assert balanceOf(to) == toBalanceBefore + amount; + assert totalSupply() == totalSupplyBefore + amount; + + // no other balance is modified + assert balanceOf(other) != otherBalanceBefore => other == to; + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: burn behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule burn(env e) { + requireInvariant totalSupplyIsSumOfBalances(); + require nonpayable(e); + + address from; + address other; + uint256 amount; + + // cache state + uint256 fromBalanceBefore = balanceOf(from); + uint256 otherBalanceBefore = balanceOf(other); + uint256 totalSupplyBefore = totalSupply(); + + // run transaction + burn@withrevert(e, from, amount); + + // check outcome + if (lastReverted) { + assert from == 0 || fromBalanceBefore < amount; + } else { + // updates balance and totalSupply + assert balanceOf(from) == fromBalanceBefore - amount; + assert totalSupply() == totalSupplyBefore - amount; + + // no other balance is modified + assert balanceOf(other) != otherBalanceBefore => other == from; + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: transfer behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule transfer(env e) { + requireInvariant totalSupplyIsSumOfBalances(); + require nonpayable(e); + + address holder = e.msg.sender; + address recipient; + address other; + uint256 amount; + + // cache state + uint256 holderBalanceBefore = balanceOf(holder); + uint256 recipientBalanceBefore = balanceOf(recipient); + uint256 otherBalanceBefore = balanceOf(other); + + // run transaction + transfer@withrevert(e, recipient, amount); + + // check outcome + if (lastReverted) { + assert holder == 0 || recipient == 0 || amount > holderBalanceBefore; + } else { + // balances of holder and recipient are updated + assert balanceOf(holder) == holderBalanceBefore - (holder == recipient ? 0 : amount); + assert balanceOf(recipient) == recipientBalanceBefore + (holder == recipient ? 0 : amount); + + // no other balance is modified + assert balanceOf(other) != otherBalanceBefore => (other == holder || other == recipient); + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: transferFrom behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule transferFrom(env e) { + requireInvariant totalSupplyIsSumOfBalances(); + require nonpayable(e); + + address spender = e.msg.sender; + address holder; + address recipient; + address other; + uint256 amount; + + // cache state + uint256 allowanceBefore = allowance(holder, spender); + uint256 holderBalanceBefore = balanceOf(holder); + uint256 recipientBalanceBefore = balanceOf(recipient); + uint256 otherBalanceBefore = balanceOf(other); + + // run transaction + transferFrom@withrevert(e, holder, recipient, amount); + + // check outcome + if (lastReverted) { + assert holder == 0 || recipient == 0 || spender == 0 || amount > holderBalanceBefore || amount > allowanceBefore; + } else { + // allowance is valid & updated + assert allowanceBefore >= amount; + assert allowance(holder, spender) == (allowanceBefore == max_uint256 ? to_uint256(max_uint256) : allowanceBefore - amount); + + // balances of holder and recipient are updated + assert balanceOf(holder) == holderBalanceBefore - (holder == recipient ? 0 : amount); + assert balanceOf(recipient) == recipientBalanceBefore + (holder == recipient ? 0 : amount); + + // no other balance is modified + assert balanceOf(other) != otherBalanceBefore => (other == holder || other == recipient); + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: approve behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule approve(env e) { + require nonpayable(e); + + address holder = e.msg.sender; + address spender; + address otherHolder; + address otherSpender; + uint256 amount; + + // cache state + uint256 otherAllowanceBefore = allowance(otherHolder, otherSpender); + + // run transaction + approve@withrevert(e, spender, amount); + + // check outcome + if (lastReverted) { + assert holder == 0 || spender == 0; + } else { + // allowance is updated + assert allowance(holder, spender) == amount; + + // other allowances are untouched + assert allowance(otherHolder, otherSpender) != otherAllowanceBefore => (otherHolder == holder && otherSpender == spender); + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: increaseAllowance behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule increaseAllowance(env e) { + require nonpayable(e); + + address holder = e.msg.sender; + address spender; + address otherHolder; + address otherSpender; + uint256 amount; + + // cache state + uint256 allowanceBefore = allowance(holder, spender); + uint256 otherAllowanceBefore = allowance(otherHolder, otherSpender); + + // run transaction + increaseAllowance@withrevert(e, spender, amount); + + // check outcome + if (lastReverted) { + assert holder == 0 || spender == 0 || allowanceBefore + amount > max_uint256; + } else { + // allowance is updated + assert allowance(holder, spender) == allowanceBefore + amount; + + // other allowances are untouched + assert allowance(otherHolder, otherSpender) != otherAllowanceBefore => (otherHolder == holder && otherSpender == spender); + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: decreaseAllowance behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule decreaseAllowance(env e) { + require nonpayable(e); + + address holder = e.msg.sender; + address spender; + address otherHolder; + address otherSpender; + uint256 amount; + + // cache state + uint256 allowanceBefore = allowance(holder, spender); + uint256 otherAllowanceBefore = allowance(otherHolder, otherSpender); + + // run transaction + decreaseAllowance@withrevert(e, spender, amount); + + // check outcome + if (lastReverted) { + assert holder == 0 || spender == 0 || allowanceBefore < amount; + } else { + // allowance is updated + assert allowance(holder, spender) == allowanceBefore - amount; + + // other allowances are untouched + assert allowance(otherHolder, otherSpender) != otherAllowanceBefore => (otherHolder == holder && otherSpender == spender); + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: permit behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule permit(env e) { + require nonpayable(e); + + address holder; + address spender; + uint256 amount; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + + address account1; + address account2; + address account3; + + // cache state + uint256 nonceBefore = nonces(holder); + uint256 otherNonceBefore = nonces(account1); + uint256 otherAllowanceBefore = allowance(account2, account3); + + // sanity: nonce overflow, which possible in theory, is assumed to be impossible in practice + require nonceBefore < max_uint256; + require otherNonceBefore < max_uint256; + + // run transaction + permit@withrevert(e, holder, spender, amount, deadline, v, r, s); + + // check outcome + if (lastReverted) { + // Without formally checking the signature, we can't verify exactly the revert causes + assert true; + } else { + // allowance and nonce are updated + assert allowance(holder, spender) == amount; + assert nonces(holder) == nonceBefore + 1; + + // deadline was respected + assert deadline >= e.block.timestamp; + + // no other allowance or nonce is modified + assert nonces(account1) != otherNonceBefore => account1 == holder; + assert allowance(account2, account3) != otherAllowanceBefore => (account2 == holder && account3 == spender); + } +} diff --git a/certora/specs/ERC20FlashMint.spec b/certora/specs/ERC20FlashMint.spec new file mode 100644 index 000000000..64d97342a --- /dev/null +++ b/certora/specs/ERC20FlashMint.spec @@ -0,0 +1,48 @@ +import "helpers.spec" +import "methods/IERC20.spec" +import "methods/IERC3156.spec" + +methods { + // non standard ERC3156 functions + flashFeeReceiver() returns (address) envfree + + // function summaries below + _mint(address account, uint256 amount) => specMint(account, amount) + _burn(address account, uint256 amount) => specBurn(account, amount) + _transfer(address from, address to, uint256 amount) => specTransfer(from, to, amount) +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Ghost: track mint and burns in the CVL │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +ghost mapping(address => uint256) trackedMintAmount; +ghost mapping(address => uint256) trackedBurnAmount; +ghost mapping(address => mapping(address => uint256)) trackedTransferedAmount; + +function specMint(address account, uint256 amount) returns bool { trackedMintAmount[account] = amount; return true; } +function specBurn(address account, uint256 amount) returns bool { trackedBurnAmount[account] = amount; return true; } +function specTransfer(address from, address to, uint256 amount) returns bool { trackedTransferedAmount[from][to] = amount; return true; } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: When doing a flashLoan, "amount" is minted and burnt, additionally, the fee is either burnt │ +│ (if the fee recipient is 0) or transferred (if the fee recipient is not 0) │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule checkMintAndBurn(env e) { + address receiver; + address token; + uint256 amount; + bytes data; + + uint256 fees = flashFee(token, amount); + address recipient = flashFeeReceiver(); + + flashLoan(e, receiver, token, amount, data); + + assert trackedMintAmount[receiver] == amount; + assert trackedBurnAmount[receiver] == amount + (recipient == 0 ? fees : 0); + assert (fees > 0 && recipient != 0) => trackedTransferedAmount[receiver][recipient] == fees; +} diff --git a/certora/specs/GovernorBase.spec b/certora/specs/GovernorBase.spec deleted file mode 100644 index de728ddac..000000000 --- a/certora/specs/GovernorBase.spec +++ /dev/null @@ -1,333 +0,0 @@ -////////////////////////////////////////////////////////////////////////////// -///////////////////// Governor.sol base definitions ////////////////////////// -////////////////////////////////////////////////////////////////////////////// - -using ERC20VotesHarness as erc20votes - -methods { - proposalSnapshot(uint256) returns uint256 envfree // matches proposalVoteStart - proposalDeadline(uint256) returns uint256 envfree // matches proposalVoteEnd - hashProposal(address[],uint256[],bytes[],bytes32) returns uint256 envfree - isExecuted(uint256) returns bool envfree - isCanceled(uint256) returns bool envfree - execute(address[], uint256[], bytes[], bytes32) returns uint256 - hasVoted(uint256, address) returns bool - castVote(uint256, uint8) returns uint256 - updateQuorumNumerator(uint256) - queue(address[], uint256[], bytes[], bytes32) returns uint256 - - // internal functions made public in harness: - _quorumReached(uint256) returns bool - _voteSucceeded(uint256) returns bool envfree - - // function summarization - proposalThreshold() returns uint256 envfree - - getVotes(address, uint256) returns uint256 => DISPATCHER(true) - - getPastTotalSupply(uint256 t) returns uint256 => PER_CALLEE_CONSTANT - getPastVotes(address a, uint256 t) returns uint256 => PER_CALLEE_CONSTANT - - //scheduleBatch(address[],uint256[],bytes[],bytes32,bytes32,uint256) => DISPATCHER(true) - //executeBatch(address[], uint256[], bytes[], bytes32, bytes32) => DISPATCHER(true) -} - -////////////////////////////////////////////////////////////////////////////// -//////////////////////////////// Definitions ///////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - - -// proposal was created - relation proved in noStartBeforeCreation -definition proposalCreated(uint256 pId) returns bool = proposalSnapshot(pId) > 0; - - -////////////////////////////////////////////////////////////////////////////// -///////////////////////////// Helper Functions /////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - -function helperFunctionsWithRevert(uint256 proposalId, method f, env e) { - address[] targets; uint256[] values; bytes[] calldatas; string reason; bytes32 descriptionHash; - uint8 support; uint8 v; bytes32 r; bytes32 s; - if (f.selector == propose(address[], uint256[], bytes[], string).selector) { - uint256 result = propose@withrevert(e, targets, values, calldatas, reason); - require(result == proposalId); - } else if (f.selector == execute(address[], uint256[], bytes[], bytes32).selector) { - uint256 result = execute@withrevert(e, targets, values, calldatas, descriptionHash); - require(result == proposalId); - } else if (f.selector == castVote(uint256, uint8).selector) { - castVote@withrevert(e, proposalId, support); - } else if (f.selector == castVoteWithReason(uint256, uint8, string).selector) { - castVoteWithReason@withrevert(e, proposalId, support, reason); - } else if (f.selector == castVoteBySig(uint256, uint8,uint8, bytes32, bytes32).selector) { - castVoteBySig@withrevert(e, proposalId, support, v, r, s); - } else if (f.selector == queue(address[], uint256[], bytes[], bytes32).selector) { - queue@withrevert(e, targets, values, calldatas, descriptionHash); - } else { - calldataarg args; - f@withrevert(e, args); - } -} - -/* - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - ///////////////////////////////////////////////////// State Diagram ////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // // - // castVote(s)() // - // ------------- propose() ---------------------- time pass --------------- time passes ----------- // - // | No Proposal | --------> | Before Start (Delay) | --------> | Voting Period | ----------------------> | execute() | // - // ------------- ---------------------- --------------- -> Executed/Canceled ----------- // - // ------------------------------------------------------------|---------------|-------------------------|--------------> // - // t start end timelock // - // // - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -*/ - - -/////////////////////////////////////////////////////////////////////////////////////// -///////////////////////////////// Global Valid States ///////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////// - - -/* - * Start and end date are either initialized (non zero) or uninitialized (zero) simultaneously - * This invariant assumes that the block number cannot be 0 at any stage of the contract cycle - * This is very safe assumption as usually the 0 block is genesis block which is uploaded with data - * by the developers and will not be valid to raise proposals (at the current way that block chain is functioning) - */ - // To use env with general preserved block disable type checking [--disableLocalTypeChecking] -invariant startAndEndDatesNonZero(uint256 pId) - proposalSnapshot(pId) != 0 <=> proposalDeadline(pId) != 0 - { preserved with (env e){ - require e.block.number > 0; - }} - - -/* - * If a proposal is canceled it must have a start and an end date - */ - // To use env with general preserved block disable type checking [--disableLocalTypeChecking] -invariant canceledImplyStartAndEndDateNonZero(uint pId) - isCanceled(pId) => proposalSnapshot(pId) != 0 - {preserved with (env e){ - require e.block.number > 0; - }} - - -/* - * If a proposal is executed it must have a start and an end date - */ - // To use env with general preserved block disable type checking [--disableLocalTypeChecking] -invariant executedImplyStartAndEndDateNonZero(uint pId) - isExecuted(pId) => proposalSnapshot(pId) != 0 - { preserved with (env e){ - requireInvariant startAndEndDatesNonZero(pId); - require e.block.number > 0; - }} - - -/* - * A proposal starting block number must be less or equal than the proposal end date - */ -invariant voteStartBeforeVoteEnd(uint256 pId) - // from < to <= because snapshot and deadline can be the same block number if delays are set to 0 - // This is possible before the integration of GovernorSettings.sol to the system. - // After integration of GovernorSettings.sol the invariant expression should be changed from <= to < - (proposalSnapshot(pId) > 0 => proposalSnapshot(pId) <= proposalDeadline(pId)) - // (proposalSnapshot(pId) > 0 => proposalSnapshot(pId) <= proposalDeadline(pId)) - { preserved { - requireInvariant startAndEndDatesNonZero(pId); - }} - - -/* - * A proposal cannot be both executed and canceled simultaneously. - */ -invariant noBothExecutedAndCanceled(uint256 pId) - !isExecuted(pId) || !isCanceled(pId) - - -/* - * A proposal could be executed only if quorum was reached and vote succeeded - */ -rule executionOnlyIfQuoromReachedAndVoteSucceeded(uint256 pId, env e, method f){ - bool isExecutedBefore = isExecuted(pId); - bool quorumReachedBefore = _quorumReached(e, pId); - bool voteSucceededBefore = _voteSucceeded(pId); - - calldataarg args; - f(e, args); - - bool isExecutedAfter = isExecuted(pId); - assert (!isExecutedBefore && isExecutedAfter) => (quorumReachedBefore && voteSucceededBefore), "quorum was changed"; -} - -/////////////////////////////////////////////////////////////////////////////////////// -////////////////////////////////// In-State Rules ///////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////// - -//========================================== -//------------- Voting Period -------------- -//========================================== - -/* - * A user cannot vote twice - */ - // Checked for castVote only. all 3 castVote functions call _castVote, so the completeness of the verification is counted on - // the fact that the 3 functions themselves makes no changes, but rather call an internal function to execute. - // That means that we do not check those 3 functions directly, however for castVote & castVoteWithReason it is quite trivial - // to understand why this is ok. For castVoteBySig we basically assume that the signature referendum is correct without checking it. - // We could check each function separately and pass the rule, but that would have uglyfied the code with no concrete - // benefit, as it is evident that nothing is happening in the first 2 functions (calling a view function), and we do not desire to check the signature verification. -rule doubleVoting(uint256 pId, uint8 sup, method f) { - env e; - address user = e.msg.sender; - bool votedCheck = hasVoted(e, pId, user); - - castVote@withrevert(e, pId, sup); - - assert votedCheck => lastReverted, "double voting occurred"; -} - - -/////////////////////////////////////////////////////////////////////////////////////// -//////////////////////////// State Transitions Rules ////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////// - -//=========================================== -//-------- Propose() --> End of Time -------- -//=========================================== - - -/* - * Once a proposal is created, voteStart and voteEnd are immutable - */ -rule immutableFieldsAfterProposalCreation(uint256 pId, method f) { - uint256 _voteStart = proposalSnapshot(pId); - uint256 _voteEnd = proposalDeadline(pId); - - require proposalCreated(pId); // startDate > 0 - - env e; calldataarg arg; - f(e, arg); - - uint256 voteStart_ = proposalSnapshot(pId); - uint256 voteEnd_ = proposalDeadline(pId); - assert _voteStart == voteStart_, "Start date was changed"; - assert _voteEnd == voteEnd_, "End date was changed"; -} - - -/* - * Voting cannot start at a block number prior to proposal’s creation block number - */ -rule noStartBeforeCreation(uint256 pId) { - uint256 previousStart = proposalSnapshot(pId); - // This line makes sure that we see only cases where start date is changed from 0, i.e. creation of proposal - // We proved in immutableFieldsAfterProposalCreation that once dates set for proposal, it cannot be changed - require !proposalCreated(pId); // previousStart == 0; - - env e; calldataarg args; - propose(e, args); - - uint256 newStart = proposalSnapshot(pId); - // if created, start is after current block number (creation block) - assert(newStart != previousStart => newStart >= e.block.number); -} - - -//============================================ -//--- End of Voting Period --> End of Time --- -//============================================ - - -/* - * A proposal can neither be executed nor canceled before it ends - */ - // By induction it cannot be executed nor canceled before it starts, due to voteStartBeforeVoteEnd -rule noExecuteOrCancelBeforeDeadline(uint256 pId, method f){ - require !isExecuted(pId) && !isCanceled(pId); - - env e; calldataarg args; - f(e, args); - - assert e.block.number < proposalDeadline(pId) => (!isExecuted(pId) && !isCanceled(pId)), "executed/cancelled before deadline"; -} - -//////////////////////////////////////////////////////////////////////////////// -////////////////////// Integrity Of Functions (Unit Tests) ///////////////////// -//////////////////////////////////////////////////////////////////////////////// - - -//////////////////////////////////////////////////////////////////////////////// -////////////////////////////// High Level Rules //////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - - -//////////////////////////////////////////////////////////////////////////////// -///////////////////////////// Not Categorized Yet ////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - - -/* - * All proposal specific (non-view) functions should revert if proposal is executed - */ - // In this rule we show that if a function is executed, i.e. execute() was called on the proposal ID, - // non of the proposal specific functions can make changes again. In executedOnlyAfterExecuteFunc - // we connected the executed attribute to the execute() function, showing that only execute() can - // change it, and that it will always change it. -rule allFunctionsRevertIfExecuted(method f) filtered { f -> - !f.isView && !f.isFallback - && f.selector != updateTimelock(address).selector - && f.selector != updateQuorumNumerator(uint256).selector - && f.selector != queue(address[],uint256[],bytes[],bytes32).selector - && f.selector != relay(address,uint256,bytes).selector - && f.selector != 0xb9a61961 // __acceptAdmin() -} { - env e; calldataarg args; - uint256 pId; - require(isExecuted(pId)); - requireInvariant noBothExecutedAndCanceled(pId); - requireInvariant executedImplyStartAndEndDateNonZero(pId); - - helperFunctionsWithRevert(pId, f, e); - - assert(lastReverted, "Function was not reverted"); -} - -/* - * All proposal specific (non-view) functions should revert if proposal is canceled - */ -rule allFunctionsRevertIfCanceled(method f) filtered { - f -> !f.isView && !f.isFallback - && f.selector != updateTimelock(address).selector - && f.selector != updateQuorumNumerator(uint256).selector - && f.selector != queue(address[],uint256[],bytes[],bytes32).selector - && f.selector != relay(address,uint256,bytes).selector - && f.selector != 0xb9a61961 // __acceptAdmin() -} { - env e; calldataarg args; - uint256 pId; - require(isCanceled(pId)); - requireInvariant noBothExecutedAndCanceled(pId); - requireInvariant canceledImplyStartAndEndDateNonZero(pId); - - helperFunctionsWithRevert(pId, f, e); - - assert(lastReverted, "Function was not reverted"); -} - -/* - * Proposal can be switched to executed only via execute() function - */ -rule executedOnlyAfterExecuteFunc(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash, method f) { - env e; calldataarg args; - uint256 pId; - bool executedBefore = isExecuted(pId); - require(!executedBefore); - - helperFunctionsWithRevert(pId, f, e); - - bool executedAfter = isExecuted(pId); - assert(executedAfter != executedBefore => f.selector == execute(address[], uint256[], bytes[], bytes32).selector, "isExecuted only changes in the execute method"); -} diff --git a/certora/specs/GovernorCountingSimple.spec b/certora/specs/GovernorCountingSimple.spec deleted file mode 100644 index 7af73beb6..000000000 --- a/certora/specs/GovernorCountingSimple.spec +++ /dev/null @@ -1,221 +0,0 @@ -import "GovernorBase.spec" - -using ERC20VotesHarness as erc20votes - -methods { - ghost_sum_vote_power_by_id(uint256) returns uint256 envfree - - quorum(uint256) returns uint256 - proposalVotes(uint256) returns (uint256, uint256, uint256) envfree - - quorumNumerator() returns uint256 - _executor() returns address - - erc20votes._getPastVotes(address, uint256) returns uint256 - - getExecutor() returns address - - timelock() returns address -} - - -////////////////////////////////////////////////////////////////////////////// -///////////////////////////////// GHOSTS ///////////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - - -//////////// ghosts to keep track of votes counting //////////// - -/* - * the sum of voting power of those who voted - */ -ghost sum_all_votes_power() returns uint256 { - init_state axiom sum_all_votes_power() == 0; -} - -hook Sstore ghost_sum_vote_power_by_id [KEY uint256 pId] uint256 current_power(uint256 old_power) STORAGE { - havoc sum_all_votes_power assuming sum_all_votes_power@new() == sum_all_votes_power@old() - old_power + current_power; -} - -/* - * sum of all votes casted per proposal - */ -ghost tracked_weight(uint256) returns uint256 { - init_state axiom forall uint256 p. tracked_weight(p) == 0; -} - -/* - * sum of all votes casted - */ -ghost sum_tracked_weight() returns uint256 { - init_state axiom sum_tracked_weight() == 0; -} - -/* - * getter for _proposalVotes.againstVotes - */ -ghost votesAgainst() returns uint256 { - init_state axiom votesAgainst() == 0; -} - -/* - * getter for _proposalVotes.forVotes - */ -ghost votesFor() returns uint256 { - init_state axiom votesFor() == 0; -} - -/* - * getter for _proposalVotes.abstainVotes - */ -ghost votesAbstain() returns uint256 { - init_state axiom votesAbstain() == 0; -} - -hook Sstore _proposalVotes [KEY uint256 pId].againstVotes uint256 votes(uint256 old_votes) STORAGE { - havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) && - (p != pId => tracked_weight@new(p) == tracked_weight@old(p)); - havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes; - havoc votesAgainst assuming votesAgainst@new() == votesAgainst@old() - old_votes + votes; -} - -hook Sstore _proposalVotes [KEY uint256 pId].forVotes uint256 votes(uint256 old_votes) STORAGE { - havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) && - (p != pId => tracked_weight@new(p) == tracked_weight@old(p)); - havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes; - havoc votesFor assuming votesFor@new() == votesFor@old() - old_votes + votes; -} - -hook Sstore _proposalVotes [KEY uint256 pId].abstainVotes uint256 votes(uint256 old_votes) STORAGE { - havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) && - (p != pId => tracked_weight@new(p) == tracked_weight@old(p)); - havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes; - havoc votesAbstain assuming votesAbstain@new() == votesAbstain@old() - old_votes + votes; -} - - -////////////////////////////////////////////////////////////////////////////// -////////////////////////////// INVARIANTS //////////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - - -/* - * sum of all votes casted is equal to the sum of voting power of those who voted, per each proposal - */ -invariant SumOfVotesCastEqualSumOfPowerOfVotedPerProposal(uint256 pId) - tracked_weight(pId) == ghost_sum_vote_power_by_id(pId) - - -/* - * sum of all votes casted is equal to the sum of voting power of those who voted - */ -invariant SumOfVotesCastEqualSumOfPowerOfVoted() - sum_tracked_weight() == sum_all_votes_power() - - -/* -* sum of all votes casted is greater or equal to the sum of voting power of those who voted at a specific proposal -*/ -invariant OneIsNotMoreThanAll(uint256 pId) - sum_all_votes_power() >= tracked_weight(pId) - - -////////////////////////////////////////////////////////////////////////////// -///////////////////////////////// RULES ////////////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - - -/* - * Only sender's voting status can be changed by execution of any cast vote function - */ -// Checked for castVote only. all 3 castVote functions call _castVote, so the completeness of the verification is counted on - // the fact that the 3 functions themselves makes no changes, but rather call an internal function to execute. - // That means that we do not check those 3 functions directly, however for castVote & castVoteWithReason it is quite trivial - // to understand why this is ok. For castVoteBySig we basically assume that the signature referendum is correct without checking it. - // We could check each function separately and pass the rule, but that would have uglyfied the code with no concrete - // benefit, as it is evident that nothing is happening in the first 2 functions (calling a view function), and we do not desire to check the signature verification. -rule noVoteForSomeoneElse(uint256 pId, uint8 sup, method f) { - env e; calldataarg args; - - address voter = e.msg.sender; - address user; - - bool hasVotedBefore_User = hasVoted(e, pId, user); - - castVote@withrevert(e, pId, sup); - require(!lastReverted); - - bool hasVotedAfter_User = hasVoted(e, pId, user); - - assert user != voter => hasVotedBefore_User == hasVotedAfter_User; -} - - -/* -* Total voting tally is monotonically non-decreasing in every operation -*/ -rule votingWeightMonotonicity(method f){ - uint256 votingWeightBefore = sum_tracked_weight(); - - env e; - calldataarg args; - f(e, args); - - uint256 votingWeightAfter = sum_tracked_weight(); - - assert votingWeightBefore <= votingWeightAfter, "Voting weight was decreased somehow"; -} - - -/* -* A change in hasVoted must be correlated with an non-decreasing of the vote supports (nondecrease because user can vote with weight 0) -*/ -rule hasVotedCorrelation(uint256 pId, method f, env e, uint256 bn) { - address acc = e.msg.sender; - - uint256 againstBefore = votesAgainst(); - uint256 forBefore = votesFor(); - uint256 abstainBefore = votesAbstain(); - - bool hasVotedBefore = hasVoted(e, pId, acc); - - helperFunctionsWithRevert(pId, f, e); - require(!lastReverted); - - uint256 againstAfter = votesAgainst(); - uint256 forAfter = votesFor(); - uint256 abstainAfter = votesAbstain(); - - bool hasVotedAfter = hasVoted(e, pId, acc); - - assert (!hasVotedBefore && hasVotedAfter) => againstBefore <= againstAfter || forBefore <= forAfter || abstainBefore <= abstainAfter, "no correlation"; -} - - -/* -* Only privileged users can execute privileged operations, e.g. change _quorumNumerator or _timelock -*/ -rule privilegedOnlyNumerator(method f, uint256 newQuorumNumerator){ - env e; - calldataarg arg; - uint256 quorumNumBefore = quorumNumerator(e); - - f(e, arg); - - uint256 quorumNumAfter = quorumNumerator(e); - address executorCheck = getExecutor(e); - - assert quorumNumBefore != quorumNumAfter => e.msg.sender == executorCheck, "non privileged user changed quorum numerator"; -} - -rule privilegedOnlyTimelock(method f, uint256 newQuorumNumerator){ - env e; - calldataarg arg; - uint256 timelockBefore = timelock(e); - - f(e, arg); - - uint256 timelockAfter = timelock(e); - - assert timelockBefore != timelockAfter => e.msg.sender == timelockBefore, "non privileged user changed timelock"; -} diff --git a/certora/specs/RulesInProgress.spec b/certora/specs/RulesInProgress.spec deleted file mode 100644 index cbad3336e..000000000 --- a/certora/specs/RulesInProgress.spec +++ /dev/null @@ -1,139 +0,0 @@ -////////////////////////////////////////////////////////////////////////////// -////////////// THIS SPEC IS A RESERVE FOR NOT IN PROGRESS ////////////// -////////////////////////////////////////////////////////////////////////////// - -import "GovernorBase.spec" - -using ERC20VotesHarness as erc20votes - -methods { - ghost_sum_vote_power_by_id(uint256) returns uint256 envfree - - quorum(uint256) returns uint256 - proposalVotes(uint256) returns (uint256, uint256, uint256) envfree - - quorumNumerator() returns uint256 - _executor() returns address - - erc20votes._getPastVotes(address, uint256) returns uint256 - - getExecutor() returns address - - timelock() returns address -} - - -////////////////////////////////////////////////////////////////////////////// -///////////////////////////////// GHOSTS ///////////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - - -//////////// ghosts to keep track of votes counting //////////// - -/* - * the sum of voting power of those who voted - */ -ghost sum_all_votes_power() returns uint256 { - init_state axiom sum_all_votes_power() == 0; -} - -hook Sstore ghost_sum_vote_power_by_id [KEY uint256 pId] uint256 current_power(uint256 old_power) STORAGE { - havoc sum_all_votes_power assuming sum_all_votes_power@new() == sum_all_votes_power@old() - old_power + current_power; -} - -/* - * sum of all votes casted per proposal - */ -ghost tracked_weight(uint256) returns uint256 { - init_state axiom forall uint256 p. tracked_weight(p) == 0; -} - -/* - * sum of all votes casted - */ -ghost sum_tracked_weight() returns uint256 { - init_state axiom sum_tracked_weight() == 0; -} - -/* - * getter for _proposalVotes.againstVotes - */ -ghost votesAgainst() returns uint256 { - init_state axiom votesAgainst() == 0; -} - -/* - * getter for _proposalVotes.forVotes - */ -ghost votesFor() returns uint256 { - init_state axiom votesFor() == 0; -} - -/* - * getter for _proposalVotes.abstainVotes - */ -ghost votesAbstain() returns uint256 { - init_state axiom votesAbstain() == 0; -} - -hook Sstore _proposalVotes [KEY uint256 pId].againstVotes uint256 votes(uint256 old_votes) STORAGE { - havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) && - (p != pId => tracked_weight@new(p) == tracked_weight@old(p)); - havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes; - havoc votesAgainst assuming votesAgainst@new() == votesAgainst@old() - old_votes + votes; -} - -hook Sstore _proposalVotes [KEY uint256 pId].forVotes uint256 votes(uint256 old_votes) STORAGE { - havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) && - (p != pId => tracked_weight@new(p) == tracked_weight@old(p)); - havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes; - havoc votesFor assuming votesFor@new() == votesFor@old() - old_votes + votes; -} - -hook Sstore _proposalVotes [KEY uint256 pId].abstainVotes uint256 votes(uint256 old_votes) STORAGE { - havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) && - (p != pId => tracked_weight@new(p) == tracked_weight@old(p)); - havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes; - havoc votesAbstain assuming votesAbstain@new() == votesAbstain@old() - old_votes + votes; -} - - -////////////////////////////////////////////////////////////////////////////// -////////////////////////////// INVARIANTS //////////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - - - -////////////////////////////////////////////////////////////////////////////// -///////////////////////////////// RULES ////////////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - - -//NOT FINISHED -/* -* the sum of voting power of those who voted is less or equal to the maximum possible votes, per each proposal -*/ -rule possibleTotalVotes(uint256 pId, uint8 sup, env e, method f) { - - // add requireinvariant for all i, j. i = i - 1 && i < j => checkpointlookup[i] < checkpointlookup[j]; - require tracked_weight(pId) <= erc20votes.getPastTotalSupply(e, proposalSnapshot(pId)); - - uint256 againstB; - uint256 forB; - uint256 absatinB; - againstB, forB, absatinB = proposalVotes(pId); - - calldataarg args; - //f(e, args); - - castVote(e, pId, sup); - - uint256 against; - uint256 for; - uint256 absatin; - against, for, absatin = proposalVotes(pId); - - uint256 ps = proposalSnapshot(pId); - - assert tracked_weight(pId) <= erc20votes.getPastTotalSupply(e, proposalSnapshot(pId)), "bla bla bla"; -} \ No newline at end of file diff --git a/certora/specs/helpers.spec b/certora/specs/helpers.spec new file mode 100644 index 000000000..24842d62f --- /dev/null +++ b/certora/specs/helpers.spec @@ -0,0 +1 @@ +definition nonpayable(env e) returns bool = e.msg.value == 0; diff --git a/certora/specs/methods/IERC20.spec b/certora/specs/methods/IERC20.spec new file mode 100644 index 000000000..cfa454e13 --- /dev/null +++ b/certora/specs/methods/IERC20.spec @@ -0,0 +1,11 @@ +methods { + name() returns (string) envfree => DISPATCHER(true) + symbol() returns (string) envfree => DISPATCHER(true) + decimals() returns (uint8) envfree => DISPATCHER(true) + totalSupply() returns (uint256) envfree => DISPATCHER(true) + balanceOf(address) returns (uint256) envfree => DISPATCHER(true) + allowance(address,address) returns (uint256) envfree => DISPATCHER(true) + approve(address,uint256) returns (bool) => DISPATCHER(true) + transfer(address,uint256) returns (bool) => DISPATCHER(true) + transferFrom(address,address,uint256) returns (bool) => DISPATCHER(true) +} diff --git a/certora/specs/methods/IERC2612.spec b/certora/specs/methods/IERC2612.spec new file mode 100644 index 000000000..0c1689da4 --- /dev/null +++ b/certora/specs/methods/IERC2612.spec @@ -0,0 +1,5 @@ +methods { + permit(address,address,uint256,uint256,uint8,bytes32,bytes32) => DISPATCHER(true) + nonces(address) returns (uint256) envfree => DISPATCHER(true) + DOMAIN_SEPARATOR() returns (bytes32) envfree => DISPATCHER(true) +} diff --git a/certora/specs/methods/IERC3156.spec b/certora/specs/methods/IERC3156.spec new file mode 100644 index 000000000..18c10c515 --- /dev/null +++ b/certora/specs/methods/IERC3156.spec @@ -0,0 +1,5 @@ +methods { + maxFlashLoan(address) returns (uint256) envfree => DISPATCHER(true) + flashFee(address,uint256) returns (uint256) envfree => DISPATCHER(true) + flashLoan(address,address,uint256,bytes) returns (bool) => DISPATCHER(true) +} diff --git a/certora/specs/sanity.spec b/certora/specs/sanity.spec deleted file mode 100644 index e08f68845..000000000 --- a/certora/specs/sanity.spec +++ /dev/null @@ -1,14 +0,0 @@ -/* -This rule looks for a non-reverting execution path to each method, including those overridden in the harness. -A method has such an execution path if it violates this rule. -How it works: - - If there is a non-reverting execution path, we reach the false assertion, and the sanity fails. - - If all execution paths are reverting, we never call the assertion, and the method will pass this rule vacuously. -*/ - -rule sanity(method f) { - env e; - calldataarg arg; - f(e, arg); - assert false; -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f3081ffa9..b9b491e05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "lodash.zip": "^4.2.0", "merkletreejs": "^0.2.13", "micromatch": "^4.0.2", + "p-limit": "^3.1.0", "prettier": "^2.8.1", "prettier-plugin-solidity": "^1.1.0", "rimraf": "^3.0.2", @@ -215,6 +216,21 @@ "changeset": "bin.js" } }, + "node_modules/@changesets/cli/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@changesets/cli/node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -5375,21 +5391,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -5835,6 +5836,21 @@ "node": ">= 0.4" } }, + "node_modules/eth-gas-reporter/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eth-gas-reporter/node_modules/p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -9516,21 +9532,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/mocha/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mocha/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -10108,15 +10109,15 @@ } }, "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "dependencies": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -10134,6 +10135,21 @@ "node": ">=8" } }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -10456,21 +10472,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/preferred-pm/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/preferred-pm/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -12942,6 +12943,21 @@ "node": ">= 0.4" } }, + "node_modules/solidity-coverage/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/solidity-coverage/node_modules/p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -15347,6 +15363,15 @@ "tty-table": "^4.1.5" }, "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -19264,15 +19289,6 @@ "p-locate": "^5.0.0" } }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, "p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -19650,6 +19666,15 @@ "object-keys": "^1.0.11" } }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, "p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -22564,15 +22589,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, "p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -23028,12 +23044,12 @@ } }, "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "requires": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" } }, "p-locate": { @@ -23043,6 +23059,17 @@ "dev": true, "requires": { "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } } }, "p-map": { @@ -23286,15 +23313,6 @@ "p-locate": "^5.0.0" } }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, "p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -25146,6 +25164,15 @@ "object-keys": "^1.0.11" } }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, "p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", diff --git a/package.json b/package.json index 282547535..56798f788 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "lodash.zip": "^4.2.0", "merkletreejs": "^0.2.13", "micromatch": "^4.0.2", + "p-limit": "^3.1.0", "prettier": "^2.8.1", "prettier-plugin-solidity": "^1.1.0", "rimraf": "^3.0.2", diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..797b3598d --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +certora-cli==3.6.3 From aaad1f4a4ffdba7da789d93a28030e5c411f0181 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 8 Mar 2023 19:30:07 +0100 Subject: [PATCH 072/197] Add FV specs for Ownable and Ownable2Steps (#4094) Co-authored-by: Santiago Palladino Co-authored-by: Francisco --- certora/harnesses/Ownable2StepHarness.sol | 9 ++ certora/harnesses/OwnableHarness.sol | 9 ++ certora/run.js | 4 +- certora/specs.json | 10 ++ certora/specs/Ownable.spec | 78 ++++++++++++++++ certora/specs/Ownable2Step.spec | 108 ++++++++++++++++++++++ certora/specs/methods/IOwnable.spec | 5 + certora/specs/methods/IOwnable2Step.spec | 7 ++ 8 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 certora/harnesses/Ownable2StepHarness.sol create mode 100644 certora/harnesses/OwnableHarness.sol create mode 100644 certora/specs/Ownable.spec create mode 100644 certora/specs/Ownable2Step.spec create mode 100644 certora/specs/methods/IOwnable.spec create mode 100644 certora/specs/methods/IOwnable2Step.spec diff --git a/certora/harnesses/Ownable2StepHarness.sol b/certora/harnesses/Ownable2StepHarness.sol new file mode 100644 index 000000000..4d30e5041 --- /dev/null +++ b/certora/harnesses/Ownable2StepHarness.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../patched/access/Ownable2Step.sol"; + +contract Ownable2StepHarness is Ownable2Step { + function restricted() external onlyOwner {} +} diff --git a/certora/harnesses/OwnableHarness.sol b/certora/harnesses/OwnableHarness.sol new file mode 100644 index 000000000..93cbb4770 --- /dev/null +++ b/certora/harnesses/OwnableHarness.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../patched/access/Ownable.sol"; + +contract OwnableHarness is Ownable { + function restricted() external onlyOwner {} +} diff --git a/certora/run.js b/certora/run.js index 1da238d4f..161465258 100644 --- a/certora/run.js +++ b/certora/run.js @@ -64,7 +64,7 @@ async function runCertora(spec, contract, files, options = []) { stream.end(); // write results in markdown format - writeEntry(spec, contract, code || signal, (await output).match(/https:\S*/)[0]); + writeEntry(spec, contract, code || signal, (await output).match(/https:\S*/)?.[0]); // write all details console.error(`+ certoraRun ${args.join(' ')}\n` + (await output)); @@ -103,7 +103,7 @@ function writeEntry(spec, contract, success, url) { contract, success ? ':x:' : ':heavy_check_mark:', `[link](${url})`, - `[link](${url.replace('/jobStatus/', '/output/')})`, + `[link](${url?.replace('/jobStatus/', '/output/')})`, ), ); } diff --git a/certora/specs.json b/certora/specs.json index 4d3e1fb90..c22b1ebba 100644 --- a/certora/specs.json +++ b/certora/specs.json @@ -4,6 +4,16 @@ "contract": "AccessControlHarness", "files": ["certora/harnesses/AccessControlHarness.sol"] }, + { + "spec": "Ownable", + "contract": "OwnableHarness", + "files": ["certora/harnesses/OwnableHarness.sol"] + }, + { + "spec": "Ownable2Step", + "contract": "Ownable2StepHarness", + "files": ["certora/harnesses/Ownable2StepHarness.sol"] + }, { "spec": "ERC20", "contract": "ERC20PermitHarness", diff --git a/certora/specs/Ownable.spec b/certora/specs/Ownable.spec new file mode 100644 index 000000000..48bd84d13 --- /dev/null +++ b/certora/specs/Ownable.spec @@ -0,0 +1,78 @@ +import "helpers.spec" +import "methods/IOwnable.spec" + +methods { + restricted() +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: transferOwnership changes ownership │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule transferOwnership(env e) { + require nonpayable(e); + + address newOwner; + address current = owner(); + + transferOwnership@withrevert(e, newOwner); + bool success = !lastReverted; + + assert success <=> (e.msg.sender == current && newOwner != 0), "unauthorized caller or invalid arg"; + assert success => owner() == newOwner, "current owner changed"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: renounceOwnership removes the owner │ + +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule renounceOwnership(env e) { + require nonpayable(e); + + address current = owner(); + + renounceOwnership@withrevert(e); + bool success = !lastReverted; + + assert success <=> e.msg.sender == current, "unauthorized caller"; + assert success => owner() == 0, "owner not cleared"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Access control: only current owner can call restricted functions │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyCurrentOwnerCanCallOnlyOwner(env e) { + require nonpayable(e); + + address current = owner(); + + calldataarg args; + restricted@withrevert(e, args); + + assert !lastReverted <=> e.msg.sender == current, "access control failed"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: ownership can only change in specific ways │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyOwnerOrPendingOwnerCanChangeOwnership(env e, method f) { + address oldCurrent = owner(); + + calldataarg args; + f(e, args); + + address newCurrent = owner(); + + // If owner changes, must be either transferOwnership or renounceOwnership + assert oldCurrent != newCurrent => ( + (e.msg.sender == oldCurrent && newCurrent != 0 && f.selector == transferOwnership(address).selector) || + (e.msg.sender == oldCurrent && newCurrent == 0 && f.selector == renounceOwnership().selector) + ); +} diff --git a/certora/specs/Ownable2Step.spec b/certora/specs/Ownable2Step.spec new file mode 100644 index 000000000..70c520a03 --- /dev/null +++ b/certora/specs/Ownable2Step.spec @@ -0,0 +1,108 @@ +import "helpers.spec" +import "methods/IOwnable2Step.spec" + +methods { + restricted() +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: transferOwnership sets the pending owner │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule transferOwnership(env e) { + require nonpayable(e); + + address newOwner; + address current = owner(); + + transferOwnership@withrevert(e, newOwner); + bool success = !lastReverted; + + assert success <=> e.msg.sender == current, "unauthorized caller"; + assert success => pendingOwner() == newOwner, "pending owner not set"; + assert success => owner() == current, "current owner changed"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: renounceOwnership removes the owner and the pendingOwner │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule renounceOwnership(env e) { + require nonpayable(e); + + address current = owner(); + + renounceOwnership@withrevert(e); + bool success = !lastReverted; + + assert success <=> e.msg.sender == current, "unauthorized caller"; + assert success => pendingOwner() == 0, "pending owner not cleared"; + assert success => owner() == 0, "owner not cleared"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: acceptOwnership changes owner and reset pending owner │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule acceptOwnership(env e) { + + require nonpayable(e); + + address current = owner(); + address pending = pendingOwner(); + + acceptOwnership@withrevert(e); + bool success = !lastReverted; + + assert success <=> e.msg.sender == pending, "unauthorized caller"; + assert success => pendingOwner() == 0, "pending owner not cleared"; + assert success => owner() == pending, "owner not transferred"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Access control: only current owner can call restricted functions │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyCurrentOwnerCanCallOnlyOwner(env e) { + require nonpayable(e); + + address current = owner(); + + calldataarg args; + restricted@withrevert(e, args); + + assert !lastReverted <=> e.msg.sender == current, "access control failed"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: ownership and pending ownership can only change in specific ways │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule ownerOrPendingOwnerChange(env e, method f) { + address oldCurrent = owner(); + address oldPending = pendingOwner(); + + calldataarg args; + f(e, args); + + address newCurrent = owner(); + address newPending = pendingOwner(); + + // If owner changes, must be either acceptOwnership or renounceOwnership + assert oldCurrent != newCurrent => ( + (e.msg.sender == oldPending && newCurrent == oldPending && newPending == 0 && f.selector == acceptOwnership().selector) || + (e.msg.sender == oldCurrent && newCurrent == 0 && newPending == 0 && f.selector == renounceOwnership().selector) + ); + + // If pending changes, must be either acceptance or reset + assert oldPending != newPending => ( + (e.msg.sender == oldCurrent && newCurrent == oldCurrent && f.selector == transferOwnership(address).selector) || + (e.msg.sender == oldPending && newCurrent == oldPending && newPending == 0 && f.selector == acceptOwnership().selector) || + (e.msg.sender == oldCurrent && newCurrent == 0 && newPending == 0 && f.selector == renounceOwnership().selector) + ); +} diff --git a/certora/specs/methods/IOwnable.spec b/certora/specs/methods/IOwnable.spec new file mode 100644 index 000000000..cfa15f95f --- /dev/null +++ b/certora/specs/methods/IOwnable.spec @@ -0,0 +1,5 @@ +methods { + owner() returns (address) envfree + transferOwnership(address) + renounceOwnership() +} diff --git a/certora/specs/methods/IOwnable2Step.spec b/certora/specs/methods/IOwnable2Step.spec new file mode 100644 index 000000000..c8e671d27 --- /dev/null +++ b/certora/specs/methods/IOwnable2Step.spec @@ -0,0 +1,7 @@ +methods { + owner() returns (address) envfree + pendingOwner() returns (address) envfree + transferOwnership(address) + acceptOwnership() + renounceOwnership() +} From a55013e742d0673394192e3df36c58778f8d8a59 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 8 Mar 2023 19:31:46 +0100 Subject: [PATCH 073/197] Add effect checks on the accesscontrol specs (#4099) --- certora/specs/AccessControl.spec | 85 +++++++++++++++++++------------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/certora/specs/AccessControl.spec b/certora/specs/AccessControl.spec index 170d56fd4..8e8f6ac9d 100644 --- a/certora/specs/AccessControl.spec +++ b/certora/specs/AccessControl.spec @@ -44,22 +44,27 @@ rule onlyGrantCanGrant(env e, bytes32 role, address account) { rule grantRoleEffect(env e) { require nonpayable(e); - bytes32 role1; bytes32 role2; - address account1; address account2; + bytes32 role; + bytes32 otherRole; + address account; + address otherAccount; - bool isCallerAdmin = hasRole(getRoleAdmin(role2), e.msg.sender); - bool hasRoleBefore = hasRole(role1, account1); + bool isCallerAdmin = hasRole(getRoleAdmin(role), e.msg.sender); + bool hasOtherRoleBefore = hasRole(otherRole, otherAccount); - grantRole@withrevert(e, role2, account2); - assert !lastReverted <=> isCallerAdmin; + grantRole@withrevert(e, role, account); + bool success = !lastReverted; - bool hasRoleAfter = hasRole(role1, account1); + bool hasOtherRoleAfter = hasRole(otherRole, otherAccount); - assert ( - hasRoleBefore != hasRoleAfter - ) => ( - hasRoleAfter == true && role1 == role2 && account1 == account2 - ); + // liveness + assert success <=> isCallerAdmin; + + // effect + assert success => hasRole(role, account); + + // no side effect + assert hasOtherRoleBefore != hasOtherRoleAfter => (role == otherRole && account == otherAccount); } /* @@ -70,22 +75,27 @@ rule grantRoleEffect(env e) { rule revokeRoleEffect(env e) { require nonpayable(e); - bytes32 role1; bytes32 role2; - address account1; address account2; + bytes32 role; + bytes32 otherRole; + address account; + address otherAccount; - bool isCallerAdmin = hasRole(getRoleAdmin(role2), e.msg.sender); - bool hasRoleBefore = hasRole(role1, account1); + bool isCallerAdmin = hasRole(getRoleAdmin(role), e.msg.sender); + bool hasOtherRoleBefore = hasRole(otherRole, otherAccount); - revokeRole@withrevert(e, role2, account2); - assert !lastReverted <=> isCallerAdmin; + revokeRole@withrevert(e, role, account); + bool success = !lastReverted; - bool hasRoleAfter = hasRole(role1, account1); + bool hasOtherRoleAfter = hasRole(otherRole, otherAccount); - assert ( - hasRoleBefore != hasRoleAfter - ) => ( - hasRoleAfter == false && role1 == role2 && account1 == account2 - ); + // liveness + assert success <=> isCallerAdmin; + + // effect + assert success => !hasRole(role, account); + + // no side effect + assert hasOtherRoleBefore != hasOtherRoleAfter => (role == otherRole && account == otherAccount); } /* @@ -96,19 +106,24 @@ rule revokeRoleEffect(env e) { rule renounceRoleEffect(env e) { require nonpayable(e); - bytes32 role1; bytes32 role2; - address account1; address account2; + bytes32 role; + bytes32 otherRole; + address account; + address otherAccount; - bool hasRoleBefore = hasRole(role1, account1); + bool hasOtherRoleBefore = hasRole(otherRole, otherAccount); - renounceRole@withrevert(e, role2, account2); - assert !lastReverted <=> account2 == e.msg.sender; + renounceRole@withrevert(e, role, account); + bool success = !lastReverted; - bool hasRoleAfter = hasRole(role1, account1); + bool hasOtherRoleAfter = hasRole(otherRole, otherAccount); - assert ( - hasRoleBefore != hasRoleAfter - ) => ( - hasRoleAfter == false && role1 == role2 && account1 == account2 - ); + // liveness + assert success <=> account == e.msg.sender; + + // effect + assert success => !hasRole(role, account); + + // no side effect + assert hasOtherRoleBefore != hasOtherRoleAfter => (role == otherRole && account == otherAccount); } From b952a82d29661995d75c476501f977b716860e58 Mon Sep 17 00:00:00 2001 From: Francisco Date: Wed, 8 Mar 2023 18:16:37 -0300 Subject: [PATCH 074/197] Throw error when requested specs are not found (#4101) --- certora/run.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/certora/run.js b/certora/run.js index 161465258..172309e19 100644 --- a/certora/run.js +++ b/certora/run.js @@ -8,7 +8,7 @@ const MAX_PARALLEL = 4; -const specs = require(__dirname + '/specs.json'); +let specs = require(__dirname + '/specs.json'); const proc = require('child_process'); const { PassThrough } = require('stream'); @@ -20,12 +20,18 @@ if (request.startsWith('-')) { extraOptions.unshift(request); request = ''; } -const [reqSpec, reqContract] = request.split(':').reverse(); + +if (request) { + const [reqSpec, reqContract] = request.split(':').reverse(); + specs = Object.values(specs).filter(s => reqSpec === s.spec && (!reqContract || reqContract === s.contract)); + if (specs.length === 0) { + console.error(`Error: Requested spec '${request}' not found in specs.json`); + process.exit(1); + } +} for (const { spec, contract, files, options = [] } of Object.values(specs)) { - if ((!reqSpec || reqSpec === spec) && (!reqContract || reqContract === contract)) { - limit(runCertora, spec, contract, files, [...options, ...extraOptions]); - } + limit(runCertora, spec, contract, files, [...options, ...extraOptions]); } // Run certora, aggregate the output and print it at the end From 5f7f660c6e247cf0e91d87c6d64635908997f738 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 8 Mar 2023 22:17:35 +0100 Subject: [PATCH 075/197] Add FV specs for Initializable (#4095) Co-authored-by: Francisco --- certora/harnesses/InitializableHarness.sol | 23 +++ certora/specs.json | 5 + certora/specs/Initializable.spec | 165 +++++++++++++++++++++ 3 files changed, 193 insertions(+) create mode 100644 certora/harnesses/InitializableHarness.sol create mode 100644 certora/specs/Initializable.spec diff --git a/certora/harnesses/InitializableHarness.sol b/certora/harnesses/InitializableHarness.sol new file mode 100644 index 000000000..19437e0fc --- /dev/null +++ b/certora/harnesses/InitializableHarness.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.2; + +import "../patched/proxy/utils/Initializable.sol"; + +contract InitializableHarness is Initializable { + function initialize() public initializer {} + function reinitialize(uint8 n) public reinitializer(n) {} + function disable() public { _disableInitializers(); } + + function nested_init_init() public initializer { initialize(); } + function nested_init_reinit(uint8 m) public initializer { reinitialize(m); } + function nested_reinit_init(uint8 n) public reinitializer(n) { initialize(); } + function nested_reinit_reinit(uint8 n, uint8 m) public reinitializer(n) { reinitialize(m); } + + function version() public view returns (uint8) { + return _getInitializedVersion(); + } + + function initializing() public view returns (bool) { + return _isInitializing(); + } +} diff --git a/certora/specs.json b/certora/specs.json index c22b1ebba..8e4fe21f4 100644 --- a/certora/specs.json +++ b/certora/specs.json @@ -28,5 +28,10 @@ "certora/harnesses/ERC3156FlashBorrowerHarness.sol" ], "options": ["--optimistic_loop"] + }, + { + "spec": "Initializable", + "contract": "InitializableHarness", + "files": ["certora/harnesses/InitializableHarness.sol"] } ] diff --git a/certora/specs/Initializable.spec b/certora/specs/Initializable.spec new file mode 100644 index 000000000..1ba8d54e8 --- /dev/null +++ b/certora/specs/Initializable.spec @@ -0,0 +1,165 @@ +import "helpers.spec" + +methods { + // initialize, reinitialize, disable + initialize() envfree + reinitialize(uint8) envfree + disable() envfree + + nested_init_init() envfree + nested_init_reinit(uint8) envfree + nested_reinit_init(uint8) envfree + nested_reinit_reinit(uint8,uint8) envfree + + // view + version() returns uint8 envfree + initializing() returns bool envfree +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Definitions │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +definition isUninitialized() returns bool = version() == 0; +definition isInitialized() returns bool = version() > 0; +definition isDisabled() returns bool = version() == 255; + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: A contract must only ever be in an initializing state while in the middle of a transaction execution. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant notInitializing() + !initializing() + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: The version cannot decrease & disable state is irrevocable. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule increasingVersion(env e) { + uint8 versionBefore = version(); + bool disabledBefore = isDisabled(); + + method f; calldataarg args; + f(e, args); + + assert versionBefore <= version(), "_initialized must only increase"; + assert disabledBefore => isDisabled(), "a disabled initializer must stay disabled"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Cannot initialize a contract that is already initialized. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule cannotInitializeTwice() { + require isInitialized(); + + initialize@withrevert(); + + assert lastReverted, "contract must only be initialized once"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Cannot initialize once disabled. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule cannotInitializeOnceDisabled() { + require isDisabled(); + + initialize@withrevert(); + + assert lastReverted, "contract is disabled"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Cannot reinitialize once disabled. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule cannotReinitializeOnceDisabled() { + require isDisabled(); + + uint8 n; + reinitialize@withrevert(n); + + assert lastReverted, "contract is disabled"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Cannot nest initializers (after construction). │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule cannotNestInitializers_init_init() { + nested_init_init@withrevert(); + assert lastReverted, "nested initializers"; +} + +rule cannotNestInitializers_init_reinit(uint8 m) { + nested_init_reinit@withrevert(m); + assert lastReverted, "nested initializers"; +} + +rule cannotNestInitializers_reinit_init(uint8 n) { + nested_reinit_init@withrevert(n); + assert lastReverted, "nested initializers"; +} + +rule cannotNestInitializers_reinit_reinit(uint8 n, uint8 m) { + nested_reinit_reinit@withrevert(n, m); + assert lastReverted, "nested initializers"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Initialize correctly sets the version. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule initializeEffects() { + requireInvariant notInitializing(); + + bool isUninitializedBefore = isUninitialized(); + + initialize@withrevert(); + bool success = !lastReverted; + + assert success <=> isUninitializedBefore, "can only initialize uninitialized contracts"; + assert success => version() == 1, "initialize must set version() to 1"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Reinitialize correctly sets the version. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule reinitializeEffects() { + requireInvariant notInitializing(); + + uint8 versionBefore = version(); + + uint8 n; + reinitialize@withrevert(n); + bool success = !lastReverted; + + assert success <=> versionBefore < n, "can only reinitialize to a latter versions"; + assert success => version() == n, "reinitialize must set version() to n"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Can disable. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule disableEffect() { + requireInvariant notInitializing(); + + disable@withrevert(); + bool success = !lastReverted; + + assert success, "call to _disableInitializers failed"; + assert isDisabled(), "disable state not set"; +} From 3214f6c2567f0e7938f481b0a6b5cd2fe3b13cdb Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 8 Mar 2023 23:12:37 +0100 Subject: [PATCH 076/197] Add FV specification for ERC20Wrapper (#4100) Co-authored-by: Francisco --- .changeset/tender-needles-dance.md | 5 + certora/harnesses/ERC20PermitHarness.sol | 3 +- certora/harnesses/ERC20WrapperHarness.sol | 25 +++ certora/run.js | 8 +- certora/specs.json | 12 ++ certora/specs/ERC20Wrapper.spec | 198 ++++++++++++++++++ .../token/ERC20/extensions/ERC20Wrapper.sol | 5 +- 7 files changed, 249 insertions(+), 7 deletions(-) create mode 100644 .changeset/tender-needles-dance.md create mode 100644 certora/harnesses/ERC20WrapperHarness.sol create mode 100644 certora/specs/ERC20Wrapper.spec diff --git a/.changeset/tender-needles-dance.md b/.changeset/tender-needles-dance.md new file mode 100644 index 000000000..d75adec95 --- /dev/null +++ b/.changeset/tender-needles-dance.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC20Wrapper`: self wrapping and deposit by the wrapper itself are now explicitelly forbiden. diff --git a/certora/harnesses/ERC20PermitHarness.sol b/certora/harnesses/ERC20PermitHarness.sol index cc66358b2..dd0aacae2 100644 --- a/certora/harnesses/ERC20PermitHarness.sol +++ b/certora/harnesses/ERC20PermitHarness.sol @@ -2,10 +2,9 @@ pragma solidity ^0.8.0; -import "../patched/token/ERC20/ERC20.sol"; import "../patched/token/ERC20/extensions/ERC20Permit.sol"; -contract ERC20PermitHarness is ERC20, ERC20Permit { +contract ERC20PermitHarness is ERC20Permit { constructor(string memory name, string memory symbol) ERC20(name, symbol) ERC20Permit(name) {} function mint(address account, uint256 amount) external { diff --git a/certora/harnesses/ERC20WrapperHarness.sol b/certora/harnesses/ERC20WrapperHarness.sol new file mode 100644 index 000000000..50a96cc17 --- /dev/null +++ b/certora/harnesses/ERC20WrapperHarness.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../patched/token/ERC20/extensions/ERC20Wrapper.sol"; + +contract ERC20WrapperHarness is ERC20Wrapper { + constructor(IERC20 _underlying, string memory _name, string memory _symbol) ERC20(_name, _symbol) ERC20Wrapper(_underlying) {} + + function underlyingTotalSupply() public view returns (uint256) { + return underlying().totalSupply(); + } + + function underlyingBalanceOf(address account) public view returns (uint256) { + return underlying().balanceOf(account); + } + + function underlyingAllowanceToThis(address account) public view returns (uint256) { + return underlying().allowance(account, address(this)); + } + + function recover(address account) public returns (uint256) { + return _recover(account); + } +} diff --git a/certora/run.js b/certora/run.js index 172309e19..f3234c1a3 100644 --- a/certora/run.js +++ b/certora/run.js @@ -31,7 +31,7 @@ if (request) { } for (const { spec, contract, files, options = [] } of Object.values(specs)) { - limit(runCertora, spec, contract, files, [...options, ...extraOptions]); + limit(runCertora, spec, contract, files, [...options.flatMap(opt => opt.split(' ')), ...extraOptions]); } // Run certora, aggregate the output and print it at the end @@ -50,7 +50,7 @@ async function runCertora(spec, contract, files, options = []) { const urls = data.toString('utf8').match(/https?:\S*/g); for (const url of urls ?? []) { if (url.includes('/jobStatus/')) { - console.error(`[${spec}] ${url}`); + console.error(`[${spec}] ${url.replace('/jobStatus/', '/output/')}`); stream.off('data', logStatusUrl); break; } @@ -108,8 +108,8 @@ function writeEntry(spec, contract, success, url) { spec, contract, success ? ':x:' : ':heavy_check_mark:', - `[link](${url})`, - `[link](${url?.replace('/jobStatus/', '/output/')})`, + url ? `[link](${url})` : 'error', + url ? `[link](${url?.replace('/jobStatus/', '/output/')})` : 'error', ), ); } diff --git a/certora/specs.json b/certora/specs.json index 8e4fe21f4..228e85fe4 100644 --- a/certora/specs.json +++ b/certora/specs.json @@ -29,6 +29,18 @@ ], "options": ["--optimistic_loop"] }, + { + "spec": "ERC20Wrapper", + "contract": "ERC20WrapperHarness", + "files": [ + "certora/harnesses/ERC20PermitHarness.sol", + "certora/harnesses/ERC20WrapperHarness.sol" + ], + "options": [ + "--link ERC20WrapperHarness:_underlying=ERC20PermitHarness", + "--optimistic_loop" + ] + }, { "spec": "Initializable", "contract": "InitializableHarness", diff --git a/certora/specs/ERC20Wrapper.spec b/certora/specs/ERC20Wrapper.spec new file mode 100644 index 000000000..c10173766 --- /dev/null +++ b/certora/specs/ERC20Wrapper.spec @@ -0,0 +1,198 @@ +import "helpers.spec" +import "ERC20.spec" + +methods { + underlying() returns(address) envfree + underlyingTotalSupply() returns(uint256) envfree + underlyingBalanceOf(address) returns(uint256) envfree + underlyingAllowanceToThis(address) returns(uint256) envfree + + depositFor(address, uint256) returns(bool) + withdrawTo(address, uint256) returns(bool) + recover(address) returns(uint256) +} + +use invariant totalSupplyIsSumOfBalances + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Helper: consequence of `totalSupplyIsSumOfBalances` applied to underlying │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +function underlyingBalancesLowerThanUnderlyingSupply(address a) returns bool { + return underlyingBalanceOf(a) <= underlyingTotalSupply(); +} + +function sumOfUnderlyingBalancesLowerThanUnderlyingSupply(address a, address b) returns bool { + return a != b => underlyingBalanceOf(a) + underlyingBalanceOf(b) <= underlyingTotalSupply(); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: wrapped token can't be undercollateralized (solvency of the wrapper) │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant totalSupplyIsSmallerThanUnderlyingBalance() + totalSupply() <= underlyingBalanceOf(currentContract) && + underlyingBalanceOf(currentContract) <= underlyingTotalSupply() && + underlyingTotalSupply() <= max_uint256 + { + preserved { + requireInvariant totalSupplyIsSumOfBalances; + require underlyingBalancesLowerThanUnderlyingSupply(currentContract); + } + preserved depositFor(address account, uint256 amount) with (env e) { + require sumOfUnderlyingBalancesLowerThanUnderlyingSupply(e.msg.sender, currentContract); + } + } + +invariant noSelfWrap() + currentContract != underlying() + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: depositFor liveness and effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule depositFor(env e) { + require nonpayable(e); + + address sender = e.msg.sender; + address receiver; + address other; + uint256 amount; + + // sanity + requireInvariant noSelfWrap; + requireInvariant totalSupplyIsSumOfBalances; + requireInvariant totalSupplyIsSmallerThanUnderlyingBalance; + require sumOfUnderlyingBalancesLowerThanUnderlyingSupply(currentContract, sender); + + uint256 balanceBefore = balanceOf(receiver); + uint256 supplyBefore = totalSupply(); + uint256 senderUnderlyingBalanceBefore = underlyingBalanceOf(sender); + uint256 senderUnderlyingAllowanceBefore = underlyingAllowanceToThis(sender); + uint256 wrapperUnderlyingBalanceBefore = underlyingBalanceOf(currentContract); + uint256 underlyingSupplyBefore = underlyingTotalSupply(); + + uint256 otherBalanceBefore = balanceOf(other); + uint256 otherUnderlyingBalanceBefore = underlyingBalanceOf(other); + + depositFor@withrevert(e, receiver, amount); + bool success = !lastReverted; + + // liveness + assert success <=> ( + sender != currentContract && // invalid sender + sender != 0 && // invalid sender + receiver != 0 && // invalid receiver + amount <= senderUnderlyingBalanceBefore && // deposit doesn't exceed balance + amount <= senderUnderlyingAllowanceBefore // deposit doesn't exceed allowance + ); + + // effects + assert success => ( + balanceOf(receiver) == balanceBefore + amount && + totalSupply() == supplyBefore + amount && + underlyingBalanceOf(currentContract) == wrapperUnderlyingBalanceBefore + amount && + underlyingBalanceOf(sender) == senderUnderlyingBalanceBefore - amount + ); + + // no side effect + assert underlyingTotalSupply() == underlyingSupplyBefore; + assert balanceOf(other) != otherBalanceBefore => other == receiver; + assert underlyingBalanceOf(other) != otherUnderlyingBalanceBefore => (other == sender || other == currentContract); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: withdrawTo liveness and effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule withdrawTo(env e) { + require nonpayable(e); + + address sender = e.msg.sender; + address receiver; + address other; + uint256 amount; + + // sanity + requireInvariant noSelfWrap; + requireInvariant totalSupplyIsSumOfBalances; + requireInvariant totalSupplyIsSmallerThanUnderlyingBalance; + require sumOfUnderlyingBalancesLowerThanUnderlyingSupply(currentContract, receiver); + + uint256 balanceBefore = balanceOf(sender); + uint256 supplyBefore = totalSupply(); + uint256 receiverUnderlyingBalanceBefore = underlyingBalanceOf(receiver); + uint256 wrapperUnderlyingBalanceBefore = underlyingBalanceOf(currentContract); + uint256 underlyingSupplyBefore = underlyingTotalSupply(); + + uint256 otherBalanceBefore = balanceOf(other); + uint256 otherUnderlyingBalanceBefore = underlyingBalanceOf(other); + + withdrawTo@withrevert(e, receiver, amount); + bool success = !lastReverted; + + // liveness + assert success <=> ( + sender != 0 && // invalid sender + receiver != 0 && // invalid receiver + amount <= balanceBefore // withdraw doesn't exceed balance + ); + + // effects + assert success => ( + balanceOf(sender) == balanceBefore - amount && + totalSupply() == supplyBefore - amount && + underlyingBalanceOf(currentContract) == wrapperUnderlyingBalanceBefore - (currentContract != receiver ? amount : 0) && + underlyingBalanceOf(receiver) == receiverUnderlyingBalanceBefore + (currentContract != receiver ? amount : 0) + ); + + // no side effect + assert underlyingTotalSupply() == underlyingSupplyBefore; + assert balanceOf(other) != otherBalanceBefore => other == sender; + assert underlyingBalanceOf(other) != otherUnderlyingBalanceBefore => (other == receiver || other == currentContract); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: recover liveness and effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule recover(env e) { + require nonpayable(e); + + address receiver; + address other; + + // sanity + requireInvariant noSelfWrap; + requireInvariant totalSupplyIsSumOfBalances; + requireInvariant totalSupplyIsSmallerThanUnderlyingBalance; + + uint256 value = underlyingBalanceOf(currentContract) - totalSupply(); + uint256 supplyBefore = totalSupply(); + uint256 balanceBefore = balanceOf(receiver); + + uint256 otherBalanceBefore = balanceOf(other); + uint256 otherUnderlyingBalanceBefore = underlyingBalanceOf(other); + + recover@withrevert(e, receiver); + bool success = !lastReverted; + + // liveness + assert success <=> receiver != 0; + + // effect + assert success => ( + balanceOf(receiver) == balanceBefore + value && + totalSupply() == supplyBefore + value && + totalSupply() == underlyingBalanceOf(currentContract) + ); + + // no side effect + assert underlyingBalanceOf(other) == otherUnderlyingBalanceBefore; + assert balanceOf(other) != otherBalanceBefore => other == receiver; +} diff --git a/contracts/token/ERC20/extensions/ERC20Wrapper.sol b/contracts/token/ERC20/extensions/ERC20Wrapper.sol index ce515693e..bfe782e43 100644 --- a/contracts/token/ERC20/extensions/ERC20Wrapper.sol +++ b/contracts/token/ERC20/extensions/ERC20Wrapper.sol @@ -19,6 +19,7 @@ abstract contract ERC20Wrapper is ERC20 { IERC20 private immutable _underlying; constructor(IERC20 underlyingToken) { + require(underlyingToken != this, "ERC20Wrapper: cannot self wrap"); _underlying = underlyingToken; } @@ -44,7 +45,9 @@ abstract contract ERC20Wrapper is ERC20 { * @dev Allow a user to deposit underlying tokens and mint the corresponding number of wrapped tokens. */ function depositFor(address account, uint256 amount) public virtual returns (bool) { - SafeERC20.safeTransferFrom(_underlying, _msgSender(), address(this), amount); + address sender = _msgSender(); + require(sender != address(this), "ERC20Wrapper: wrapper can't deposit"); + SafeERC20.safeTransferFrom(_underlying, sender, address(this), amount); _mint(account, amount); return true; } From 58a62916de431db0756a8f474725f861c30c5f36 Mon Sep 17 00:00:00 2001 From: Paul Razvan Berg Date: Thu, 9 Mar 2023 19:41:08 +0200 Subject: [PATCH 077/197] Bump and pin Forge Std submodule (#4102) --- .gitmodules | 1 + lib/forge-std | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index aaa229f8d..08a09bbcf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,5 @@ [submodule "lib/forge-std"] + branch = v1 path = lib/forge-std url = https://github.com/foundry-rs/forge-std [submodule "lib/erc4626-tests"] diff --git a/lib/forge-std b/lib/forge-std index eb980e1d4..c2236853a 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit eb980e1d4f0e8173ec27da77297ae411840c8ccb +Subproject commit c2236853aadb8e2d9909bbecdc490099519b70a4 From f8e3c375d19bd12f54222109dd0801c0e0b60dd2 Mon Sep 17 00:00:00 2001 From: Francisco Date: Thu, 9 Mar 2023 17:55:52 -0300 Subject: [PATCH 078/197] Credit YieldBox for virtual offset (#4103) --- docs/modules/ROOT/pages/erc4626.adoc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/erc4626.adoc b/docs/modules/ROOT/pages/erc4626.adoc index 00d46a4d0..c8adce736 100644 --- a/docs/modules/ROOT/pages/erc4626.adoc +++ b/docs/modules/ROOT/pages/erc4626.adoc @@ -102,14 +102,13 @@ In this scenario, the attack is stem:[n] times less powerful (in how much it is === Defending with a virtual offset -The defense we propose consists of two parts: +The defense we propose is based on the approach used in link:https://github.com/boringcrypto/YieldBox[YieldBox]. It consists of two parts: - Use an offset between the "precision" of the representation of shares and assets. Said otherwise, we use more decimal places to represent the shares than the underlying token does to represent the assets. - Include virtual shares and virtual assets in the exchange rate computation. These virtual assets enforce the conversion rate when the vault is empty. These two parts work together in enforcing the security of the vault. First, the increased precision corresponds to a high rate, which we saw is safer as it reduces the rounding error when computing the amount of shares. Second, the virtual assets and shares (in addition to simplifying a lot of the computations) capture part of the donation, making it unprofitable for a developer to perform an attack. - Following the previous math definitions, we have: - stem:[\delta] the vault offset From 6794c9460bd557bde71fb71b595369d86728c3ee Mon Sep 17 00:00:00 2001 From: Francisco Date: Mon, 13 Mar 2023 12:45:58 -0300 Subject: [PATCH 079/197] Run formal verification when label is added (#4112) --- .github/workflows/formal-verification.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/formal-verification.yml b/.github/workflows/formal-verification.yml index 6c9935ad4..a1b3b6e6d 100644 --- a/.github/workflows/formal-verification.yml +++ b/.github/workflows/formal-verification.yml @@ -5,7 +5,12 @@ on: branches: - master - release-v* - pull_request: {} + pull_request: + types: + - opened + - reopened + - synchronize + - labeled workflow_dispatch: {} env: From e739144cb02dbbd73256a2ae79f024faf3c7ea0a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 18:31:24 -0300 Subject: [PATCH 080/197] Update dependency certora-cli to v3.6.4 (#4110) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Francisco --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 797b3598d..da3e95766 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -certora-cli==3.6.3 +certora-cli==3.6.4 From e69248e551482db39bcd54031e85f769718af587 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Mon, 13 Mar 2023 21:05:14 -0300 Subject: [PATCH 081/197] Limit concurrency of formal-verification runs --- .github/workflows/formal-verification.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/formal-verification.yml b/.github/workflows/formal-verification.yml index a1b3b6e6d..29c02541c 100644 --- a/.github/workflows/formal-verification.yml +++ b/.github/workflows/formal-verification.yml @@ -18,6 +18,8 @@ env: JAVA_VERSION: '11' SOLC_VERSION: '0.8.19' +concurrency: ${{ github.workflow }}-${{ github.ref }} + jobs: apply-diff: runs-on: ubuntu-latest From ea2d5ad2e780ed8cfa6f59c5942dce070ff14d72 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 21:09:01 -0300 Subject: [PATCH 082/197] Update lockfile (#4080) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 814 ++++++++++++++++++++++++++-------------------- 1 file changed, 456 insertions(+), 358 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9b491e05..f44a1b939 100644 --- a/package-lock.json +++ b/package-lock.json @@ -499,15 +499,39 @@ "deprecated": "Please use @ensdomains/ens-contracts", "dev": true }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz", + "integrity": "sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.0.tgz", + "integrity": "sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", - "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.1.tgz", + "integrity": "sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.4.0", + "espree": "^9.5.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -540,6 +564,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@eslint/js": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.36.0.tgz", + "integrity": "sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@ethereumjs/common": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-2.5.0.tgz", @@ -1698,30 +1731,30 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.0.tgz", - "integrity": "sha512-xGWAiVCGOycvGiP/qrlf9f9eOn7fpNbyJygcB0P21a1MDuVPlKt0Srp7rvtBEutYQ48ouYnRXm33zlRnlTOPHg==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.1.tgz", + "integrity": "sha512-1LMtXj1puAxyFusBgUIy5pZk3073cNXYnXUpuNKFghHbIit/xZgbk0AokpUADbNm3gyD6bFWl3LRFh3dhVdREg==", "dev": true, "engines": { "node": ">= 12" }, "optionalDependencies": { - "@nomicfoundation/solidity-analyzer-darwin-arm64": "0.1.0", - "@nomicfoundation/solidity-analyzer-darwin-x64": "0.1.0", - "@nomicfoundation/solidity-analyzer-freebsd-x64": "0.1.0", - "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": "0.1.0", - "@nomicfoundation/solidity-analyzer-linux-arm64-musl": "0.1.0", - "@nomicfoundation/solidity-analyzer-linux-x64-gnu": "0.1.0", - "@nomicfoundation/solidity-analyzer-linux-x64-musl": "0.1.0", - "@nomicfoundation/solidity-analyzer-win32-arm64-msvc": "0.1.0", - "@nomicfoundation/solidity-analyzer-win32-ia32-msvc": "0.1.0", - "@nomicfoundation/solidity-analyzer-win32-x64-msvc": "0.1.0" + "@nomicfoundation/solidity-analyzer-darwin-arm64": "0.1.1", + "@nomicfoundation/solidity-analyzer-darwin-x64": "0.1.1", + "@nomicfoundation/solidity-analyzer-freebsd-x64": "0.1.1", + "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": "0.1.1", + "@nomicfoundation/solidity-analyzer-linux-arm64-musl": "0.1.1", + "@nomicfoundation/solidity-analyzer-linux-x64-gnu": "0.1.1", + "@nomicfoundation/solidity-analyzer-linux-x64-musl": "0.1.1", + "@nomicfoundation/solidity-analyzer-win32-arm64-msvc": "0.1.1", + "@nomicfoundation/solidity-analyzer-win32-ia32-msvc": "0.1.1", + "@nomicfoundation/solidity-analyzer-win32-x64-msvc": "0.1.1" } }, "node_modules/@nomicfoundation/solidity-analyzer-darwin-arm64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.0.tgz", - "integrity": "sha512-vEF3yKuuzfMHsZecHQcnkUrqm8mnTWfJeEVFHpg+cO+le96xQA4lAJYdUan8pXZohQxv1fSReQsn4QGNuBNuCw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.1.tgz", + "integrity": "sha512-KcTodaQw8ivDZyF+D76FokN/HdpgGpfjc/gFCImdLUyqB6eSWVaZPazMbeAjmfhx3R0zm/NYVzxwAokFKgrc0w==", "cpu": [ "arm64" ], @@ -1735,9 +1768,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-darwin-x64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.1.0.tgz", - "integrity": "sha512-dlHeIg0pTL4dB1l9JDwbi/JG6dHQaU1xpDK+ugYO8eJ1kxx9Dh2isEUtA4d02cQAl22cjOHTvifAk96A+ItEHA==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.1.1.tgz", + "integrity": "sha512-XhQG4BaJE6cIbjAVtzGOGbK3sn1BO9W29uhk9J8y8fZF1DYz0Doj8QDMfpMu+A6TjPDs61lbsmeYodIDnfveSA==", "cpu": [ "x64" ], @@ -1751,9 +1784,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-freebsd-x64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-freebsd-x64/-/solidity-analyzer-freebsd-x64-0.1.0.tgz", - "integrity": "sha512-WFCZYMv86WowDA4GiJKnebMQRt3kCcFqHeIomW6NMyqiKqhK1kIZCxSLDYsxqlx396kKLPN1713Q1S8tu68GKg==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-freebsd-x64/-/solidity-analyzer-freebsd-x64-0.1.1.tgz", + "integrity": "sha512-GHF1VKRdHW3G8CndkwdaeLkVBi5A9u2jwtlS7SLhBc8b5U/GcoL39Q+1CSO3hYqePNP+eV5YI7Zgm0ea6kMHoA==", "cpu": [ "x64" ], @@ -1767,9 +1800,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-linux-arm64-gnu": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.1.0.tgz", - "integrity": "sha512-DTw6MNQWWlCgc71Pq7CEhEqkb7fZnS7oly13pujs4cMH1sR0JzNk90Mp1zpSCsCs4oKan2ClhMlLKtNat/XRKQ==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.1.1.tgz", + "integrity": "sha512-g4Cv2fO37ZsUENQ2vwPnZc2zRenHyAxHcyBjKcjaSmmkKrFr64yvzeNO8S3GBFCo90rfochLs99wFVGT/0owpg==", "cpu": [ "arm64" ], @@ -1783,9 +1816,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-linux-arm64-musl": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.1.0.tgz", - "integrity": "sha512-wUpUnR/3GV5Da88MhrxXh/lhb9kxh9V3Jya2NpBEhKDIRCDmtXMSqPMXHZmOR9DfCwCvG6vLFPr/+YrPCnUN0w==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.1.1.tgz", + "integrity": "sha512-WJ3CE5Oek25OGE3WwzK7oaopY8xMw9Lhb0mlYuJl/maZVo+WtP36XoQTb7bW/i8aAdHW5Z+BqrHMux23pvxG3w==", "cpu": [ "arm64" ], @@ -1799,9 +1832,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-gnu": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.0.tgz", - "integrity": "sha512-lR0AxK1x/MeKQ/3Pt923kPvwigmGX3OxeU5qNtQ9pj9iucgk4PzhbS3ruUeSpYhUxG50jN4RkIGwUMoev5lguw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.1.tgz", + "integrity": "sha512-5WN7leSr5fkUBBjE4f3wKENUy9HQStu7HmWqbtknfXkkil+eNWiBV275IOlpXku7v3uLsXTOKpnnGHJYI2qsdA==", "cpu": [ "x64" ], @@ -1815,9 +1848,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-musl": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.0.tgz", - "integrity": "sha512-A1he/8gy/JeBD3FKvmI6WUJrGrI5uWJNr5Xb9WdV+DK0F8msuOqpEByLlnTdLkXMwW7nSl3awvLezOs9xBHJEg==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.1.tgz", + "integrity": "sha512-KdYMkJOq0SYPQMmErv/63CwGwMm5XHenEna9X9aB8mQmhDBrYrlAOSsIPgFCUSL0hjxE3xHP65/EPXR/InD2+w==", "cpu": [ "x64" ], @@ -1831,9 +1864,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-win32-arm64-msvc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-arm64-msvc/-/solidity-analyzer-win32-arm64-msvc-0.1.0.tgz", - "integrity": "sha512-7x5SXZ9R9H4SluJZZP8XPN+ju7Mx+XeUMWZw7ZAqkdhP5mK19I4vz3x0zIWygmfE8RT7uQ5xMap0/9NPsO+ykw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-arm64-msvc/-/solidity-analyzer-win32-arm64-msvc-0.1.1.tgz", + "integrity": "sha512-VFZASBfl4qiBYwW5xeY20exWhmv6ww9sWu/krWSesv3q5hA0o1JuzmPHR4LPN6SUZj5vcqci0O6JOL8BPw+APg==", "cpu": [ "arm64" ], @@ -1847,9 +1880,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-win32-ia32-msvc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-ia32-msvc/-/solidity-analyzer-win32-ia32-msvc-0.1.0.tgz", - "integrity": "sha512-m7w3xf+hnE774YRXu+2mGV7RiF3QJtUoiYU61FascCkQhX3QMQavh7saH/vzb2jN5D24nT/jwvaHYX/MAM9zUw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-ia32-msvc/-/solidity-analyzer-win32-ia32-msvc-0.1.1.tgz", + "integrity": "sha512-JnFkYuyCSA70j6Si6cS1A9Gh1aHTEb8kOTBApp/c7NRTFGNMH8eaInKlyuuiIbvYFhlXW4LicqyYuWNNq9hkpQ==", "cpu": [ "ia32" ], @@ -1863,9 +1896,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-win32-x64-msvc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.0.tgz", - "integrity": "sha512-xCuybjY0sLJQnJhupiFAXaek2EqF0AP0eBjgzaalPXSNvCEN6ZYHvUzdA50ENDVeSYFXcUsYf3+FsD3XKaeptA==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.1.tgz", + "integrity": "sha512-HrVJr6+WjIXGnw3Q9u6KQcbZCtk0caVWhCdFADySvRyUxJ8PnzlaP+MhwNE8oyT8OZ6ejHBRrrgjSqDCFXGirw==", "cpu": [ "x64" ], @@ -2082,9 +2115,9 @@ } }, "node_modules/@openzeppelin/upgrades-core": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.24.0.tgz", - "integrity": "sha512-lXf1tUrCZ3Q/YmWhw0cuSSOHMp0OAsmeOg1fhSGEM6nQQ6cIVlFvq2pCV5hZMb7xkOm5pmmzV8JW1W3kfW6Lfw==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.24.1.tgz", + "integrity": "sha512-QhdIQDUykJ3vQauB6CheV7vk4zgn0e1iY+IDg7r1KqpA1m2bqIGjQCpzidW33K4bZc9zdJSPx2/Z6Um5KxCB7A==", "dev": true, "dependencies": { "cbor": "^8.0.0", @@ -2347,9 +2380,9 @@ } }, "node_modules/@truffle/abi-utils": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@truffle/abi-utils/-/abi-utils-0.3.8.tgz", - "integrity": "sha512-RevMtlEuQw9cE2VWzQuvLDSYPCVSZQQUOsaK8ArcwxXct6ugiJCDC7tNouhHzP1Y4ccdZtG2y/XPHQJdzgYdZQ==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@truffle/abi-utils/-/abi-utils-0.3.9.tgz", + "integrity": "sha512-G5dqgwRHx5zwlXjz3QT8OJVfB2cOqWwD6DwKso0KttUt/zejhCjnkKq72rSgyeLMkz7wBB9ERLOsupLBILM8MA==", "dev": true, "dependencies": { "change-case": "3.0.2", @@ -2364,13 +2397,13 @@ "dev": true }, "node_modules/@truffle/codec": { - "version": "0.14.15", - "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.14.15.tgz", - "integrity": "sha512-cooWy8blmvYQQRBKgzWJnDUS6mZE9cvnmpVN15jU6TseAQkBtmfYfCH12QCKEWMq2+R4yQICP54WNCYg78g72g==", + "version": "0.14.16", + "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.14.16.tgz", + "integrity": "sha512-a9UY3n/FnkKN3Q4zOuMFOOcLWb80mdknj+voim4vvXYtJm1aAZQZE5sG9aLnMBTl4TiGLzUtfNDVYY7WgWgDag==", "dev": true, "dependencies": { - "@truffle/abi-utils": "^0.3.8", - "@truffle/compile-common": "^0.9.3", + "@truffle/abi-utils": "^0.3.9", + "@truffle/compile-common": "^0.9.4", "big.js": "^6.0.3", "bn.js": "^5.1.3", "cbor": "^5.2.0", @@ -2452,9 +2485,9 @@ "dev": true }, "node_modules/@truffle/compile-common": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@truffle/compile-common/-/compile-common-0.9.3.tgz", - "integrity": "sha512-9S86H5DRC0zEj164KeClP/6jVt1M/nRd7St89h6QbuIU0JjpqSz1SXpkvqhbFoV9hhW+4ZGh0NysjrdPlk7gFw==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@truffle/compile-common/-/compile-common-0.9.4.tgz", + "integrity": "sha512-mnqJB/hLiPHNf+WKwt/2MH6lv34xSG/SFCib7+ckAklutUqVLeFo8EwQxinuHNkU7LY0C+YgZXhK1WTCO5YRJQ==", "dev": true, "dependencies": { "@truffle/error": "^0.2.0", @@ -2468,17 +2501,17 @@ "dev": true }, "node_modules/@truffle/contract": { - "version": "4.6.15", - "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.15.tgz", - "integrity": "sha512-3V+50z94XRh+I3axz6Zrot1wlMhRiG2O9XjR20LwR5YBnzHV7YFfOs8x6pluXVLQyxcVJBISJyqxvGxpkecrZg==", + "version": "4.6.17", + "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.17.tgz", + "integrity": "sha512-sIMam5Wqr9AEiqHfOcWGJGqTv8Qy+BT765PaNHUUT6JBAY+tpHM3FlQd2nM6zLJ8paR3SLDGIthkhCBH/KNgDA==", "dev": true, "dependencies": { "@ensdomains/ensjs": "^2.1.0", "@truffle/blockchain-utils": "^0.1.6", - "@truffle/contract-schema": "^3.4.12", - "@truffle/debug-utils": "^6.0.46", + "@truffle/contract-schema": "^3.4.13", + "@truffle/debug-utils": "^6.0.47", "@truffle/error": "^0.2.0", - "@truffle/interface-adapter": "^0.5.29", + "@truffle/interface-adapter": "^0.5.30", "bignumber.js": "^7.2.1", "debug": "^4.3.1", "ethers": "^4.0.32", @@ -2490,9 +2523,9 @@ } }, "node_modules/@truffle/contract-schema": { - "version": "3.4.12", - "resolved": "https://registry.npmjs.org/@truffle/contract-schema/-/contract-schema-3.4.12.tgz", - "integrity": "sha512-XpMMps/bqHwiRuCyLiEEGWEAvGGzGj4u1X1+lzxrtIsrwbQhSZcdgEbXl9vGxOOJWOup3HXRCIsjlao27kS4OA==", + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/@truffle/contract-schema/-/contract-schema-3.4.13.tgz", + "integrity": "sha512-emG7upuryYFrsPDbHqeASPWXL824M1tinhQwSPG0phSoa3g+RX9fUNNN/VPmF3tSkXLWUMhRnb7ehxnaCuRbZg==", "dev": true, "dependencies": { "ajv": "^6.10.0", @@ -2506,12 +2539,12 @@ "dev": true }, "node_modules/@truffle/debug-utils": { - "version": "6.0.46", - "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.46.tgz", - "integrity": "sha512-MWko3c1M0NI8IT0yNh8Dh2xoDgR2R1apP8hc/9VsR/4Q7P6OE/578WRpZnvo2HWXYBiJ2PrWBDtMYBHntImrrA==", + "version": "6.0.47", + "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.47.tgz", + "integrity": "sha512-bUjdzLPdEKtoUCDzaXkrOoi+PbyAJlMBzGequBK8tirT7xL9bCP2Pd/WxvnmRd7AnfroxGNvXwVXWTItW5SMWQ==", "dev": true, "dependencies": { - "@truffle/codec": "^0.14.15", + "@truffle/codec": "^0.14.16", "@trufflesuite/chromafi": "^3.0.0", "bn.js": "^5.1.3", "chalk": "^2.4.2", @@ -2532,9 +2565,9 @@ "dev": true }, "node_modules/@truffle/interface-adapter": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.29.tgz", - "integrity": "sha512-6UlJ+f87z7y/dWk9UfbIU+4e80iRsp8h03LEiE5B+PvZbr6cuMjLJUBtBBQZMo3+xrIcS/2u3p5hOxW8OJm8tw==", + "version": "0.5.30", + "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.30.tgz", + "integrity": "sha512-wyCcESeZJBkAfuSGU8GHCusIWDUDyQjJZMcyShv59ZTSAwQR7xx0+a0Q1LlS532G/pGFLYe2VzKSY1pRHRwgug==", "dev": true, "dependencies": { "bn.js": "^5.1.3", @@ -3006,6 +3039,19 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -3367,6 +3413,21 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -4227,9 +4288,9 @@ } }, "node_modules/cosmiconfig": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.0.0.tgz", - "integrity": "sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.0.tgz", + "integrity": "sha512-0tLZ9URlPGU7JsKq0DQOQ3FoRsYX8xDZ7xMiATQfaiGMz7EHowNkbU9u1coAOmnh9p/1ySpm0RB3JNWRXM5GCg==", "dev": true, "dependencies": { "import-fresh": "^3.2.1", @@ -4239,6 +4300,9 @@ }, "engines": { "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" } }, "node_modules/cosmiconfig/node_modules/argparse": { @@ -4881,18 +4945,18 @@ } }, "node_modules/es-abstract": { - "version": "1.21.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz", - "integrity": "sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==", + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", "dev": true, "dependencies": { + "array-buffer-byte-length": "^1.0.0", "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", @@ -4900,8 +4964,8 @@ "has-property-descriptors": "^1.0.0", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.4", - "is-array-buffer": "^3.0.1", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", @@ -4909,11 +4973,12 @@ "is-string": "^1.0.7", "is-typed-array": "^1.1.10", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.4.3", "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", "string.prototype.trimend": "^1.0.6", "string.prototype.trimstart": "^1.0.6", "typed-array-length": "^1.0.4", @@ -5135,12 +5200,15 @@ } }, "node_modules/eslint": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.34.0.tgz", - "integrity": "sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.36.0.tgz", + "integrity": "sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.4.1", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.1", + "@eslint/js": "8.36.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -5151,10 +5219,9 @@ "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", + "espree": "^9.5.0", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", @@ -5175,7 +5242,6 @@ "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.1", - "regexpp": "^3.2.0", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" @@ -5191,9 +5257,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz", - "integrity": "sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.7.0.tgz", + "integrity": "sha512-HHVXLSlVUhMSmyW4ZzEuvjpwqamgmlfkutD53cYXLikh4pt/modINRcCIApJ84czDxM4GZInwUrromsDdTImTA==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" @@ -5215,33 +5281,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint-visitor-keys": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", @@ -5431,9 +5470,9 @@ } }, "node_modules/espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.0.tgz", + "integrity": "sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw==", "dev": true, "dependencies": { "acorn": "^8.8.0", @@ -5461,9 +5500,9 @@ } }, "node_modules/esquery": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz", - "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -6422,6 +6461,21 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/express/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/express/node_modules/raw-body": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", @@ -7203,9 +7257,9 @@ } }, "node_modules/hardhat": { - "version": "2.12.7", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.12.7.tgz", - "integrity": "sha512-voWoN6zn5d8BOEaczSyK/1PyfdeOeI3SbGCFb36yCHTJUt6OIqLb+ZDX30VhA1UsYKzLqG7UnWl3fKJUuANc6A==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.13.0.tgz", + "integrity": "sha512-ZlzBOLML1QGlm6JWyVAG8lVTEAoOaVm1in/RU2zoGAnYEoD1Rp4T+ZMvrLNhHaaeS9hfjJ1gJUBfiDr4cx+htQ==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.1.2", @@ -7260,10 +7314,10 @@ "ws": "^7.4.6" }, "bin": { - "hardhat": "internal/cli/cli.js" + "hardhat": "internal/cli/bootstrap.js" }, "engines": { - "node": "^14.0.0 || ^16.0.0 || ^18.0.0" + "node": ">=14.0.0" }, "peerDependencies": { "ts-node": "*", @@ -7884,9 +7938,9 @@ } }, "node_modules/immutable": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz", - "integrity": "sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", + "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", "dev": true }, "node_modules/import-fresh": { @@ -8021,13 +8075,13 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz", - "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "is-typed-array": "^1.1.10" }, "funding": { @@ -10512,12 +10566,12 @@ } }, "node_modules/prettier-plugin-solidity": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.2.tgz", - "integrity": "sha512-KC5oNbFJfyBaFiO0kl56J6AXnDmr9tUlBV1iqo864x4KQrKYKaBZvW9jhT2oC0NHoNp7/GoMJNxqL8pp8k7C/g==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.3.tgz", + "integrity": "sha512-fQ9yucPi2sBbA2U2Xjh6m4isUTJ7S7QLc/XDDsktqqxYfTwdYKJ0EnnywXHwCGAaYbQNK+HIYPL1OemxuMsgeg==", "dev": true, "dependencies": { - "@solidity-parser/parser": "^0.15.0", + "@solidity-parser/parser": "^0.16.0", "semver": "^7.3.8", "solidity-comments-extractor": "^0.0.7" }, @@ -10529,9 +10583,9 @@ } }, "node_modules/prettier-plugin-solidity/node_modules/@solidity-parser/parser": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.15.0.tgz", - "integrity": "sha512-5UFJJTzWi1hgFk6aGCZ5rxG2DJkCJOzJ74qg7UkWSNCDSigW+CJLoYUb5bLiKrtI34Nr9rpFSUNHfkqtlL+N/w==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.0.tgz", + "integrity": "sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q==", "dev": true, "dependencies": { "antlr4ts": "^0.5.0-alpha.4" @@ -10633,9 +10687,9 @@ ] }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", + "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", "dev": true, "dependencies": { "side-channel": "^1.0.4" @@ -10789,9 +10843,9 @@ } }, "node_modules/readable-stream": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", - "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "dependencies": { "inherits": "^2.0.3", @@ -10886,18 +10940,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, "node_modules/req-cwd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/req-cwd/-/req-cwd-2.0.0.tgz", @@ -12240,12 +12282,12 @@ } }, "node_modules/solhint": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/solhint/-/solhint-3.4.0.tgz", - "integrity": "sha512-FYEs/LoTxMsWFP/OGsEqR1CBDn3Bn7hrTWsgtjai17MzxITgearIdlo374KKZjjIycu8E2xBcJ+RSWeoBvQmkw==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/solhint/-/solhint-3.4.1.tgz", + "integrity": "sha512-pzZn2RlZhws1XwvLPVSsxfHrwsteFf5eySOhpAytzXwKQYbTCJV6z8EevYDiSVKMpWrvbKpEtJ055CuEmzp4Xg==", "dev": true, "dependencies": { - "@solidity-parser/parser": "^0.15.0", + "@solidity-parser/parser": "^0.16.0", "ajv": "^6.12.6", "antlr4": "^4.11.0", "ast-parents": "^0.0.1", @@ -12271,9 +12313,9 @@ } }, "node_modules/solhint/node_modules/@solidity-parser/parser": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.15.0.tgz", - "integrity": "sha512-5UFJJTzWi1hgFk6aGCZ5rxG2DJkCJOzJ74qg7UkWSNCDSigW+CJLoYUb5bLiKrtI34Nr9rpFSUNHfkqtlL+N/w==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.0.tgz", + "integrity": "sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q==", "dev": true, "dependencies": { "antlr4ts": "^0.5.0-alpha.4" @@ -13113,9 +13155,9 @@ } }, "node_modules/solidity-docgen": { - "version": "0.6.0-beta.34", - "resolved": "https://registry.npmjs.org/solidity-docgen/-/solidity-docgen-0.6.0-beta.34.tgz", - "integrity": "sha512-igdGrkg8gT1jn+B2NwzjEtSf+7NTrSi/jz88zO7MZWgETmcWbXaxgAsQP4BQeC4YFeH0Pie1NsLP7+9qDgvFtA==", + "version": "0.6.0-beta.35", + "resolved": "https://registry.npmjs.org/solidity-docgen/-/solidity-docgen-0.6.0-beta.35.tgz", + "integrity": "sha512-9QdwK1THk/MWIdq1PEW/6dvtND0pUqpFTsbKwwU9YQIMYuRhH1lek9SsgnsGGYtdJ0VTrXXcVT30q20a8Y610A==", "dev": true, "dependencies": { "handlebars": "^4.7.7", @@ -13228,9 +13270,9 @@ "dev": true }, "node_modules/spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "dependencies": { "spdx-expression-parse": "^3.0.0", @@ -13254,9 +13296,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", "dev": true }, "node_modules/sprintf-js": { @@ -13384,6 +13426,23 @@ "node": ">=4" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/string.prototype.trimend": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", @@ -14161,9 +14220,9 @@ "dev": true }, "node_modules/undici": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.20.0.tgz", - "integrity": "sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.21.0.tgz", + "integrity": "sha512-HOjK8l6a57b2ZGXOcUsI5NLfoTrfmbOl90ixJDl0AEFG4wgHNDQxtZy15/ZQp7HhjkpaGlp/eneMgtsu1dIlUA==", "dev": true, "dependencies": { "busboy": "^1.6.0" @@ -15628,15 +15687,30 @@ "integrity": "sha512-bvaTH34PMCbv6anRa9I/0zjLJgY4EuznbEMgbV77JBCQ9KNC46rzi0avuxpOfu+xDjPEtSFGqVEOr5GlUSGudA==", "dev": true }, + "@eslint-community/eslint-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz", + "integrity": "sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.0.tgz", + "integrity": "sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==", + "dev": true + }, "@eslint/eslintrc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", - "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.1.tgz", + "integrity": "sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.4.0", + "espree": "^9.5.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -15662,6 +15736,12 @@ } } }, + "@eslint/js": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.36.0.tgz", + "integrity": "sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==", + "dev": true + }, "@ethereumjs/common": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-2.5.0.tgz", @@ -16443,90 +16523,90 @@ } }, "@nomicfoundation/solidity-analyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.0.tgz", - "integrity": "sha512-xGWAiVCGOycvGiP/qrlf9f9eOn7fpNbyJygcB0P21a1MDuVPlKt0Srp7rvtBEutYQ48ouYnRXm33zlRnlTOPHg==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.1.tgz", + "integrity": "sha512-1LMtXj1puAxyFusBgUIy5pZk3073cNXYnXUpuNKFghHbIit/xZgbk0AokpUADbNm3gyD6bFWl3LRFh3dhVdREg==", "dev": true, "requires": { - "@nomicfoundation/solidity-analyzer-darwin-arm64": "0.1.0", - "@nomicfoundation/solidity-analyzer-darwin-x64": "0.1.0", - "@nomicfoundation/solidity-analyzer-freebsd-x64": "0.1.0", - "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": "0.1.0", - "@nomicfoundation/solidity-analyzer-linux-arm64-musl": "0.1.0", - "@nomicfoundation/solidity-analyzer-linux-x64-gnu": "0.1.0", - "@nomicfoundation/solidity-analyzer-linux-x64-musl": "0.1.0", - "@nomicfoundation/solidity-analyzer-win32-arm64-msvc": "0.1.0", - "@nomicfoundation/solidity-analyzer-win32-ia32-msvc": "0.1.0", - "@nomicfoundation/solidity-analyzer-win32-x64-msvc": "0.1.0" + "@nomicfoundation/solidity-analyzer-darwin-arm64": "0.1.1", + "@nomicfoundation/solidity-analyzer-darwin-x64": "0.1.1", + "@nomicfoundation/solidity-analyzer-freebsd-x64": "0.1.1", + "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": "0.1.1", + "@nomicfoundation/solidity-analyzer-linux-arm64-musl": "0.1.1", + "@nomicfoundation/solidity-analyzer-linux-x64-gnu": "0.1.1", + "@nomicfoundation/solidity-analyzer-linux-x64-musl": "0.1.1", + "@nomicfoundation/solidity-analyzer-win32-arm64-msvc": "0.1.1", + "@nomicfoundation/solidity-analyzer-win32-ia32-msvc": "0.1.1", + "@nomicfoundation/solidity-analyzer-win32-x64-msvc": "0.1.1" } }, "@nomicfoundation/solidity-analyzer-darwin-arm64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.0.tgz", - "integrity": "sha512-vEF3yKuuzfMHsZecHQcnkUrqm8mnTWfJeEVFHpg+cO+le96xQA4lAJYdUan8pXZohQxv1fSReQsn4QGNuBNuCw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.1.tgz", + "integrity": "sha512-KcTodaQw8ivDZyF+D76FokN/HdpgGpfjc/gFCImdLUyqB6eSWVaZPazMbeAjmfhx3R0zm/NYVzxwAokFKgrc0w==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-darwin-x64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.1.0.tgz", - "integrity": "sha512-dlHeIg0pTL4dB1l9JDwbi/JG6dHQaU1xpDK+ugYO8eJ1kxx9Dh2isEUtA4d02cQAl22cjOHTvifAk96A+ItEHA==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.1.1.tgz", + "integrity": "sha512-XhQG4BaJE6cIbjAVtzGOGbK3sn1BO9W29uhk9J8y8fZF1DYz0Doj8QDMfpMu+A6TjPDs61lbsmeYodIDnfveSA==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-freebsd-x64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-freebsd-x64/-/solidity-analyzer-freebsd-x64-0.1.0.tgz", - "integrity": "sha512-WFCZYMv86WowDA4GiJKnebMQRt3kCcFqHeIomW6NMyqiKqhK1kIZCxSLDYsxqlx396kKLPN1713Q1S8tu68GKg==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-freebsd-x64/-/solidity-analyzer-freebsd-x64-0.1.1.tgz", + "integrity": "sha512-GHF1VKRdHW3G8CndkwdaeLkVBi5A9u2jwtlS7SLhBc8b5U/GcoL39Q+1CSO3hYqePNP+eV5YI7Zgm0ea6kMHoA==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.1.0.tgz", - "integrity": "sha512-DTw6MNQWWlCgc71Pq7CEhEqkb7fZnS7oly13pujs4cMH1sR0JzNk90Mp1zpSCsCs4oKan2ClhMlLKtNat/XRKQ==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.1.1.tgz", + "integrity": "sha512-g4Cv2fO37ZsUENQ2vwPnZc2zRenHyAxHcyBjKcjaSmmkKrFr64yvzeNO8S3GBFCo90rfochLs99wFVGT/0owpg==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-linux-arm64-musl": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.1.0.tgz", - "integrity": "sha512-wUpUnR/3GV5Da88MhrxXh/lhb9kxh9V3Jya2NpBEhKDIRCDmtXMSqPMXHZmOR9DfCwCvG6vLFPr/+YrPCnUN0w==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.1.1.tgz", + "integrity": "sha512-WJ3CE5Oek25OGE3WwzK7oaopY8xMw9Lhb0mlYuJl/maZVo+WtP36XoQTb7bW/i8aAdHW5Z+BqrHMux23pvxG3w==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-linux-x64-gnu": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.0.tgz", - "integrity": "sha512-lR0AxK1x/MeKQ/3Pt923kPvwigmGX3OxeU5qNtQ9pj9iucgk4PzhbS3ruUeSpYhUxG50jN4RkIGwUMoev5lguw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.1.tgz", + "integrity": "sha512-5WN7leSr5fkUBBjE4f3wKENUy9HQStu7HmWqbtknfXkkil+eNWiBV275IOlpXku7v3uLsXTOKpnnGHJYI2qsdA==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-linux-x64-musl": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.0.tgz", - "integrity": "sha512-A1he/8gy/JeBD3FKvmI6WUJrGrI5uWJNr5Xb9WdV+DK0F8msuOqpEByLlnTdLkXMwW7nSl3awvLezOs9xBHJEg==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.1.tgz", + "integrity": "sha512-KdYMkJOq0SYPQMmErv/63CwGwMm5XHenEna9X9aB8mQmhDBrYrlAOSsIPgFCUSL0hjxE3xHP65/EPXR/InD2+w==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-win32-arm64-msvc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-arm64-msvc/-/solidity-analyzer-win32-arm64-msvc-0.1.0.tgz", - "integrity": "sha512-7x5SXZ9R9H4SluJZZP8XPN+ju7Mx+XeUMWZw7ZAqkdhP5mK19I4vz3x0zIWygmfE8RT7uQ5xMap0/9NPsO+ykw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-arm64-msvc/-/solidity-analyzer-win32-arm64-msvc-0.1.1.tgz", + "integrity": "sha512-VFZASBfl4qiBYwW5xeY20exWhmv6ww9sWu/krWSesv3q5hA0o1JuzmPHR4LPN6SUZj5vcqci0O6JOL8BPw+APg==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-win32-ia32-msvc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-ia32-msvc/-/solidity-analyzer-win32-ia32-msvc-0.1.0.tgz", - "integrity": "sha512-m7w3xf+hnE774YRXu+2mGV7RiF3QJtUoiYU61FascCkQhX3QMQavh7saH/vzb2jN5D24nT/jwvaHYX/MAM9zUw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-ia32-msvc/-/solidity-analyzer-win32-ia32-msvc-0.1.1.tgz", + "integrity": "sha512-JnFkYuyCSA70j6Si6cS1A9Gh1aHTEb8kOTBApp/c7NRTFGNMH8eaInKlyuuiIbvYFhlXW4LicqyYuWNNq9hkpQ==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-win32-x64-msvc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.0.tgz", - "integrity": "sha512-xCuybjY0sLJQnJhupiFAXaek2EqF0AP0eBjgzaalPXSNvCEN6ZYHvUzdA50ENDVeSYFXcUsYf3+FsD3XKaeptA==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.1.tgz", + "integrity": "sha512-HrVJr6+WjIXGnw3Q9u6KQcbZCtk0caVWhCdFADySvRyUxJ8PnzlaP+MhwNE8oyT8OZ6ejHBRrrgjSqDCFXGirw==", "dev": true, "optional": true }, @@ -16694,9 +16774,9 @@ } }, "@openzeppelin/upgrades-core": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.24.0.tgz", - "integrity": "sha512-lXf1tUrCZ3Q/YmWhw0cuSSOHMp0OAsmeOg1fhSGEM6nQQ6cIVlFvq2pCV5hZMb7xkOm5pmmzV8JW1W3kfW6Lfw==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.24.1.tgz", + "integrity": "sha512-QhdIQDUykJ3vQauB6CheV7vk4zgn0e1iY+IDg7r1KqpA1m2bqIGjQCpzidW33K4bZc9zdJSPx2/Z6Um5KxCB7A==", "dev": true, "requires": { "cbor": "^8.0.0", @@ -16892,9 +16972,9 @@ } }, "@truffle/abi-utils": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@truffle/abi-utils/-/abi-utils-0.3.8.tgz", - "integrity": "sha512-RevMtlEuQw9cE2VWzQuvLDSYPCVSZQQUOsaK8ArcwxXct6ugiJCDC7tNouhHzP1Y4ccdZtG2y/XPHQJdzgYdZQ==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@truffle/abi-utils/-/abi-utils-0.3.9.tgz", + "integrity": "sha512-G5dqgwRHx5zwlXjz3QT8OJVfB2cOqWwD6DwKso0KttUt/zejhCjnkKq72rSgyeLMkz7wBB9ERLOsupLBILM8MA==", "dev": true, "requires": { "change-case": "3.0.2", @@ -16909,13 +16989,13 @@ "dev": true }, "@truffle/codec": { - "version": "0.14.15", - "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.14.15.tgz", - "integrity": "sha512-cooWy8blmvYQQRBKgzWJnDUS6mZE9cvnmpVN15jU6TseAQkBtmfYfCH12QCKEWMq2+R4yQICP54WNCYg78g72g==", + "version": "0.14.16", + "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.14.16.tgz", + "integrity": "sha512-a9UY3n/FnkKN3Q4zOuMFOOcLWb80mdknj+voim4vvXYtJm1aAZQZE5sG9aLnMBTl4TiGLzUtfNDVYY7WgWgDag==", "dev": true, "requires": { - "@truffle/abi-utils": "^0.3.8", - "@truffle/compile-common": "^0.9.3", + "@truffle/abi-utils": "^0.3.9", + "@truffle/compile-common": "^0.9.4", "big.js": "^6.0.3", "bn.js": "^5.1.3", "cbor": "^5.2.0", @@ -16981,9 +17061,9 @@ } }, "@truffle/compile-common": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@truffle/compile-common/-/compile-common-0.9.3.tgz", - "integrity": "sha512-9S86H5DRC0zEj164KeClP/6jVt1M/nRd7St89h6QbuIU0JjpqSz1SXpkvqhbFoV9hhW+4ZGh0NysjrdPlk7gFw==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@truffle/compile-common/-/compile-common-0.9.4.tgz", + "integrity": "sha512-mnqJB/hLiPHNf+WKwt/2MH6lv34xSG/SFCib7+ckAklutUqVLeFo8EwQxinuHNkU7LY0C+YgZXhK1WTCO5YRJQ==", "dev": true, "requires": { "@truffle/error": "^0.2.0", @@ -16999,17 +17079,17 @@ } }, "@truffle/contract": { - "version": "4.6.15", - "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.15.tgz", - "integrity": "sha512-3V+50z94XRh+I3axz6Zrot1wlMhRiG2O9XjR20LwR5YBnzHV7YFfOs8x6pluXVLQyxcVJBISJyqxvGxpkecrZg==", + "version": "4.6.17", + "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.17.tgz", + "integrity": "sha512-sIMam5Wqr9AEiqHfOcWGJGqTv8Qy+BT765PaNHUUT6JBAY+tpHM3FlQd2nM6zLJ8paR3SLDGIthkhCBH/KNgDA==", "dev": true, "requires": { "@ensdomains/ensjs": "^2.1.0", "@truffle/blockchain-utils": "^0.1.6", - "@truffle/contract-schema": "^3.4.12", - "@truffle/debug-utils": "^6.0.46", + "@truffle/contract-schema": "^3.4.13", + "@truffle/debug-utils": "^6.0.47", "@truffle/error": "^0.2.0", - "@truffle/interface-adapter": "^0.5.29", + "@truffle/interface-adapter": "^0.5.30", "bignumber.js": "^7.2.1", "debug": "^4.3.1", "ethers": "^4.0.32", @@ -17029,9 +17109,9 @@ } }, "@truffle/contract-schema": { - "version": "3.4.12", - "resolved": "https://registry.npmjs.org/@truffle/contract-schema/-/contract-schema-3.4.12.tgz", - "integrity": "sha512-XpMMps/bqHwiRuCyLiEEGWEAvGGzGj4u1X1+lzxrtIsrwbQhSZcdgEbXl9vGxOOJWOup3HXRCIsjlao27kS4OA==", + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/@truffle/contract-schema/-/contract-schema-3.4.13.tgz", + "integrity": "sha512-emG7upuryYFrsPDbHqeASPWXL824M1tinhQwSPG0phSoa3g+RX9fUNNN/VPmF3tSkXLWUMhRnb7ehxnaCuRbZg==", "dev": true, "requires": { "ajv": "^6.10.0", @@ -17039,12 +17119,12 @@ } }, "@truffle/debug-utils": { - "version": "6.0.46", - "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.46.tgz", - "integrity": "sha512-MWko3c1M0NI8IT0yNh8Dh2xoDgR2R1apP8hc/9VsR/4Q7P6OE/578WRpZnvo2HWXYBiJ2PrWBDtMYBHntImrrA==", + "version": "6.0.47", + "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.47.tgz", + "integrity": "sha512-bUjdzLPdEKtoUCDzaXkrOoi+PbyAJlMBzGequBK8tirT7xL9bCP2Pd/WxvnmRd7AnfroxGNvXwVXWTItW5SMWQ==", "dev": true, "requires": { - "@truffle/codec": "^0.14.15", + "@truffle/codec": "^0.14.16", "@trufflesuite/chromafi": "^3.0.0", "bn.js": "^5.1.3", "chalk": "^2.4.2", @@ -17067,9 +17147,9 @@ "dev": true }, "@truffle/interface-adapter": { - "version": "0.5.29", - "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.29.tgz", - "integrity": "sha512-6UlJ+f87z7y/dWk9UfbIU+4e80iRsp8h03LEiE5B+PvZbr6cuMjLJUBtBBQZMo3+xrIcS/2u3p5hOxW8OJm8tw==", + "version": "0.5.30", + "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.30.tgz", + "integrity": "sha512-wyCcESeZJBkAfuSGU8GHCusIWDUDyQjJZMcyShv59ZTSAwQR7xx0+a0Q1LlS532G/pGFLYe2VzKSY1pRHRwgug==", "dev": true, "requires": { "bn.js": "^5.1.3", @@ -17468,6 +17548,16 @@ "sprintf-js": "~1.0.2" } }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -17748,6 +17838,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } } } }, @@ -18445,9 +18544,9 @@ } }, "cosmiconfig": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.0.0.tgz", - "integrity": "sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.0.tgz", + "integrity": "sha512-0tLZ9URlPGU7JsKq0DQOQ3FoRsYX8xDZ7xMiATQfaiGMz7EHowNkbU9u1coAOmnh9p/1ySpm0RB3JNWRXM5GCg==", "dev": true, "requires": { "import-fresh": "^3.2.1", @@ -18948,18 +19047,18 @@ } }, "es-abstract": { - "version": "1.21.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.1.tgz", - "integrity": "sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==", + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", "dev": true, "requires": { + "array-buffer-byte-length": "^1.0.0", "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", @@ -18967,8 +19066,8 @@ "has-property-descriptors": "^1.0.0", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.4", - "is-array-buffer": "^3.0.1", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", @@ -18976,11 +19075,12 @@ "is-string": "^1.0.7", "is-typed-array": "^1.1.10", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.4.3", "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", "string.prototype.trimend": "^1.0.6", "string.prototype.trimstart": "^1.0.6", "typed-array-length": "^1.0.4", @@ -19148,12 +19248,15 @@ } }, "eslint": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.34.0.tgz", - "integrity": "sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.36.0.tgz", + "integrity": "sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.4.1", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.1", + "@eslint/js": "8.36.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -19164,10 +19267,9 @@ "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", + "espree": "^9.5.0", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", @@ -19188,7 +19290,6 @@ "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.1", - "regexpp": "^3.2.0", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" @@ -19319,9 +19420,9 @@ } }, "eslint-config-prettier": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz", - "integrity": "sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.7.0.tgz", + "integrity": "sha512-HHVXLSlVUhMSmyW4ZzEuvjpwqamgmlfkutD53cYXLikh4pt/modINRcCIApJ84czDxM4GZInwUrromsDdTImTA==", "dev": true, "requires": {} }, @@ -19335,23 +19436,6 @@ "estraverse": "^5.2.0" } }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - } - } - }, "eslint-visitor-keys": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", @@ -19359,9 +19443,9 @@ "dev": true }, "espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.0.tgz", + "integrity": "sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw==", "dev": true, "requires": { "acorn": "^8.8.0", @@ -19376,9 +19460,9 @@ "dev": true }, "esquery": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz", - "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -20204,6 +20288,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, "raw-body": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", @@ -20816,9 +20909,9 @@ "dev": true }, "hardhat": { - "version": "2.12.7", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.12.7.tgz", - "integrity": "sha512-voWoN6zn5d8BOEaczSyK/1PyfdeOeI3SbGCFb36yCHTJUt6OIqLb+ZDX30VhA1UsYKzLqG7UnWl3fKJUuANc6A==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.13.0.tgz", + "integrity": "sha512-ZlzBOLML1QGlm6JWyVAG8lVTEAoOaVm1in/RU2zoGAnYEoD1Rp4T+ZMvrLNhHaaeS9hfjJ1gJUBfiDr4cx+htQ==", "dev": true, "requires": { "@ethersproject/abi": "^5.1.2", @@ -21347,9 +21440,9 @@ "dev": true }, "immutable": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.4.tgz", - "integrity": "sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", + "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", "dev": true }, "import-fresh": { @@ -21453,13 +21546,13 @@ } }, "is-array-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.1.tgz", - "integrity": "sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", "dev": true, "requires": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "is-typed-array": "^1.1.10" } }, @@ -23337,20 +23430,20 @@ "dev": true }, "prettier-plugin-solidity": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.2.tgz", - "integrity": "sha512-KC5oNbFJfyBaFiO0kl56J6AXnDmr9tUlBV1iqo864x4KQrKYKaBZvW9jhT2oC0NHoNp7/GoMJNxqL8pp8k7C/g==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.3.tgz", + "integrity": "sha512-fQ9yucPi2sBbA2U2Xjh6m4isUTJ7S7QLc/XDDsktqqxYfTwdYKJ0EnnywXHwCGAaYbQNK+HIYPL1OemxuMsgeg==", "dev": true, "requires": { - "@solidity-parser/parser": "^0.15.0", + "@solidity-parser/parser": "^0.16.0", "semver": "^7.3.8", "solidity-comments-extractor": "^0.0.7" }, "dependencies": { "@solidity-parser/parser": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.15.0.tgz", - "integrity": "sha512-5UFJJTzWi1hgFk6aGCZ5rxG2DJkCJOzJ74qg7UkWSNCDSigW+CJLoYUb5bLiKrtI34Nr9rpFSUNHfkqtlL+N/w==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.0.tgz", + "integrity": "sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q==", "dev": true, "requires": { "antlr4ts": "^0.5.0-alpha.4" @@ -23435,9 +23528,9 @@ "dev": true }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", + "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", "dev": true, "requires": { "side-channel": "^1.0.4" @@ -23545,9 +23638,9 @@ } }, "readable-stream": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.1.tgz", - "integrity": "sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "requires": { "inherits": "^2.0.3", @@ -23620,12 +23713,6 @@ "functions-have-names": "^1.2.2" } }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, "req-cwd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/req-cwd/-/req-cwd-2.0.0.tgz", @@ -24670,12 +24757,12 @@ } }, "solhint": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/solhint/-/solhint-3.4.0.tgz", - "integrity": "sha512-FYEs/LoTxMsWFP/OGsEqR1CBDn3Bn7hrTWsgtjai17MzxITgearIdlo374KKZjjIycu8E2xBcJ+RSWeoBvQmkw==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/solhint/-/solhint-3.4.1.tgz", + "integrity": "sha512-pzZn2RlZhws1XwvLPVSsxfHrwsteFf5eySOhpAytzXwKQYbTCJV6z8EevYDiSVKMpWrvbKpEtJ055CuEmzp4Xg==", "dev": true, "requires": { - "@solidity-parser/parser": "^0.15.0", + "@solidity-parser/parser": "^0.16.0", "ajv": "^6.12.6", "antlr4": "^4.11.0", "ast-parents": "^0.0.1", @@ -24696,9 +24783,9 @@ }, "dependencies": { "@solidity-parser/parser": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.15.0.tgz", - "integrity": "sha512-5UFJJTzWi1hgFk6aGCZ5rxG2DJkCJOzJ74qg7UkWSNCDSigW+CJLoYUb5bLiKrtI34Nr9rpFSUNHfkqtlL+N/w==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.0.tgz", + "integrity": "sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q==", "dev": true, "requires": { "antlr4ts": "^0.5.0-alpha.4" @@ -25300,9 +25387,9 @@ } }, "solidity-docgen": { - "version": "0.6.0-beta.34", - "resolved": "https://registry.npmjs.org/solidity-docgen/-/solidity-docgen-0.6.0-beta.34.tgz", - "integrity": "sha512-igdGrkg8gT1jn+B2NwzjEtSf+7NTrSi/jz88zO7MZWgETmcWbXaxgAsQP4BQeC4YFeH0Pie1NsLP7+9qDgvFtA==", + "version": "0.6.0-beta.35", + "resolved": "https://registry.npmjs.org/solidity-docgen/-/solidity-docgen-0.6.0-beta.35.tgz", + "integrity": "sha512-9QdwK1THk/MWIdq1PEW/6dvtND0pUqpFTsbKwwU9YQIMYuRhH1lek9SsgnsGGYtdJ0VTrXXcVT30q20a8Y610A==", "dev": true, "requires": { "handlebars": "^4.7.7", @@ -25401,9 +25488,9 @@ } }, "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", @@ -25427,9 +25514,9 @@ } }, "spdx-license-ids": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", "dev": true }, "sprintf-js": { @@ -25532,6 +25619,17 @@ "strip-ansi": "^4.0.0" } }, + "string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, "string.prototype.trimend": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", @@ -26139,9 +26237,9 @@ "dev": true }, "undici": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.20.0.tgz", - "integrity": "sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.21.0.tgz", + "integrity": "sha512-HOjK8l6a57b2ZGXOcUsI5NLfoTrfmbOl90ixJDl0AEFG4wgHNDQxtZy15/ZQp7HhjkpaGlp/eneMgtsu1dIlUA==", "dev": true, "requires": { "busboy": "^1.6.0" From 75ef7b8b27cf8d5ad8874d23754b0ff0dc256794 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 14 Mar 2023 14:27:37 +0100 Subject: [PATCH 083/197] Add FV specs for TimelockController (#4098) Co-authored-by: Francisco --- .../harnesses/TimelockControllerHarness.sol | 12 + certora/specs.json | 6 + certora/specs/AccessControl.spec | 9 +- certora/specs/TimelockController.spec | 275 ++++++++++++++++++ certora/specs/methods/IAccessControl.spec | 7 + 5 files changed, 301 insertions(+), 8 deletions(-) create mode 100644 certora/harnesses/TimelockControllerHarness.sol create mode 100644 certora/specs/TimelockController.spec create mode 100644 certora/specs/methods/IAccessControl.spec diff --git a/certora/harnesses/TimelockControllerHarness.sol b/certora/harnesses/TimelockControllerHarness.sol new file mode 100644 index 000000000..d1ea99065 --- /dev/null +++ b/certora/harnesses/TimelockControllerHarness.sol @@ -0,0 +1,12 @@ +pragma solidity ^0.8.0; + +import "../patched/governance/TimelockController.sol"; + +contract TimelockControllerHarness is TimelockController { + constructor( + uint256 minDelay, + address[] memory proposers, + address[] memory executors, + address admin + ) TimelockController(minDelay, proposers, executors, admin) {} +} diff --git a/certora/specs.json b/certora/specs.json index 228e85fe4..e76db3084 100644 --- a/certora/specs.json +++ b/certora/specs.json @@ -45,5 +45,11 @@ "spec": "Initializable", "contract": "InitializableHarness", "files": ["certora/harnesses/InitializableHarness.sol"] + }, + { + "spec": "TimelockController", + "contract": "TimelockControllerHarness", + "files": ["certora/harnesses/TimelockControllerHarness.sol"], + "options": ["--optimistic_hashing", "--optimistic_loop"] } ] diff --git a/certora/specs/AccessControl.spec b/certora/specs/AccessControl.spec index 8e8f6ac9d..35927c5d3 100644 --- a/certora/specs/AccessControl.spec +++ b/certora/specs/AccessControl.spec @@ -1,12 +1,5 @@ import "helpers.spec" - -methods { - hasRole(bytes32, address) returns(bool) envfree - getRoleAdmin(bytes32) returns(bytes32) envfree - grantRole(bytes32, address) - revokeRole(bytes32, address) - renounceRole(bytes32, address) -} +import "methods/IAccessControl.spec" /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ diff --git a/certora/specs/TimelockController.spec b/certora/specs/TimelockController.spec new file mode 100644 index 000000000..e140c11de --- /dev/null +++ b/certora/specs/TimelockController.spec @@ -0,0 +1,275 @@ +import "helpers.spec" +import "methods/IAccessControl.spec" + +methods { + TIMELOCK_ADMIN_ROLE() returns (bytes32) envfree + PROPOSER_ROLE() returns (bytes32) envfree + EXECUTOR_ROLE() returns (bytes32) envfree + CANCELLER_ROLE() returns (bytes32) envfree + isOperation(bytes32) returns (bool) envfree + isOperationPending(bytes32) returns (bool) envfree + isOperationReady(bytes32) returns (bool) + isOperationDone(bytes32) returns (bool) envfree + getTimestamp(bytes32) returns (uint256) envfree + getMinDelay() returns (uint256) envfree + + hashOperation(address, uint256, bytes, bytes32, bytes32) returns(bytes32) envfree + hashOperationBatch(address[], uint256[], bytes[], bytes32, bytes32) returns(bytes32) envfree + + schedule(address, uint256, bytes, bytes32, bytes32, uint256) + scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256) + execute(address, uint256, bytes, bytes32, bytes32) + executeBatch(address[], uint256[], bytes[], bytes32, bytes32) + cancel(bytes32) + + updateDelay(uint256) +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Helpers │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +// Uniformly handle scheduling of batched and non-batched operations. +function helperScheduleWithRevert(env e, method f, bytes32 id, uint256 delay) { + if (f.selector == schedule(address, uint256, bytes, bytes32, bytes32, uint256).selector) { + address target; uint256 value; bytes data; bytes32 predecessor; bytes32 salt; + require hashOperation(target, value, data, predecessor, salt) == id; // Correlation + schedule@withrevert(e, target, value, data, predecessor, salt, delay); + } else if (f.selector == scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256).selector) { + address[] targets; uint256[] values; bytes[] payloads; bytes32 predecessor; bytes32 salt; + require hashOperationBatch(targets, values, payloads, predecessor, salt) == id; // Correlation + scheduleBatch@withrevert(e, targets, values, payloads, predecessor, salt, delay); + } else { + calldataarg args; + f@withrevert(e, args); + } +} + +// Uniformly handle execution of batched and non-batched operations. +function helperExecuteWithRevert(env e, method f, bytes32 id, bytes32 predecessor) { + if (f.selector == execute(address, uint256, bytes, bytes32, bytes32).selector) { + address target; uint256 value; bytes data; bytes32 salt; + require hashOperation(target, value, data, predecessor, salt) == id; // Correlation + execute@withrevert(e, target, value, data, predecessor, salt); + } else if (f.selector == executeBatch(address[], uint256[], bytes[], bytes32, bytes32).selector) { + address[] targets; uint256[] values; bytes[] payloads; bytes32 salt; + require hashOperationBatch(targets, values, payloads, predecessor, salt) == id; // Correlation + executeBatch@withrevert(e, targets, values, payloads, predecessor, salt); + } else { + calldataarg args; + f@withrevert(e, args); + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Definitions │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +definition DONE_TIMESTAMP() returns uint256 = 1; +definition UNSET() returns uint8 = 0x1; +definition PENDING() returns uint8 = 0x2; +definition DONE() returns uint8 = 0x4; + +definition isUnset(bytes32 id) returns bool = !isOperation(id); +definition isPending(bytes32 id) returns bool = isOperationPending(id); +definition isDone(bytes32 id) returns bool = isOperationDone(id); +definition state(bytes32 id) returns uint8 = (isUnset(id) ? UNSET() : 0) | (isPending(id) ? PENDING() : 0) | (isDone(id) ? DONE() : 0); + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariants: consistency of accessors │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant isOperationCheck(bytes32 id) + isOperation(id) <=> getTimestamp(id) > 0 + filtered { f -> !f.isView } + +invariant isOperationPendingCheck(bytes32 id) + isOperationPending(id) <=> getTimestamp(id) > DONE_TIMESTAMP() + filtered { f -> !f.isView } + +invariant isOperationDoneCheck(bytes32 id) + isOperationDone(id) <=> getTimestamp(id) == DONE_TIMESTAMP() + filtered { f -> !f.isView } + +invariant isOperationReadyCheck(env e, bytes32 id) + isOperationReady(e, id) <=> (isOperationPending(id) && getTimestamp(id) <= e.block.timestamp) + filtered { f -> !f.isView } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: a proposal id is either unset, pending or done │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant stateConsistency(bytes32 id, env e) + // Check states are mutually exclusive + (isUnset(id) <=> (!isPending(id) && !isDone(id) )) && + (isPending(id) <=> (!isUnset(id) && !isDone(id) )) && + (isDone(id) <=> (!isUnset(id) && !isPending(id))) && + // Check that the state helper behaves as expected: + (isUnset(id) <=> state(id) == UNSET() ) && + (isPending(id) <=> state(id) == PENDING() ) && + (isDone(id) <=> state(id) == DONE() ) && + // Check substate + isOperationReady(e, id) => isPending(id) + filtered { f -> !f.isView } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: state transition rules │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule stateTransition(bytes32 id, env e, method f, calldataarg args) { + require e.block.timestamp > 1; // Sanity + + uint8 stateBefore = state(id); + f(e, args); + uint8 stateAfter = state(id); + + // Cannot jump from UNSET to DONE + assert stateBefore == UNSET() => stateAfter != DONE(); + + // UNSET → PENDING: schedule or scheduleBatch + assert stateBefore == UNSET() && stateAfter == PENDING() => ( + f.selector == schedule(address, uint256, bytes, bytes32, bytes32, uint256).selector || + f.selector == scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256).selector + ); + + // PENDING → UNSET: cancel + assert stateBefore == PENDING() && stateAfter == UNSET() => ( + f.selector == cancel(bytes32).selector + ); + + // PENDING → DONE: execute or executeBatch + assert stateBefore == PENDING() && stateAfter == DONE() => ( + f.selector == execute(address, uint256, bytes, bytes32, bytes32).selector || + f.selector == executeBatch(address[], uint256[], bytes[], bytes32, bytes32).selector + ); + + // DONE is final + assert stateBefore == DONE() => stateAfter == DONE(); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: minimum delay can only be updated through a timelock execution │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule minDelayOnlyChange(env e) { + uint256 delayBefore = getMinDelay(); + + method f; calldataarg args; + f(e, args); + + assert delayBefore != getMinDelay() => (e.msg.sender == currentContract && f.selector == updateDelay(uint256).selector), "Unauthorized delay update"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: schedule liveness and effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule schedule(env e, method f, bytes32 id, uint256 delay) filtered { f -> + f.selector == schedule(address, uint256, bytes, bytes32, bytes32, uint256).selector || + f.selector == scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256).selector +} { + require nonpayable(e); + + // Basic timestamp assumptions + require e.block.timestamp > 1; + require e.block.timestamp + delay < max_uint256; + require e.block.timestamp + getMinDelay() < max_uint256; + + bytes32 otherId; uint256 otherTimestamp = getTimestamp(otherId); + + uint8 stateBefore = state(id); + bool isDelaySufficient = delay >= getMinDelay(); + bool isProposerBefore = hasRole(PROPOSER_ROLE(), e.msg.sender); + + helperScheduleWithRevert(e, f, id, delay); + bool success = !lastReverted; + + // liveness + assert success <=> ( + stateBefore == UNSET() && + isDelaySufficient && + isProposerBefore + ); + + // effect + assert success => state(id) == PENDING(), "State transition violation"; + assert success => getTimestamp(id) == to_uint256(e.block.timestamp + delay), "Proposal timestamp not correctly set"; + + // no side effect + assert otherTimestamp != getTimestamp(otherId) => id == otherId, "Other proposal affected"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: execute liveness and effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule execute(env e, method f, bytes32 id, bytes32 predecessor) filtered { f -> + f.selector == execute(address, uint256, bytes, bytes32, bytes32).selector || + f.selector == executeBatch(address[], uint256[], bytes[], bytes32, bytes32).selector +} { + bytes32 otherId; uint256 otherTimestamp = getTimestamp(otherId); + + uint8 stateBefore = state(id); + bool isOperationReadyBefore = isOperationReady(e, id); + bool isExecutorOrOpen = hasRole(EXECUTOR_ROLE(), e.msg.sender) || hasRole(EXECUTOR_ROLE(), 0); + bool predecessorDependency = predecessor == 0 || isDone(predecessor); + + helperExecuteWithRevert(e, f, id, predecessor); + bool success = !lastReverted; + + // The underlying transaction can revert, and that would cause the execution to revert. We can check that all non + // reverting calls meet the requirements in terms of proposal readiness, access control and predecessor dependency. + // We can't however guarantee that these requirements being meet ensure liveness of the proposal, because the + // proposal can revert for reasons beyond our control. + + // liveness, should be `<=>` but can only check `=>` (see comment above) + assert success => ( + stateBefore == PENDING() && + isOperationReadyBefore && + predecessorDependency && + isExecutorOrOpen + ); + + // effect + assert success => state(id) == DONE(), "State transition violation"; + + // no side effect + assert otherTimestamp != getTimestamp(otherId) => id == otherId, "Other proposal affected"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: cancel liveness and effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule cancel(env e, bytes32 id) { + require nonpayable(e); + + bytes32 otherId; uint256 otherTimestamp = getTimestamp(otherId); + + uint8 stateBefore = state(id); + bool isCanceller = hasRole(CANCELLER_ROLE(), e.msg.sender); + + cancel@withrevert(e, id); + bool success = !lastReverted; + + // liveness + assert success <=> ( + stateBefore == PENDING() && + isCanceller + ); + + // effect + assert success => state(id) == UNSET(), "State transition violation"; + + // no side effect + assert otherTimestamp != getTimestamp(otherId) => id == otherId, "Other proposal affected"; +} diff --git a/certora/specs/methods/IAccessControl.spec b/certora/specs/methods/IAccessControl.spec new file mode 100644 index 000000000..4d41ffda7 --- /dev/null +++ b/certora/specs/methods/IAccessControl.spec @@ -0,0 +1,7 @@ +methods { + hasRole(bytes32, address) returns(bool) envfree + getRoleAdmin(bytes32) returns(bytes32) envfree + grantRole(bytes32, address) + revokeRole(bytes32, address) + renounceRole(bytes32, address) +} From 1c8df659b98177b737fd8af411b30bf24c1cbef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 14 Mar 2023 15:14:01 -0700 Subject: [PATCH 084/197] Clarify Governor Bravo compatibility scope (#4090) Co-authored-by: Francisco --- docs/modules/ROOT/pages/governance.adoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index cd7dd4e6f..c5bcf58cd 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -20,7 +20,9 @@ The ERC20 extension to keep track of votes and vote delegation is one such case. === Governor & GovernorCompatibilityBravo -An OpenZeppelin Governor contract is by default not interface-compatible with GovernorAlpha or Bravo, since some of the functions are different or missing, although it shares all of the same events. However, it’s possible to opt in to full compatibility by inheriting from the GovernorCompatibilityBravo module. The contract will be cheaper to deploy and use without this module. +An OpenZeppelin Governor contract is by default not interface-compatible with Compound's GovernorAlpha or Bravo. Even though events are fully compatible, proposal lifecycle functions (creation, execution, etc.) have different signatures that are meant to optimize storage use. Other functions from GovernorAlpha are Bravo are likewise not available. It’s possible to opt in to a higher level of compatibility by inheriting from the GovernorCompatibilityBravo module, which covers the proposal lifecycle functions such as `propose` and `execute`. + +Note that even with the use of this module, there will still be differences in the way that `proposalId`s are calculated. Governor uses the hash of the proposal parameters with the purpose of keeping its data off-chain by event indexing, while the original Bravo implementation uses sequential `proposalId`s. Due to this and other differences, several of the functions from GovernorBravo are not included in the compatibility module. === GovernorTimelockControl & GovernorTimelockCompound From 4f4b6ab40315e2fbfc06e65d78f06c5b26d4646c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 15 Mar 2023 08:06:25 -0600 Subject: [PATCH 085/197] Update certora/README.md (#4114) Co-authored-by: Francisco --- certora/README.md | 76 +++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/certora/README.md b/certora/README.md index 55f84d42f..cd85ba3d4 100644 --- a/certora/README.md +++ b/certora/README.md @@ -1,56 +1,60 @@ # Running the certora verification tool -These instructions detail the process for running CVT on the OpenZeppelin (Wizard/Governor) contracts. +These instructions detail the process for running Certora Verification Tool on OpenZeppelin Contracts. -Documentation for CVT and the specification language are available -[here](https://certora.atlassian.net/wiki/spaces/CPD/overview) +Documentation for CVT and the specification language are available [here](https://certora.atlassian.net/wiki/spaces/CPD/overview). + +## Prerequisites + +Follow the [Certora installation guide](https://docs.certora.com/en/latest/docs/user-guide/getting-started/install.html) in order to get the Certora Prover Package and the `solc` executable folder in your path. + +> **Note** +> An API Key is required for local testing. Although the prover will run on a Github Actions' CI environment on selected Pull Requests. ## Running the verification -The scripts in the `certora/scripts` directory are used to submit verification -jobs to the Certora verification service. After the job is complete, the results will be available on -[the Certora portal](https://vaas-stg.certora.com/). +The Certora Verification Tool proves specs for contracts, which are defined by the `./specs.json` file along with their pre-configured options. -These scripts should be run from the root directory; for example by running +The verification script `./run.js` is used to submit verification jobs to the Certora Verification service. -``` -sh certora/scripts/verifyAll.sh +You can run it from the root of the repository with the following command: + +```bash +node certora/run.js [[CONTRACT_NAME:]SPEC_NAME] [OPTIONS...] ``` -The most important of these is `verifyAll.sh`, which checks -all of the harnessed contracts (`certora/harness/Wizard*.sol`) against all of -the specifications (`certora/spec/*.spec`). +Where: -The other scripts run a subset of the specifications or the contracts. You can -verify different contracts or specifications by changing the `--verify` option, -and you can run a single rule or method with the `--rule` or `--method` option. +- `CONTRACT_NAME` matches the `contract` key in the `./spec.json` file and may be empty. It will run all matching contracts if not provided. +- `SPEC_NAME` refers to a `spec` key from the `./specs.json` file. It will run every spec if not provided. +- `OPTIONS` extend the [Certora Prover CLI options](https://docs.certora.com/en/latest/docs/prover/cli/options.html#certora-prover-cli-options) and will respect the preconfigured options in the `specs.json` file. -For example, to verify the `WizardFirstPriority` contract against the -`GovernorCountingSimple` specification, you could change the `--verify` line of -the `WizardControlFirstPriortity.sh` script to: +> **Note** +> A single spec may be configured to run for multiple contracts, whereas a single contract may run multiple specs. -``` ---verify WizardFirstPriority:certora/specs/GovernorCountingSimple.spec \ +Example usage: + +```bash +node certora/run.js AccessControl # Run the AccessControl spec against every contract implementing it ``` ## Adapting to changes in the contracts -Some of our rules require the code to be simplified in various ways. Our -primary tool for performing these simplifications is to run verification on a -contract that extends the original contracts and overrides some of the methods. -These "harness" contracts can be found in the `certora/harness` directory. +Some of our rules require the code to be simplified in various ways. Our primary tool for performing these simplifications is to run verification on a contract that extends the original contracts and overrides some of the methods. These "harness" contracts can be found in the `certora/harness` directory. -This pattern does require some modifications to the original code: some methods -need to be made virtual or public, for example. These changes are handled by -applying a patch to the code before verification. +This pattern does require some modifications to the original code: some methods need to be made virtual or public, for example. These changes are handled by applying a patch +to the code before verification by running: -When one of the `verify` scripts is executed, it first applies the patch file -`certora/applyHarness.patch` to the `contracts` directory, placing the output -in the `certora/munged` directory. We then verify the contracts in the -`certora/munged` directory. +```bash +make -C certora apply +``` -If the original contracts change, it is possible to create a conflict with the -patch. In this case, the verify scripts will report an error message and output -rejected changes in the `munged` directory. After merging the changes, run -`make record` in the `certora` directory; this will regenerate the patch file, -which can then be checked into git. +Before running the `certora/run.js` script, it's required to apply the corresponding patches to the `contracts` directory, placing the output in the `certora/patched` directory. Then, the contracts are verified by running the verification for the `certora/patched` directory. + +If the original contracts change, it is possible to create a conflict with the patch. In this case, the verify scripts will report an error message and output rejected changes in the `patched` directory. After merging the changes, run `make record` in the `certora` directory; this will regenerate the patch file, which can then be checked into git. + +For more information about the `make` scripts available, run: + +```bash +make -C certora help +``` From 1a60b061d5bb809c3d7e4ee915c77a00b1eca95d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 16 Mar 2023 13:08:28 -0600 Subject: [PATCH 086/197] Add Pausable FV (#4117) Co-authored-by: Hadrien Croubois --- .gitignore | 1 + certora/harnesses/PausableHarness.sol | 19 ++++++ certora/specs.json | 5 ++ certora/specs/Pausable.spec | 96 +++++++++++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 certora/harnesses/PausableHarness.sol create mode 100644 certora/specs/Pausable.spec diff --git a/.gitignore b/.gitignore index 9350b262e..2aac74485 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,4 @@ contracts-exposed .certora* .last_confs certora_* +.zip-output-url.txt diff --git a/certora/harnesses/PausableHarness.sol b/certora/harnesses/PausableHarness.sol new file mode 100644 index 000000000..37c5d591d --- /dev/null +++ b/certora/harnesses/PausableHarness.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../patched/security/Pausable.sol"; + +contract PausableHarness is Pausable { + function pause() external { + _pause(); + } + + function unpause() external { + _unpause(); + } + + function onlyWhenPaused() external whenPaused {} + + function onlyWhenNotPaused() external whenNotPaused {} +} diff --git a/certora/specs.json b/certora/specs.json index e76db3084..80fbf26af 100644 --- a/certora/specs.json +++ b/certora/specs.json @@ -1,4 +1,9 @@ [ + { + "spec": "Pausable", + "contract": "PausableHarness", + "files": ["certora/harnesses/PausableHarness.sol"] + }, { "spec": "AccessControl", "contract": "AccessControlHarness", diff --git a/certora/specs/Pausable.spec b/certora/specs/Pausable.spec new file mode 100644 index 000000000..e49293ffc --- /dev/null +++ b/certora/specs/Pausable.spec @@ -0,0 +1,96 @@ +import "helpers.spec" + +methods { + paused() returns (bool) envfree + pause() + unpause() + onlyWhenPaused() + onlyWhenNotPaused() +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: _pause pauses the contract │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pause(env e) { + require nonpayable(e); + + bool pausedBefore = paused(); + + pause@withrevert(e); + bool success = !lastReverted; + + bool pausedAfter = paused(); + + // liveness + assert success <=> !pausedBefore, "works if and only if the contract was not paused before"; + + // effect + assert success => pausedAfter, "contract must be paused after a successful call"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: _unpause unpauses the contract │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule unpause(env e) { + require nonpayable(e); + + bool pausedBefore = paused(); + + unpause@withrevert(e); + bool success = !lastReverted; + + bool pausedAfter = paused(); + + // liveness + assert success <=> pausedBefore, "works if and only if the contract was paused before"; + + // effect + assert success => !pausedAfter, "contract must be unpaused after a successful call"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: whenPaused modifier can only be called if the contract is paused │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule whenPaused(env e) { + require nonpayable(e); + + onlyWhenPaused@withrevert(e); + assert !lastReverted <=> paused(), "works if and only if the contract is paused"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: whenNotPaused modifier can only be called if the contract is not paused │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule whenNotPaused(env e) { + require nonpayable(e); + + onlyWhenNotPaused@withrevert(e); + assert !lastReverted <=> !paused(), "works if and only if the contract is not paused"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: only _pause and _unpause can change paused status │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noPauseChange(env e) { + method f; + calldataarg args; + + bool pausedBefore = paused(); + f(e, args); + bool pausedAfter = paused(); + + assert pausedBefore != pausedAfter => ( + (!pausedAfter && f.selector == unpause().selector) || + (pausedAfter && f.selector == pause().selector) + ), "contract's paused status can only be changed by _pause() or _unpause()"; +} From 3f610ebc25480bf6145e519c96e2f809996db8ed Mon Sep 17 00:00:00 2001 From: mmqxyz <127844428+mmqxyz@users.noreply.github.com> Date: Tue, 21 Mar 2023 15:33:16 +0100 Subject: [PATCH 087/197] Fix typo in README (#4129) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 094637ed6..9fc95518a 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ The engineering guidelines we follow to promote project quality can be found in Past audits can be found in [`audits/`](./audits). -Smart contracts are a nascent techology and carry a high level of technical risk and uncertainty. Although OpenZeppelin is well known for its security audits, using OpenZeppelin Contracts is not a substitute for a security audit. +Smart contracts are a nascent technology and carry a high level of technical risk and uncertainty. Although OpenZeppelin is well known for its security audits, using OpenZeppelin Contracts is not a substitute for a security audit. OpenZeppelin Contracts is made available under the MIT License, which disclaims all warranties in relation to the project and which limits the liability of those that contribute and maintain the project, including OpenZeppelin. As set out further in the Terms, you acknowledge that you are solely responsible for any use of OpenZeppelin Contracts and you assume all risks associated with any such use. From ca822213f2275a14c26167bd387ac3522da67fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Sun, 26 Mar 2023 11:23:13 -0600 Subject: [PATCH 088/197] Make AccessControlDefaultAdminRules delay configurable (#4079) Co-authored-by: Francisco Co-authored-by: Hadrien Croubois --- .../access/AccessControlDefaultAdminRules.sol | 409 +++++++---- .../IAccessControlDefaultAdminRules.sol | 139 +++- test/access/AccessControl.behavior.js | 651 ++++++++++++++---- .../AccessControlDefaultAdminRules.test.js | 2 +- .../SupportsInterface.behavior.js | 5 +- 5 files changed, 901 insertions(+), 305 deletions(-) diff --git a/contracts/access/AccessControlDefaultAdminRules.sol b/contracts/access/AccessControlDefaultAdminRules.sol index e3bce6b75..43fca9350 100644 --- a/contracts/access/AccessControlDefaultAdminRules.sol +++ b/contracts/access/AccessControlDefaultAdminRules.sol @@ -19,9 +19,9 @@ import "../interfaces/IERC5313.sol"; * This contract implements the following risk mitigations on top of {AccessControl}: * * * Only one account holds the `DEFAULT_ADMIN_ROLE` since deployment until it's potentially renounced. - * * Enforce a 2-step process to transfer the `DEFAULT_ADMIN_ROLE` to another account. - * * Enforce a configurable delay between the two steps, with the ability to cancel in between. - * - Even after the timer has passed to avoid locking it forever. + * * Enforces a 2-step process to transfer the `DEFAULT_ADMIN_ROLE` to another account. + * * Enforces a configurable delay between the two steps, with the ability to cancel before the transfer is accepted. + * * The delay can be changed by scheduling, see {changeDefaultAdminDelay}. * * It is not possible to use another role to manage the `DEFAULT_ADMIN_ROLE`. * * Example usage: @@ -32,66 +32,31 @@ import "../interfaces/IERC5313.sol"; * 3 days, * msg.sender // Explicit initial `DEFAULT_ADMIN_ROLE` holder * ) {} - *} + * } * ``` * - * NOTE: The `delay` can only be set in the constructor and is fixed thereafter. - * * _Available since v4.9._ */ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRules, IERC5313, AccessControl { - uint48 private immutable _defaultAdminDelay; - - address private _currentDefaultAdmin; + // pending admin pair read/written together frequently address private _pendingDefaultAdmin; + uint48 private _pendingDefaultAdminSchedule; // 0 == unset - uint48 private _defaultAdminTransferDelayedUntil; + uint48 private _currentDelay; + address private _currentDefaultAdmin; + + // pending delay pair read/written together frequently + uint48 private _pendingDelay; + uint48 private _pendingDelaySchedule; // 0 == unset /** - * @dev Sets the initial values for {defaultAdminDelay} in seconds and {defaultAdmin}. - * - * The `defaultAdminDelay` value is immutable. It can only be set at the constructor. + * @dev Sets the initial values for {defaultAdminDelay} and {defaultAdmin} address. */ - constructor(uint48 defaultAdminDelay_, address initialDefaultAdmin) { - _defaultAdminDelay = defaultAdminDelay_; + constructor(uint48 initialDelay, address initialDefaultAdmin) { + _currentDelay = initialDelay; _grantRole(DEFAULT_ADMIN_ROLE, initialDefaultAdmin); } - /** - * @dev See {IERC5313-owner}. - */ - function owner() public view virtual returns (address) { - return defaultAdmin(); - } - - /** - * @inheritdoc IAccessControlDefaultAdminRules - */ - function defaultAdminDelay() public view virtual returns (uint48) { - return _defaultAdminDelay; - } - - /** - * @inheritdoc IAccessControlDefaultAdminRules - */ - function defaultAdmin() public view virtual returns (address) { - return _currentDefaultAdmin; - } - - /** - * @inheritdoc IAccessControlDefaultAdminRules - */ - function pendingDefaultAdmin() public view virtual returns (address) { - return _pendingDefaultAdmin; - } - - /** - * @inheritdoc IAccessControlDefaultAdminRules - */ - function defaultAdminTransferDelayedUntil() public view virtual returns (uint48) { - return _defaultAdminTransferDelayedUntil; - } - /** * @dev See {IERC165-supportsInterface}. */ @@ -100,50 +65,15 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu } /** - * @inheritdoc IAccessControlDefaultAdminRules + * @dev See {IERC5313-owner}. */ - function beginDefaultAdminTransfer(address newAdmin) public virtual onlyRole(DEFAULT_ADMIN_ROLE) { - _beginDefaultAdminTransfer(newAdmin); + function owner() public view virtual returns (address) { + return defaultAdmin(); } - /** - * @inheritdoc IAccessControlDefaultAdminRules - */ - function acceptDefaultAdminTransfer() public virtual { - require(_msgSender() == pendingDefaultAdmin(), "AccessControl: pending admin must accept"); - _acceptDefaultAdminTransfer(); - } - - /** - * @inheritdoc IAccessControlDefaultAdminRules - */ - function cancelDefaultAdminTransfer() public virtual onlyRole(DEFAULT_ADMIN_ROLE) { - _resetDefaultAdminTransfer(); - } - - /** - * @dev Revokes `role` from the calling account. - * - * For `DEFAULT_ADMIN_ROLE`, only allows renouncing in two steps, so it's required - * that the {defaultAdminTransferDelayedUntil} has passed and the pending default admin is the zero address. - * After its execution, it will not be possible to call `onlyRole(DEFAULT_ADMIN_ROLE)` - * functions. - * - * For other roles, see {AccessControl-renounceRole}. - * - * NOTE: Renouncing `DEFAULT_ADMIN_ROLE` will leave the contract without a defaultAdmin, - * thereby disabling any functionality that is only available to the default admin, and the - * possibility of reassigning a non-administrated role. - */ - function renounceRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { - if (role == DEFAULT_ADMIN_ROLE) { - require( - pendingDefaultAdmin() == address(0) && _hasDefaultAdminTransferDelayPassed(), - "AccessControl: only can renounce in two delayed steps" - ); - } - super.renounceRole(role, account); - } + /// + /// Override AccessControl role management + /// /** * @dev See {AccessControl-grantRole}. Reverts for `DEFAULT_ADMIN_ROLE`. @@ -162,24 +92,37 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu } /** - * @dev See {AccessControl-_setRoleAdmin}. Reverts for `DEFAULT_ADMIN_ROLE`. + * @dev See {AccessControl-renounceRole}. + * + * For the `DEFAULT_ADMIN_ROLE`, it only allows renouncing in two steps by first calling + * {beginDefaultAdminTransfer} to the `address(0)`, so it's required that the {pendingDefaultAdmin} schedule + * has also passed when calling this function. + * + * After its execution, it will not be possible to call `onlyRole(DEFAULT_ADMIN_ROLE)` functions. + * + * NOTE: Renouncing `DEFAULT_ADMIN_ROLE` will leave the contract without a {defaultAdmin}, + * thereby disabling any functionality that is only available for it, and the possibility of reassigning a + * non-administrated role. */ - function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual override { - require(role != DEFAULT_ADMIN_ROLE, "AccessControl: can't violate default admin rules"); - super._setRoleAdmin(role, adminRole); + function renounceRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { + if (role == DEFAULT_ADMIN_ROLE) { + (address newDefaultAdmin, uint48 schedule) = pendingDefaultAdmin(); + require( + newDefaultAdmin == address(0) && _isScheduleSet(schedule) && _hasSchedulePassed(schedule), + "AccessControl: only can renounce in two delayed steps" + ); + } + super.renounceRole(role, account); } /** - * @dev Grants `role` to `account`. + * @dev See {AccessControl-_grantRole}. * - * For `DEFAULT_ADMIN_ROLE`, it only allows granting if there isn't already a role's holder - * or if the role has been previously renounced. + * For `DEFAULT_ADMIN_ROLE`, it only allows granting if there isn't already a {defaultAdmin} or if the + * role has been previously renounced. * - * For other roles, see {AccessControl-renounceRole}. - * - * NOTE: Exposing this function through another mechanism may make the - * `DEFAULT_ADMIN_ROLE` assignable again. Make sure to guarantee this is - * the expected behavior in your implementation. + * NOTE: Exposing this function through another mechanism may make the `DEFAULT_ADMIN_ROLE` + * assignable again. Make sure to guarantee this is the expected behavior in your implementation. */ function _grantRole(bytes32 role, address account) internal virtual override { if (role == DEFAULT_ADMIN_ROLE) { @@ -189,29 +132,6 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu super._grantRole(role, account); } - /** - * @dev See {acceptDefaultAdminTransfer}. - * - * Internal function without access restriction. - */ - function _acceptDefaultAdminTransfer() internal virtual { - require(_hasDefaultAdminTransferDelayPassed(), "AccessControl: transfer delay not passed"); - _revokeRole(DEFAULT_ADMIN_ROLE, defaultAdmin()); - _grantRole(DEFAULT_ADMIN_ROLE, pendingDefaultAdmin()); - _resetDefaultAdminTransfer(); - } - - /** - * @dev See {beginDefaultAdminTransfer}. - * - * Internal function without access restriction. - */ - function _beginDefaultAdminTransfer(address newAdmin) internal virtual { - _defaultAdminTransferDelayedUntil = SafeCast.toUint48(block.timestamp) + defaultAdminDelay(); - _pendingDefaultAdmin = newAdmin; - emit DefaultAdminRoleChangeStarted(pendingDefaultAdmin(), defaultAdminTransferDelayedUntil()); - } - /** * @dev See {AccessControl-_revokeRole}. */ @@ -223,18 +143,239 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu } /** - * @dev Resets the pending default admin and delayed until. + * @dev See {AccessControl-_setRoleAdmin}. Reverts for `DEFAULT_ADMIN_ROLE`. */ - function _resetDefaultAdminTransfer() private { - delete _pendingDefaultAdmin; - delete _defaultAdminTransferDelayedUntil; + function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual override { + require(role != DEFAULT_ADMIN_ROLE, "AccessControl: can't violate default admin rules"); + super._setRoleAdmin(role, adminRole); + } + + /// + /// AccessControlDefaultAdminRules accessors + /// + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function defaultAdmin() public view virtual returns (address) { + return _currentDefaultAdmin; } /** - * @dev Checks if a {defaultAdminTransferDelayedUntil} has been set and passed. + * @inheritdoc IAccessControlDefaultAdminRules */ - function _hasDefaultAdminTransferDelayPassed() private view returns (bool) { - uint48 delayedUntil = defaultAdminTransferDelayedUntil(); - return delayedUntil > 0 && delayedUntil < block.timestamp; + function pendingDefaultAdmin() public view virtual returns (address newAdmin, uint48 schedule) { + return (_pendingDefaultAdmin, _pendingDefaultAdminSchedule); + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function defaultAdminDelay() public view virtual returns (uint48) { + uint48 schedule = _pendingDelaySchedule; + return (_isScheduleSet(schedule) && _hasSchedulePassed(schedule)) ? _pendingDelay : _currentDelay; + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function pendingDefaultAdminDelay() public view virtual returns (uint48 newDelay, uint48 schedule) { + schedule = _pendingDelaySchedule; + return (_isScheduleSet(schedule) && !_hasSchedulePassed(schedule)) ? (_pendingDelay, schedule) : (0, 0); + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function defaultAdminDelayIncreaseWait() public view virtual returns (uint48) { + return 5 days; + } + + /// + /// AccessControlDefaultAdminRules public and internal setters for defaultAdmin/pendingDefaultAdmin + /// + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function beginDefaultAdminTransfer(address newAdmin) public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _beginDefaultAdminTransfer(newAdmin); + } + + /** + * @dev See {beginDefaultAdminTransfer}. + * + * Internal function without access restriction. + */ + function _beginDefaultAdminTransfer(address newAdmin) internal virtual { + uint48 newSchedule = SafeCast.toUint48(block.timestamp) + defaultAdminDelay(); + _setPendingDefaultAdmin(newAdmin, newSchedule); + emit DefaultAdminTransferScheduled(newAdmin, newSchedule); + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function cancelDefaultAdminTransfer() public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _cancelDefaultAdminTransfer(); + } + + /** + * @dev See {cancelDefaultAdminTransfer}. + * + * Internal function without access restriction. + */ + function _cancelDefaultAdminTransfer() internal virtual { + _setPendingDefaultAdmin(address(0), 0); + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function acceptDefaultAdminTransfer() public virtual { + (address newDefaultAdmin, ) = pendingDefaultAdmin(); + require(_msgSender() == newDefaultAdmin, "AccessControl: pending admin must accept"); + _acceptDefaultAdminTransfer(); + } + + /** + * @dev See {acceptDefaultAdminTransfer}. + * + * Internal function without access restriction. + */ + function _acceptDefaultAdminTransfer() internal virtual { + (address newAdmin, uint48 schedule) = pendingDefaultAdmin(); + require(_isScheduleSet(schedule) && _hasSchedulePassed(schedule), "AccessControl: transfer delay not passed"); + _revokeRole(DEFAULT_ADMIN_ROLE, defaultAdmin()); + _grantRole(DEFAULT_ADMIN_ROLE, newAdmin); + delete _pendingDefaultAdmin; + delete _pendingDefaultAdminSchedule; + } + + /// + /// AccessControlDefaultAdminRules public and internal setters for defaultAdminDelay/pendingDefaultAdminDelay + /// + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function changeDefaultAdminDelay(uint48 newDelay) public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _changeDefaultAdminDelay(newDelay); + } + + /** + * @dev See {changeDefaultAdminDelay}. + * + * Internal function without access restriction. + */ + function _changeDefaultAdminDelay(uint48 newDelay) internal virtual { + uint48 newSchedule = SafeCast.toUint48(block.timestamp) + _delayChangeWait(newDelay); + _setPendingDelay(newDelay, newSchedule); + emit DefaultAdminDelayChangeScheduled(newDelay, newSchedule); + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function rollbackDefaultAdminDelay() public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _rollbackDefaultAdminDelay(); + } + + /** + * @dev See {rollbackDefaultAdminDelay}. + * + * Internal function without access restriction. + */ + function _rollbackDefaultAdminDelay() internal virtual { + _setPendingDelay(0, 0); + } + + /** + * @dev Returns the amount of seconds to wait after the `newDelay` will + * become the new {defaultAdminDelay}. + * + * The value returned guarantees that if the delay is reduced, it will go into effect + * after a wait that honors the previously set delay. + * + * See {defaultAdminDelayIncreaseWait}. + */ + function _delayChangeWait(uint48 newDelay) internal view virtual returns (uint48) { + uint48 currentDelay = defaultAdminDelay(); + + // When increasing the delay, we schedule the delay change to occur after a period of "new delay" has passed, up + // to a maximum given by defaultAdminDelayIncreaseWait, by default 5 days. For example, if increasing from 1 day + // to 3 days, the new delay will come into effect after 3 days. If increasing from 1 day to 10 days, the new + // delay will come into effect after 5 days. The 5 day wait period is intended to be able to fix an error like + // using milliseconds instead of seconds. + // + // When decreasing the delay, we wait the difference between "current delay" and "new delay". This guarantees + // that an admin transfer cannot be made faster than "current delay" at the time the delay change is scheduled. + // For example, if decreasing from 10 days to 3 days, the new delay will come into effect after 7 days. + return + newDelay > currentDelay + ? uint48(Math.min(newDelay, defaultAdminDelayIncreaseWait())) // no need to safecast, both inputs are uint48 + : currentDelay - newDelay; + } + + /// + /// Private setters + /// + + /** + * @dev Setter of the tuple for pending admin and its schedule. + * + * May emit a DefaultAdminTransferCanceled event. + */ + function _setPendingDefaultAdmin(address newAdmin, uint48 newSchedule) private { + (, uint48 oldSchedule) = pendingDefaultAdmin(); + + _pendingDefaultAdmin = newAdmin; + _pendingDefaultAdminSchedule = newSchedule; + + // An `oldSchedule` from `pendingDefaultAdmin()` is only set if it hasn't been accepted. + if (_isScheduleSet(oldSchedule)) { + // Emit for implicit cancellations when another default admin was scheduled. + emit DefaultAdminTransferCanceled(); + } + } + + /** + * @dev Setter of the tuple for pending delay and its schedule. + * + * May emit a DefaultAdminDelayChangeCanceled event. + */ + function _setPendingDelay(uint48 newDelay, uint48 newSchedule) private { + uint48 oldSchedule = _pendingDelaySchedule; + + if (_isScheduleSet(oldSchedule)) { + if (_hasSchedulePassed(oldSchedule)) { + // Materialize a virtual delay + _currentDelay = _pendingDelay; + } else { + // Emit for implicit cancellations when another delay was scheduled. + emit DefaultAdminDelayChangeCanceled(); + } + } + + _pendingDelay = newDelay; + _pendingDelaySchedule = newSchedule; + } + + /// + /// Private helpers + /// + + /** + * @dev Defines if an `schedule` is considered set. For consistency purposes. + */ + function _isScheduleSet(uint48 schedule) private pure returns (bool) { + return schedule != 0; + } + + /** + * @dev Defines if an `schedule` is considered passed. For consistency purposes. + */ + function _hasSchedulePassed(uint48 schedule) private view returns (bool) { + return schedule < block.timestamp; } } diff --git a/contracts/access/IAccessControlDefaultAdminRules.sol b/contracts/access/IAccessControlDefaultAdminRules.sol index 753c7c802..d28c49d95 100644 --- a/contracts/access/IAccessControlDefaultAdminRules.sol +++ b/contracts/access/IAccessControlDefaultAdminRules.sol @@ -12,16 +12,27 @@ import "./IAccessControl.sol"; */ interface IAccessControlDefaultAdminRules is IAccessControl { /** - * @dev Emitted when a `DEFAULT_ADMIN_ROLE` transfer is started, setting `newDefaultAdmin` - * as the next default admin, which will have rights to claim the `DEFAULT_ADMIN_ROLE` - * after `defaultAdminTransferDelayedUntil` has passed. + * @dev Emitted when a {defaultAdmin} transfer is started, setting `newAdmin` as the next + * address to become the {defaultAdmin} by calling {acceptDefaultAdminTransfer} only after `acceptSchedule` + * passes. */ - event DefaultAdminRoleChangeStarted(address indexed newDefaultAdmin, uint48 defaultAdminTransferDelayedUntil); + event DefaultAdminTransferScheduled(address indexed newAdmin, uint48 acceptSchedule); /** - * @dev Returns the delay between each `DEFAULT_ADMIN_ROLE` transfer. + * @dev Emitted when a {pendingDefaultAdmin} is reset if it was never accepted, regardless of its schedule. */ - function defaultAdminDelay() external view returns (uint48); + event DefaultAdminTransferCanceled(); + + /** + * @dev Emitted when a {defaultAdminDelay} change is started, setting `newDelay` as the next + * delay to be applied between default admin transfer after `effectSchedule` has passed. + */ + event DefaultAdminDelayChangeScheduled(uint48 newDelay, uint48 effectSchedule); + + /** + * @dev Emitted when a {pendingDefaultAdminDelay} is reset if its schedule didn't pass. + */ + event DefaultAdminDelayChangeCanceled(); /** * @dev Returns the address of the current `DEFAULT_ADMIN_ROLE` holder. @@ -29,45 +40,133 @@ interface IAccessControlDefaultAdminRules is IAccessControl { function defaultAdmin() external view returns (address); /** - * @dev Returns the address of the pending `DEFAULT_ADMIN_ROLE` holder. + * @dev Returns a tuple of a `newAdmin` and an accept schedule. + * + * After the `schedule` passes, the `newAdmin` will be able to accept the {defaultAdmin} role + * by calling {acceptDefaultAdminTransfer}, completing the role transfer. + * + * A zero value only in `acceptSchedule` indicates no pending admin transfer. + * + * NOTE: A zero address `newAdmin` means that {defaultAdmin} is being renounced. */ - function pendingDefaultAdmin() external view returns (address); + function pendingDefaultAdmin() external view returns (address newAdmin, uint48 acceptSchedule); /** - * @dev Returns the timestamp after which the pending default admin can claim the `DEFAULT_ADMIN_ROLE`. + * @dev Returns the delay required to schedule the acceptance of a {defaultAdmin} transfer started. + * + * This delay will be added to the current timestamp when calling {beginDefaultAdminTransfer} to set + * the acceptance schedule. + * + * NOTE: If a delay change has been scheduled, it will take effect as soon as the schedule passes, making this + * function returns the new delay. See {changeDefaultAdminDelay}. */ - function defaultAdminTransferDelayedUntil() external view returns (uint48); + function defaultAdminDelay() external view returns (uint48); /** - * @dev Starts a `DEFAULT_ADMIN_ROLE` transfer by setting a pending default admin - * and a timer to pass. + * @dev Returns a tuple of `newDelay` and an effect schedule. + * + * After the `schedule` passes, the `newDelay` will get into effect immediately for every + * new {defaultAdmin} transfer started with {beginDefaultAdminTransfer}. + * + * A zero value only in `effectSchedule` indicates no pending delay change. + * + * NOTE: A zero value only for `newDelay` means that the next {defaultAdminDelay} + * will be zero after the effect schedule. + */ + function pendingDefaultAdminDelay() external view returns (uint48 newDelay, uint48 effectSchedule); + + /** + * @dev Starts a {defaultAdmin} transfer by setting a {pendingDefaultAdmin} scheduled for acceptance + * after the current timestamp plus a {defaultAdminDelay}. * * Requirements: * - * - Only can be called by the current `DEFAULT_ADMIN_ROLE` holder. + * - Only can be called by the current {defaultAdmin}. * - * Emits a {DefaultAdminRoleChangeStarted}. + * Emits a DefaultAdminRoleChangeStarted event. */ function beginDefaultAdminTransfer(address newAdmin) external; /** - * @dev Completes a `DEFAULT_ADMIN_ROLE` transfer. + * @dev Cancels a {defaultAdmin} transfer previously started with {beginDefaultAdminTransfer}. + * + * A {pendingDefaultAdmin} not yet accepted can also be cancelled with this function. * * Requirements: * - * - Caller should be the pending default admin. + * - Only can be called by the current {defaultAdmin}. + * + * May emit a DefaultAdminTransferCanceled event. + */ + function cancelDefaultAdminTransfer() external; + + /** + * @dev Completes a {defaultAdmin} transfer previously started with {beginDefaultAdminTransfer}. + * + * After calling the function: + * * - `DEFAULT_ADMIN_ROLE` should be granted to the caller. * - `DEFAULT_ADMIN_ROLE` should be revoked from the previous holder. + * - {pendingDefaultAdmin} should be reset to zero values. + * + * Requirements: + * + * - Only can be called by the {pendingDefaultAdmin}'s `newAdmin`. + * - The {pendingDefaultAdmin}'s `acceptSchedule` should've passed. */ function acceptDefaultAdminTransfer() external; /** - * @dev Cancels a `DEFAULT_ADMIN_ROLE` transfer. + * @dev Initiates a {defaultAdminDelay} update by setting a {pendingDefaultAdminDelay} scheduled for getting + * into effect after the current timestamp plus a {defaultAdminDelay}. + * + * This function guarantees that any call to {beginDefaultAdminTransfer} done between the timestamp this + * method is called and the {pendingDefaultAdminDelay} effect schedule will use the current {defaultAdminDelay} + * set before calling. + * + * The {pendingDefaultAdminDelay}'s effect schedule is defined in a way that waiting until the schedule and then + * calling {beginDefaultAdminTransfer} with the new delay will take at least the same as another {defaultAdmin} + * complete transfer (including acceptance). + * + * The schedule is designed for two scenarios: + * + * - When the delay is changed for a larger one the schedule is `block.timestamp + newDelay` capped by + * {defaultAdminDelayIncreaseWait}. + * - When the delay is changed for a shorter one, the schedule is `block.timestamp + (current delay - new delay)`. + * + * A {pendingDefaultAdminDelay} that never got into effect will be canceled in favor of a new scheduled change. * * Requirements: * - * - Can be called even after the timer has passed. - * - Can only be called by the current `DEFAULT_ADMIN_ROLE` holder. + * - Only can be called by the current {defaultAdmin}. + * + * Emits a DefaultAdminDelayChangeScheduled event and may emit a DefaultAdminDelayChangeCanceled event. */ - function cancelDefaultAdminTransfer() external; + function changeDefaultAdminDelay(uint48 newDelay) external; + + /** + * @dev Cancels a scheduled {defaultAdminDelay} change. + * + * Requirements: + * + * - Only can be called by the current {defaultAdmin}. + * + * May emit a DefaultAdminDelayChangeCanceled event. + */ + function rollbackDefaultAdminDelay() external; + + /** + * @dev Maximum time in seconds for an increase to {defaultAdminDelay} (that is scheduled using {changeDefaultAdminDelay}) + * to take effect. Default to 5 days. + * + * When the {defaultAdminDelay} is scheduled to be increased, it goes into effect after the new delay has passed with + * the purpose of giving enough time for reverting any accidental change (i.e. using milliseconds instead of seconds) + * that may lock the contract. However, to avoid excessive schedules, the wait is capped by this function and it can + * be overrode for a custom {defaultAdminDelay} increase scheduling. + * + * IMPORTANT: Make sure to add a reasonable amount of time while overriding this value, otherwise, + * there's a risk of setting a high new delay that goes into effect almost immediately without the + * possibility of human intervention in the case of an input error (eg. set milliseconds instead of seconds). + */ + function defaultAdminDelayIncreaseWait() external view returns (uint48); } diff --git a/test/access/AccessControl.behavior.js b/test/access/AccessControl.behavior.js index e04c5a165..6c88aa274 100644 --- a/test/access/AccessControl.behavior.js +++ b/test/access/AccessControl.behavior.js @@ -1,13 +1,16 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS } = require('@openzeppelin/test-helpers/src/constants'); +const { expectEvent, expectRevert, constants, BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); + const { time } = require('@nomicfoundation/hardhat-network-helpers'); const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); +const { network } = require('hardhat'); +const { ZERO_ADDRESS } = require('@openzeppelin/test-helpers/src/constants'); const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000'; 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) { shouldSupportInterfaces(['AccessControl']); @@ -215,18 +218,151 @@ function shouldBehaveLikeAccessControlEnumerable(errorPrefix, admin, authorized, function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defaultAdmin, newDefaultAdmin, other) { shouldSupportInterfaces(['AccessControlDefaultAdminRules']); - it('has a default disabled delayed until', async function () { - expect(await this.accessControl.defaultAdminTransferDelayedUntil()).to.be.bignumber.equal(web3.utils.toBN(0)); + 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 () { + const value = await this.accessControl[getter](); + expect(value).to.equal(defaultAdmin); + expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, value)).to.be.true; + }); + + it('changes if the default admin changes', async function () { + // Starts an admin transfer + await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); + + // Wait for acceptance + const acceptSchedule = web3.utils.toBN(await time.latest()).add(delay); + await time.setNextBlockTimestamp(acceptSchedule.addn(1)); + await this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }); + + const value = await this.accessControl[getter](); + expect(value).to.equal(newDefaultAdmin); + }); + }); + } + + describe('pendingDefaultAdmin()', function () { + it('returns 0 if no pending default admin transfer', async function () { + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.eq(ZERO_ADDRESS); + expect(schedule).to.be.bignumber.eq(ZERO); + }); + + describe('when there is a scheduled default admin transfer', function () { + beforeEach('begins admin transfer', async function () { + await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); + }); + + for (const [fromSchedule, tag] of [ + [-1, 'before'], + [0, 'exactly when'], + [1, 'after'], + ]) { + it(`returns pending admin and delay ${tag} delay schedule passes if not accepted`, async function () { + // Wait until schedule + fromSchedule + const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdmin(); + await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule); + await network.provider.send('evm_mine'); // Mine a block to force the timestamp + + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.eq(newDefaultAdmin); + expect(schedule).to.be.bignumber.eq(firstSchedule); + }); + } + + it('returns 0 after delay schedule passes and the transfer was accepted', async function () { + // Wait after schedule + const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdmin(); + await time.setNextBlockTimestamp(firstSchedule.addn(1)); + + // Accepts + await this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }); + + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.eq(ZERO_ADDRESS); + expect(schedule).to.be.bignumber.eq(ZERO); + }); + }); }); - it('has a default pending default admin', async function () { - expect(await this.accessControl.pendingDefaultAdmin()).to.equal(ZERO_ADDRESS); + describe('defaultAdminDelay()', function () { + it('returns the current delay', async function () { + expect(await this.accessControl.defaultAdminDelay()).to.be.bignumber.eq(delay); + }); + + describe('when there is a scheduled delay change', function () { + const newDelay = web3.utils.toBN(0xdead); // Any change + + beforeEach('begins delay change', async function () { + await this.accessControl.changeDefaultAdminDelay(newDelay, { from: defaultAdmin }); + }); + + for (const [fromSchedule, tag, expectedDelay, delayTag] of [ + [-1, 'before', delay, 'old'], + [0, 'exactly when', delay, 'old'], + [1, 'after', newDelay, 'new'], + ]) { + it(`returns ${delayTag} delay ${tag} delay schedule passes`, async function () { + // Wait until schedule + fromSchedule + const { schedule } = await this.accessControl.pendingDefaultAdminDelay(); + await time.setNextBlockTimestamp(schedule.toNumber() + fromSchedule); + await network.provider.send('evm_mine'); // Mine a block to force the timestamp + + const currentDelay = await this.accessControl.defaultAdminDelay(); + expect(currentDelay).to.be.bignumber.eq(expectedDelay); + }); + } + }); }); - it('has a default current owner set to the initial default admin', async function () { - const owner = await this.accessControl.owner(); - expect(owner).to.equal(defaultAdmin); - expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, owner)).to.be.true; + describe('pendingDefaultAdminDelay()', function () { + it('returns 0 if not set', async function () { + const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay(); + expect(newDelay).to.be.bignumber.eq(ZERO); + expect(schedule).to.be.bignumber.eq(ZERO); + }); + + describe('when there is a scheduled delay change', function () { + const newDelay = web3.utils.toBN(0xdead); // Any change + + beforeEach('begins admin transfer', async function () { + await this.accessControl.changeDefaultAdminDelay(newDelay, { from: defaultAdmin }); + }); + + for (const [fromSchedule, tag, expectedDelay, delayTag, expectZeroSchedule] of [ + [-1, 'before', newDelay, 'new'], + [0, 'exactly when', newDelay, 'new'], + [1, 'after', ZERO, 'zero', true], + ]) { + it(`returns ${delayTag} delay ${tag} delay schedule passes`, async function () { + // Wait until schedule + fromSchedule + const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdminDelay(); + await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule); + await network.provider.send('evm_mine'); // Mine a block to force the timestamp + + const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay(); + expect(newDelay).to.be.bignumber.eq(expectedDelay); + expect(schedule).to.be.bignumber.eq(expectZeroSchedule ? ZERO : firstSchedule); + }); + } + }); + }); + + describe('defaultAdminDelayIncreaseWait()', function () { + it('should return 5 days (default)', async function () { + expect(await this.accessControl.defaultAdminDelayIncreaseWait()).to.be.bignumber.eq( + web3.utils.toBN(time.duration.days(5)), + ); + }); }); it('should revert if granting default admin role', async function () { @@ -257,72 +393,130 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa ); }); - describe('begins transfer of default admin', function () { + describe('begins a default admin transfer', function () { let receipt; - let defaultAdminTransferDelayedUntil; + let acceptSchedule; - beforeEach('begins admin transfer', async function () { - receipt = await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); - defaultAdminTransferDelayedUntil = web3.utils.toBN(await time.latest()).add(delay); - }); - - it('should set pending default admin and delayed until', async function () { - expect(await this.accessControl.pendingDefaultAdmin()).to.equal(newDefaultAdmin); - expect(await this.accessControl.defaultAdminTransferDelayedUntil()).to.be.bignumber.equal( - defaultAdminTransferDelayedUntil, - ); - expectEvent(receipt, 'DefaultAdminRoleChangeStarted', { - newDefaultAdmin, - defaultAdminTransferDelayedUntil, - }); - }); - - it('should be able to begin a transfer again before delay pass', async function () { - // Time passes just before delay - await time.setNextBlockTimestamp(defaultAdminTransferDelayedUntil.subn(1)); - - // defaultAdmin changes its mind and begin again to another address - await this.accessControl.beginDefaultAdminTransfer(other, { from: defaultAdmin }); - const newDelayedUntil = web3.utils.toBN(await time.latest()).add(delay); - expect(await this.accessControl.pendingDefaultAdmin()).to.equal(other); - expect(await this.accessControl.defaultAdminTransferDelayedUntil()).to.be.bignumber.equal(newDelayedUntil); - }); - - it('should be able to begin a transfer again after delay pass if not accepted', async function () { - // Time passes after delay without acceptance - await time.setNextBlockTimestamp(defaultAdminTransferDelayedUntil.addn(1)); - - // defaultAdmin changes its mind and begin again to another address - await this.accessControl.beginDefaultAdminTransfer(other, { from: defaultAdmin }); - const newDelayedUntil = web3.utils.toBN(await time.latest()).add(delay); - expect(await this.accessControl.pendingDefaultAdmin()).to.equal(other); - expect(await this.accessControl.defaultAdminTransferDelayedUntil()).to.be.bignumber.equal(newDelayedUntil); - }); - - it('should revert if it is called by non-admin accounts', async function () { + it('reverts if called by non default admin accounts', async function () { await expectRevert( this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: other }), `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, ); }); + + describe('when there is no pending delay nor pending admin transfer', function () { + beforeEach('begins admin transfer', async function () { + receipt = await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); + acceptSchedule = web3.utils.toBN(await time.latest()).add(delay); + }); + + it('should set pending default admin and schedule', async function () { + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.equal(newDefaultAdmin); + expect(schedule).to.be.bignumber.equal(acceptSchedule); + expectEvent(receipt, 'DefaultAdminTransferScheduled', { + newAdmin, + acceptSchedule, + }); + }); + }); + + describe('when there is a pending admin transfer', function () { + beforeEach('sets a pending default admin transfer', async function () { + await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); + acceptSchedule = web3.utils.toBN(await time.latest()).add(delay); + }); + + for (const [fromSchedule, tag] of [ + [-1, 'before'], + [0, 'exactly when'], + [1, 'after'], + ]) { + it(`should be able to begin a transfer again ${tag} acceptSchedule passes`, async function () { + // Wait until schedule + fromSchedule + await time.setNextBlockTimestamp(acceptSchedule.toNumber() + fromSchedule); + + // defaultAdmin changes its mind and begin again to another address + const receipt = await this.accessControl.beginDefaultAdminTransfer(other, { from: defaultAdmin }); + const newSchedule = web3.utils.toBN(await time.latest()).add(delay); + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.equal(other); + expect(schedule).to.be.bignumber.equal(newSchedule); + + // Cancellation is always emitted since it was never accepted + expectEvent(receipt, 'DefaultAdminTransferCanceled'); + }); + } + + it('should not emit a cancellation event if the new default admin accepted', async function () { + // Wait until the acceptSchedule has passed + await time.setNextBlockTimestamp(acceptSchedule.addn(1)); + + // Accept and restart + await this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }); + const receipt = await this.accessControl.beginDefaultAdminTransfer(other, { from: newDefaultAdmin }); + + expectNoEvent(receipt, 'DefaultAdminTransferCanceled'); + }); + }); + + describe('when there is a pending delay', function () { + const newDelay = web3.utils.toBN(time.duration.hours(3)); + + beforeEach('schedule a delay change', async function () { + await this.accessControl.changeDefaultAdminDelay(newDelay, { from: defaultAdmin }); + const pendingDefaultAdminDelay = await this.accessControl.pendingDefaultAdminDelay(); + acceptSchedule = pendingDefaultAdminDelay.schedule; + }); + + for (const [fromSchedule, schedulePassed, expectedDelay, delayTag] of [ + [-1, 'before', delay, 'old'], + [0, 'exactly when', delay, 'old'], + [1, 'after', newDelay, 'new'], + ]) { + it(`should set the ${delayTag} delay and apply it to next default admin transfer schedule ${schedulePassed} acceptSchedule passed`, async function () { + // Wait until the expected fromSchedule time + await time.setNextBlockTimestamp(acceptSchedule.toNumber() + fromSchedule); + + // Start the new default admin transfer and get its schedule + const receipt = await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); + const expectedAcceptSchedule = web3.utils.toBN(await time.latest()).add(expectedDelay); + + // Check that the schedule corresponds with the new delay + const { newAdmin, schedule: transferSchedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.equal(newDefaultAdmin); + expect(transferSchedule).to.be.bignumber.equal(expectedAcceptSchedule); + + expectEvent(receipt, 'DefaultAdminTransferScheduled', { + newAdmin, + acceptSchedule: expectedAcceptSchedule, + }); + }); + } + }); }); describe('accepts transfer admin', function () { - let delayPassed; + let acceptSchedule; beforeEach(async function () { await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); - delayPassed = web3.utils - .toBN(await time.latest()) - .add(delay) - .addn(1); + acceptSchedule = web3.utils.toBN(await time.latest()).add(delay); }); - describe('caller is pending default admin and delay has passed', function () { + it('should revert if caller is not pending default admin', async function () { + await time.setNextBlockTimestamp(acceptSchedule.addn(1)); + await expectRevert( + this.accessControl.acceptDefaultAdminTransfer({ from: other }), + `${errorPrefix}: pending admin must accept`, + ); + }); + + describe('when caller is pending default admin and delay has passed', function () { let from; beforeEach(async function () { - await time.setNextBlockTimestamp(delayPassed); + await time.setNextBlockTimestamp(acceptSchedule.addn(1)); from = newDefaultAdmin; }); @@ -344,108 +538,122 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa account: newDefaultAdmin, }); - // Resets pending default admin and delayed until - expect(await this.accessControl.defaultAdminTransferDelayedUntil()).to.be.bignumber.equal(web3.utils.toBN(0)); - expect(await this.accessControl.pendingDefaultAdmin()).to.equal(ZERO_ADDRESS); + // Resets pending default admin and schedule + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.equal(constants.ZERO_ADDRESS); + expect(schedule).to.be.bignumber.equal(ZERO); }); }); - it('should revert if caller is not pending default admin', async function () { - await time.setNextBlockTimestamp(delayPassed); - await expectRevert( - this.accessControl.acceptDefaultAdminTransfer({ from: other }), - `${errorPrefix}: pending admin must accept`, - ); - }); - - describe('delayedUntil not passed', function () { - let delayNotPassed; - - beforeEach(function () { - delayNotPassed = delayPassed.subn(1); - }); - - it('should revert if block.timestamp is equal to delayed until', async function () { - await time.setNextBlockTimestamp(delayNotPassed); - await expectRevert( - this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }), - `${errorPrefix}: transfer delay not passed`, - ); - }); - - it('should revert if block.timestamp is less than delayed until', async function () { - await expectRevert( - this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }), - `${errorPrefix}: transfer delay not passed`, - ); - }); + describe('schedule not passed', function () { + for (const [fromSchedule, tag] of [ + [-1, 'less'], + [0, 'equal'], + ]) { + it(`should revert if block.timestamp is ${tag} to schedule`, async function () { + await time.setNextBlockTimestamp(acceptSchedule.toNumber() + fromSchedule); + await expectRevert( + this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }), + `${errorPrefix}: transfer delay not passed`, + ); + }); + } }); }); - describe('cancel transfer default admin', function () { - let delayPassed; - - beforeEach(async function () { - await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); - delayPassed = web3.utils - .toBN(await time.latest()) - .add(delay) - .addn(1); - }); - - it('resets pending default admin and delayed until', async function () { - await this.accessControl.cancelDefaultAdminTransfer({ from: defaultAdmin }); - expect(await this.accessControl.defaultAdminTransferDelayedUntil()).to.be.bignumber.equal(web3.utils.toBN(0)); - expect(await this.accessControl.pendingDefaultAdmin()).to.equal(ZERO_ADDRESS); - - // Advance until passed delay - await time.setNextBlockTimestamp(delayPassed); - - // Previous pending default admin should not be able to accept after cancellation. - await expectRevert( - this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }), - `${errorPrefix}: pending admin must accept`, - ); - }); - - it('cancels even after delay has passed', async function () { - await this.accessControl.cancelDefaultAdminTransfer({ from: defaultAdmin }); - await time.setNextBlockTimestamp(delayPassed); - expect(await this.accessControl.defaultAdminTransferDelayedUntil()).to.be.bignumber.equal(web3.utils.toBN(0)); - expect(await this.accessControl.pendingDefaultAdmin()).to.equal(ZERO_ADDRESS); - }); - + describe('cancels a default admin transfer', function () { it('reverts if called by non default admin accounts', async function () { await expectRevert( this.accessControl.cancelDefaultAdminTransfer({ from: other }), `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, ); }); + + describe('when there is a pending default admin transfer', function () { + let acceptSchedule; + + beforeEach(async function () { + await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); + acceptSchedule = web3.utils.toBN(await time.latest()).add(delay); + }); + + for (const [fromSchedule, tag] of [ + [-1, 'before'], + [0, 'exactly when'], + [1, 'after'], + ]) { + it(`resets pending default admin and schedule ${tag} transfer schedule passes`, async function () { + // Advance until passed delay + await time.setNextBlockTimestamp(acceptSchedule.toNumber() + fromSchedule); + + const receipt = await this.accessControl.cancelDefaultAdminTransfer({ from: defaultAdmin }); + + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.equal(constants.ZERO_ADDRESS); + expect(schedule).to.be.bignumber.equal(ZERO); + + expectEvent(receipt, 'DefaultAdminTransferCanceled'); + }); + } + + it('should revert if the previous default admin tries to accept', async function () { + await this.accessControl.cancelDefaultAdminTransfer({ from: defaultAdmin }); + + // Advance until passed delay + await time.setNextBlockTimestamp(acceptSchedule.addn(1)); + + // Previous pending default admin should not be able to accept after cancellation. + await expectRevert( + this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }), + `${errorPrefix}: pending admin must accept`, + ); + }); + }); + + describe('when there is no pending default admin transfer', async function () { + it('should succeed without changes', async function () { + const receipt = await this.accessControl.cancelDefaultAdminTransfer({ from: defaultAdmin }); + + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.equal(constants.ZERO_ADDRESS); + expect(schedule).to.be.bignumber.equal(ZERO); + + expectNoEvent(receipt, 'DefaultAdminTransferCanceled'); + }); + }); }); - describe('renouncing admin', function () { + describe('renounces admin', function () { let delayPassed; let from = defaultAdmin; beforeEach(async function () { - await this.accessControl.beginDefaultAdminTransfer(ZERO_ADDRESS, { from }); + await this.accessControl.beginDefaultAdminTransfer(constants.ZERO_ADDRESS, { from }); delayPassed = web3.utils .toBN(await time.latest()) .add(delay) .addn(1); }); - it('it renounces role', async function () { + it('reverts if caller is not default admin', async function () { + await time.setNextBlockTimestamp(delayPassed); + await expectRevert( + this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, other, { from }), + `${errorPrefix}: can only renounce roles for self`, + ); + }); + + it('renounces role', async function () { await time.setNextBlockTimestamp(delayPassed); const receipt = await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, from, { from }); expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, defaultAdmin)).to.be.false; - expect(await this.accessControl.hasRole(ZERO_ADDRESS, defaultAdmin)).to.be.false; + expect(await this.accessControl.hasRole(constants.ZERO_ADDRESS, defaultAdmin)).to.be.false; expectEvent(receipt, 'RoleRevoked', { role: DEFAULT_ADMIN_ROLE, account: from, }); - expect(await this.accessControl.owner()).to.equal(ZERO_ADDRESS); + expect(await this.accessControl.owner()).to.equal(constants.ZERO_ADDRESS); }); it('allows to recover access using the internal _grantRole', async function () { @@ -459,35 +667,180 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa }); }); - it('reverts if caller is not default admin', async function () { - await time.setNextBlockTimestamp(delayPassed); - await expectRevert( - this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, other, { from }), - `${errorPrefix}: can only renounce roles for self`, - ); - }); - - describe('delayed until not passed', function () { + describe('schedule not passed', function () { let delayNotPassed; beforeEach(function () { delayNotPassed = delayPassed.subn(1); }); - it('reverts if block.timestamp is equal to delayed until', async function () { - await time.setNextBlockTimestamp(delayNotPassed); - await expectRevert( - this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from }), - `${errorPrefix}: only can renounce in two delayed steps`, - ); + for (const [fromSchedule, tag] of [ + [-1, 'less'], + [0, 'equal'], + ]) { + it(`reverts if block.timestamp is ${tag} to schedule`, async function () { + await time.setNextBlockTimestamp(delayNotPassed.toNumber() + fromSchedule); + await expectRevert( + this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from }), + `${errorPrefix}: only can renounce in two delayed steps`, + ); + }); + } + }); + }); + + describe('changes delay', function () { + it('reverts if called by non default admin accounts', async function () { + await expectRevert( + this.accessControl.changeDefaultAdminDelay(time.duration.hours(4), { + from: other, + }), + `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + ); + }); + + for (const [newDefaultAdminDelay, delayChangeType] of [ + [web3.utils.toBN(delay).subn(time.duration.hours(1)), 'decreased'], + [web3.utils.toBN(delay).addn(time.duration.hours(1)), 'increased'], + [web3.utils.toBN(delay).addn(time.duration.days(5)), 'increased to more than 5 days'], + ]) { + describe(`when the delay is ${delayChangeType}`, function () { + it('begins the delay change to the new delay', async function () { + // Begins the change + const receipt = await this.accessControl.changeDefaultAdminDelay(newDefaultAdminDelay, { + from: defaultAdmin, + }); + + // Calculate expected values + const cap = await this.accessControl.defaultAdminDelayIncreaseWait(); + const changeDelay = newDefaultAdminDelay.lte(delay) + ? delay.sub(newDefaultAdminDelay) + : BN.min(newDefaultAdminDelay, cap); + const timestamp = web3.utils.toBN(await time.latest()); + const effectSchedule = timestamp.add(changeDelay); + + // Assert + const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay(); + expect(newDelay).to.be.bignumber.eq(newDefaultAdminDelay); + expect(schedule).to.be.bignumber.eq(effectSchedule); + expectEvent(receipt, 'DefaultAdminDelayChangeScheduled', { + newDelay, + effectSchedule, + }); + }); + + describe('scheduling again', function () { + beforeEach('schedule once', async function () { + await this.accessControl.changeDefaultAdminDelay(newDefaultAdminDelay, { from: defaultAdmin }); + }); + + for (const [fromSchedule, tag] of [ + [-1, 'before'], + [0, 'exactly when'], + [1, 'after'], + ]) { + const passed = fromSchedule > 0; + + it(`succeeds ${tag} the delay schedule passes`, async function () { + // Wait until schedule + fromSchedule + const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdminDelay(); + await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule); + + // Default admin changes its mind and begins another delay change + const anotherNewDefaultAdminDelay = newDefaultAdminDelay.addn(time.duration.hours(2)); + const receipt = await this.accessControl.changeDefaultAdminDelay(anotherNewDefaultAdminDelay, { + from: defaultAdmin, + }); + + // Calculate expected values + const cap = await this.accessControl.defaultAdminDelayIncreaseWait(); + const timestamp = web3.utils.toBN(await time.latest()); + const effectSchedule = timestamp.add(BN.min(cap, anotherNewDefaultAdminDelay)); + + // Assert + const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay(); + expect(newDelay).to.be.bignumber.eq(anotherNewDefaultAdminDelay); + expect(schedule).to.be.bignumber.eq(effectSchedule); + expectEvent(receipt, 'DefaultAdminDelayChangeScheduled', { + newDelay, + effectSchedule, + }); + }); + + const emit = passed ? 'not emit' : 'emit'; + it(`should ${emit} a cancellation event ${tag} the delay schedule passes`, async function () { + // Wait until schedule + fromSchedule + const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdminDelay(); + await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule); + + // Default admin changes its mind and begins another delay change + const anotherNewDefaultAdminDelay = newDefaultAdminDelay.addn(time.duration.hours(2)); + const receipt = await this.accessControl.changeDefaultAdminDelay(anotherNewDefaultAdminDelay, { + from: defaultAdmin, + }); + + const eventMatcher = passed ? expectNoEvent : expectEvent; + eventMatcher(receipt, 'DefaultAdminDelayChangeCanceled'); + }); + } + }); + }); + } + }); + + describe('rollbacks a delay change', function () { + it('reverts if called by non default admin accounts', async function () { + await expectRevert( + this.accessControl.rollbackDefaultAdminDelay({ from: other }), + `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + ); + }); + + describe('when there is a pending delay', function () { + beforeEach('set pending delay', async function () { + await this.accessControl.changeDefaultAdminDelay(time.duration.hours(12), { from: defaultAdmin }); }); - it('reverts if block.timestamp is less than delayed until', async function () { - await time.setNextBlockTimestamp(delayNotPassed.subn(1)); - await expectRevert( - this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from }), - `${errorPrefix}: only can renounce in two delayed steps`, - ); + for (const [fromSchedule, tag] of [ + [-1, 'before'], + [0, 'exactly when'], + [1, 'after'], + ]) { + const passed = fromSchedule > 0; + + it(`resets pending delay and schedule ${tag} delay change schedule passes`, async function () { + // Wait until schedule + fromSchedule + const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdminDelay(); + await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule); + + await this.accessControl.rollbackDefaultAdminDelay({ from: defaultAdmin }); + + const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay(); + expect(newDelay).to.be.bignumber.eq(ZERO); + expect(schedule).to.be.bignumber.eq(ZERO); + }); + + const emit = passed ? 'not emit' : 'emit'; + it(`should ${emit} a cancellation event ${tag} the delay schedule passes`, async function () { + // Wait until schedule + fromSchedule + const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdminDelay(); + await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule); + + const receipt = await this.accessControl.rollbackDefaultAdminDelay({ from: defaultAdmin }); + + const eventMatcher = passed ? expectNoEvent : expectEvent; + eventMatcher(receipt, 'DefaultAdminDelayChangeCanceled'); + }); + } + }); + + describe('when there is no pending delay', function () { + it('succeeds without changes', async function () { + await this.accessControl.rollbackDefaultAdminDelay({ from: defaultAdmin }); + + const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay(); + expect(newDelay).to.be.bignumber.eq(ZERO); + expect(schedule).to.be.bignumber.eq(ZERO); }); }); }); diff --git a/test/access/AccessControlDefaultAdminRules.test.js b/test/access/AccessControlDefaultAdminRules.test.js index 2a23d3b6d..4e3167a46 100644 --- a/test/access/AccessControlDefaultAdminRules.test.js +++ b/test/access/AccessControlDefaultAdminRules.test.js @@ -7,7 +7,7 @@ const { const AccessControlDefaultAdminRules = artifacts.require('$AccessControlDefaultAdminRules'); contract('AccessControlDefaultAdminRules', function (accounts) { - const delay = web3.utils.toBN(time.duration.days(10)); + const delay = web3.utils.toBN(time.duration.hours(10)); beforeEach(async function () { this.accessControl = await AccessControlDefaultAdminRules.new(delay, accounts[0], { from: accounts[0] }); diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index 5ffe242ed..541f6c611 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -39,9 +39,12 @@ const INTERFACES = { AccessControlEnumerable: ['getRoleMember(bytes32,uint256)', 'getRoleMemberCount(bytes32)'], AccessControlDefaultAdminRules: [ 'defaultAdminDelay()', + 'pendingDefaultAdminDelay()', 'defaultAdmin()', - 'defaultAdminTransferDelayedUntil()', 'pendingDefaultAdmin()', + 'defaultAdminDelayIncreaseWait()', + 'changeDefaultAdminDelay(uint48)', + 'rollbackDefaultAdminDelay()', 'beginDefaultAdminTransfer(address)', 'acceptDefaultAdminTransfer()', 'cancelDefaultAdminTransfer()', From 7e7060e00e107460fc57c178859d3cf0c6ac64ef Mon Sep 17 00:00:00 2001 From: Antonio Viggiano Date: Thu, 30 Mar 2023 15:57:09 -0300 Subject: [PATCH 089/197] Update IERC3156FlashBorrower.sol (#4145) --- contracts/interfaces/IERC3156FlashBorrower.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/interfaces/IERC3156FlashBorrower.sol b/contracts/interfaces/IERC3156FlashBorrower.sol index c3b4f1eb1..0428391fc 100644 --- a/contracts/interfaces/IERC3156FlashBorrower.sol +++ b/contracts/interfaces/IERC3156FlashBorrower.sol @@ -17,7 +17,7 @@ interface IERC3156FlashBorrower { * @param amount The amount of tokens lent. * @param fee The additional amount of tokens to repay. * @param data Arbitrary data structure, intended to contain user-defined parameters. - * @return The keccak256 hash of "IERC3156FlashBorrower.onFlashLoan" + * @return The keccak256 hash of "ERC3156FlashBorrower.onFlashLoan" */ function onFlashLoan( address initiator, From ead3bcaccbc0ae02f0aaa1b294b432ac55f758cf Mon Sep 17 00:00:00 2001 From: Francisco Date: Tue, 4 Apr 2023 23:05:39 -0300 Subject: [PATCH 090/197] Fix spurious CI check failures (#4160) --- .github/workflows/checks.yml | 2 ++ foundry.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index c37bf935b..290c6a943 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -83,6 +83,8 @@ jobs: uses: ./.github/actions/setup - run: rm foundry.toml - uses: crytic/slither-action@v0.3.0 + with: + node-version: 18 codespell: if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' diff --git a/foundry.toml b/foundry.toml index c0da48773..d0aa4ea39 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,3 +1,3 @@ [fuzz] runs = 10000 -max_test_rejects = 100000 +max_test_rejects = 150000 From 5523c1482bd0a503e4c4dfe821bd2f6b523f3c86 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 5 Apr 2023 16:57:08 +0200 Subject: [PATCH 091/197] Fix TransparentUpgradeableProxy's transparency (#4154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Francisco Co-authored-by: Ernesto García --- .changeset/thirty-shrimps-mix.md | 5 + contracts/interfaces/IERC1967.sol | 25 +++++ contracts/proxy/ERC1967/ERC1967Upgrade.sol | 18 +-- contracts/proxy/transparent/ProxyAdmin.sol | 10 +- .../TransparentUpgradeableProxy.sol | 103 +++++++++++++----- test/proxy/transparent/ProxyAdmin.test.js | 4 +- .../TransparentUpgradeableProxy.behaviour.js | 22 ++-- .../TransparentUpgradeableProxy.test.js | 4 +- 8 files changed, 132 insertions(+), 59 deletions(-) create mode 100644 .changeset/thirty-shrimps-mix.md create mode 100644 contracts/interfaces/IERC1967.sol diff --git a/.changeset/thirty-shrimps-mix.md b/.changeset/thirty-shrimps-mix.md new file mode 100644 index 000000000..54256d52c --- /dev/null +++ b/.changeset/thirty-shrimps-mix.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`TransparentUpgradeableProxy`: Fix transparency in case of selector clash with non-decodable calldata. diff --git a/contracts/interfaces/IERC1967.sol b/contracts/interfaces/IERC1967.sol new file mode 100644 index 000000000..e5deebee9 --- /dev/null +++ b/contracts/interfaces/IERC1967.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev ERC-1967: Proxy Storage Slots. This interface contains the events defined in the ERC. + * + * _Available since v4.9._ + */ +interface IERC1967 { + /** + * @dev Emitted when the implementation is upgraded. + */ + event Upgraded(address indexed implementation); + + /** + * @dev Emitted when the admin account has changed. + */ + event AdminChanged(address previousAdmin, address newAdmin); + + /** + * @dev Emitted when the beacon is changed. + */ + event BeaconUpgraded(address indexed beacon); +} diff --git a/contracts/proxy/ERC1967/ERC1967Upgrade.sol b/contracts/proxy/ERC1967/ERC1967Upgrade.sol index 0680f3549..a79c10502 100644 --- a/contracts/proxy/ERC1967/ERC1967Upgrade.sol +++ b/contracts/proxy/ERC1967/ERC1967Upgrade.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.2; import "../beacon/IBeacon.sol"; +import "../../interfaces/IERC1967.sol"; import "../../interfaces/draft-IERC1822.sol"; import "../../utils/Address.sol"; import "../../utils/StorageSlot.sol"; @@ -14,7 +15,7 @@ import "../../utils/StorageSlot.sol"; * * _Available since v4.1._ */ -abstract contract ERC1967Upgrade { +abstract contract ERC1967Upgrade is IERC1967 { // This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1 bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; @@ -25,11 +26,6 @@ abstract contract ERC1967Upgrade { */ bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - /** - * @dev Emitted when the implementation is upgraded. - */ - event Upgraded(address indexed implementation); - /** * @dev Returns the current implementation address. */ @@ -95,11 +91,6 @@ abstract contract ERC1967Upgrade { */ bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; - /** - * @dev Emitted when the admin account has changed. - */ - event AdminChanged(address previousAdmin, address newAdmin); - /** * @dev Returns the current admin. */ @@ -131,11 +122,6 @@ abstract contract ERC1967Upgrade { */ bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; - /** - * @dev Emitted when the beacon is upgraded. - */ - event BeaconUpgraded(address indexed beacon); - /** * @dev Returns the current beacon. */ diff --git a/contracts/proxy/transparent/ProxyAdmin.sol b/contracts/proxy/transparent/ProxyAdmin.sol index 839534298..1a9698196 100644 --- a/contracts/proxy/transparent/ProxyAdmin.sol +++ b/contracts/proxy/transparent/ProxyAdmin.sol @@ -18,7 +18,7 @@ contract ProxyAdmin is Ownable { * * - This contract must be the admin of `proxy`. */ - function getProxyImplementation(TransparentUpgradeableProxy proxy) public view virtual returns (address) { + function getProxyImplementation(ITransparentUpgradeableProxy proxy) public view virtual returns (address) { // We need to manually run the static call since the getter cannot be flagged as view // bytes4(keccak256("implementation()")) == 0x5c60da1b (bool success, bytes memory returndata) = address(proxy).staticcall(hex"5c60da1b"); @@ -33,7 +33,7 @@ contract ProxyAdmin is Ownable { * * - This contract must be the admin of `proxy`. */ - function getProxyAdmin(TransparentUpgradeableProxy proxy) public view virtual returns (address) { + function getProxyAdmin(ITransparentUpgradeableProxy proxy) public view virtual returns (address) { // We need to manually run the static call since the getter cannot be flagged as view // bytes4(keccak256("admin()")) == 0xf851a440 (bool success, bytes memory returndata) = address(proxy).staticcall(hex"f851a440"); @@ -48,7 +48,7 @@ contract ProxyAdmin is Ownable { * * - This contract must be the current admin of `proxy`. */ - function changeProxyAdmin(TransparentUpgradeableProxy proxy, address newAdmin) public virtual onlyOwner { + function changeProxyAdmin(ITransparentUpgradeableProxy proxy, address newAdmin) public virtual onlyOwner { proxy.changeAdmin(newAdmin); } @@ -59,7 +59,7 @@ contract ProxyAdmin is Ownable { * * - This contract must be the admin of `proxy`. */ - function upgrade(TransparentUpgradeableProxy proxy, address implementation) public virtual onlyOwner { + function upgrade(ITransparentUpgradeableProxy proxy, address implementation) public virtual onlyOwner { proxy.upgradeTo(implementation); } @@ -72,7 +72,7 @@ contract ProxyAdmin is Ownable { * - This contract must be the admin of `proxy`. */ function upgradeAndCall( - TransparentUpgradeableProxy proxy, + ITransparentUpgradeableProxy proxy, address implementation, bytes memory data ) public payable virtual onlyOwner { diff --git a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol index 155a22e01..a89b233f2 100644 --- a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol @@ -5,6 +5,24 @@ pragma solidity ^0.8.0; import "../ERC1967/ERC1967Proxy.sol"; +/** + * @dev Interface for the {TransparentUpgradeableProxy}. This is useful because {TransparentUpgradeableProxy} uses a + * custom call-routing mechanism, the compiler is unaware of the functions being exposed, and cannot list them. Also + * {TransparentUpgradeableProxy} does not inherit from this interface because it's implemented in a way that the + * compiler doesn't understand and cannot verify. + */ +interface ITransparentUpgradeableProxy is IERC1967 { + function admin() external view returns (address); + + function implementation() external view returns (address); + + function changeAdmin(address) external; + + function upgradeTo(address) external; + + function upgradeToAndCall(address, bytes memory) external payable; +} + /** * @dev This contract implements a proxy that is upgradeable by an admin. * @@ -25,6 +43,13 @@ import "../ERC1967/ERC1967Proxy.sol"; * * Our recommendation is for the dedicated account to be an instance of the {ProxyAdmin} contract. If set up this way, * you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy. + * + * WARNING: This contract does not inherit from {ITransparentUpgradeableProxy}, and the admin function is implicitly + * implemented using a custom call-routing mechanism in `_fallback`. Consequently, the compiler will not produce an + * ABI for this contract. Also, if you inherit from this contract and add additional functions, the compiler will not + * check that there are no selector conflicts. A selector clash between any new function and the functions declared in + * {ITransparentUpgradeableProxy} will be resolved in favor of the new one. This could render the admin operations + * inaccessible, which could prevent upgradeability. */ contract TransparentUpgradeableProxy is ERC1967Proxy { /** @@ -37,6 +62,9 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { /** * @dev Modifier used internally that will delegate the call to the implementation unless the sender is the admin. + * + * CAUTION: This modifier is deprecated, as it could cause issues if the modified function has arguments, and the + * implementation provides a function with the same selector. */ modifier ifAdmin() { if (msg.sender == _getAdmin()) { @@ -46,65 +74,98 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { } } + /** + * @dev If caller is the admin process the call internally, otherwise transparently fallback to the proxy behavior + */ + function _fallback() internal virtual override { + if (msg.sender == _getAdmin()) { + bytes memory ret; + bytes4 selector = msg.sig; + if (selector == ITransparentUpgradeableProxy.upgradeTo.selector) { + ret = _dispatchUpgradeTo(); + } else if (selector == ITransparentUpgradeableProxy.upgradeToAndCall.selector) { + ret = _dispatchUpgradeToAndCall(); + } else if (selector == ITransparentUpgradeableProxy.changeAdmin.selector) { + ret = _dispatchChangeAdmin(); + } else if (selector == ITransparentUpgradeableProxy.admin.selector) { + ret = _dispatchAdmin(); + } else if (selector == ITransparentUpgradeableProxy.implementation.selector) { + ret = _dispatchImplementation(); + } else { + revert("TransparentUpgradeableProxy: admin cannot fallback to proxy target"); + } + assembly { + return(add(ret, 0x20), mload(ret)) + } + } else { + super._fallback(); + } + } + /** * @dev Returns the current admin. * - * NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyAdmin}. - * * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` */ - function admin() external payable ifAdmin returns (address admin_) { + function _dispatchAdmin() private returns (bytes memory) { _requireZeroValue(); - admin_ = _getAdmin(); + + address admin = _getAdmin(); + return abi.encode(admin); } /** * @dev Returns the current implementation. * - * NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyImplementation}. - * * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` */ - function implementation() external payable ifAdmin returns (address implementation_) { + function _dispatchImplementation() private returns (bytes memory) { _requireZeroValue(); - implementation_ = _implementation(); + + address implementation = _implementation(); + return abi.encode(implementation); } /** * @dev Changes the admin of the proxy. * * Emits an {AdminChanged} event. - * - * NOTE: Only the admin can call this function. See {ProxyAdmin-changeProxyAdmin}. */ - function changeAdmin(address newAdmin) external payable virtual ifAdmin { + function _dispatchChangeAdmin() private returns (bytes memory) { _requireZeroValue(); + + address newAdmin = abi.decode(msg.data[4:], (address)); _changeAdmin(newAdmin); + + return ""; } /** * @dev Upgrade the implementation of the proxy. - * - * NOTE: Only the admin can call this function. See {ProxyAdmin-upgrade}. */ - function upgradeTo(address newImplementation) external payable ifAdmin { + function _dispatchUpgradeTo() private returns (bytes memory) { _requireZeroValue(); + + address newImplementation = abi.decode(msg.data[4:], (address)); _upgradeToAndCall(newImplementation, bytes(""), false); + + return ""; } /** * @dev Upgrade the implementation of the proxy, and then call a function from the new implementation as specified * by `data`, which should be an encoded function call. This is useful to initialize new storage variables in the * proxied contract. - * - * NOTE: Only the admin can call this function. See {ProxyAdmin-upgradeAndCall}. */ - function upgradeToAndCall(address newImplementation, bytes calldata data) external payable ifAdmin { + function _dispatchUpgradeToAndCall() private returns (bytes memory) { + (address newImplementation, bytes memory data) = abi.decode(msg.data[4:], (address, bytes)); _upgradeToAndCall(newImplementation, data, true); + + return ""; } /** @@ -114,14 +175,6 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { return _getAdmin(); } - /** - * @dev Makes sure the admin cannot access the fallback function. See {Proxy-_beforeFallback}. - */ - function _beforeFallback() internal virtual override { - require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target"); - super._beforeFallback(); - } - /** * @dev To keep this contract fully transparent, all `ifAdmin` functions must be payable. This helper is here to * emulate some proxy functions being non-payable while still allowing value to pass through. diff --git a/test/proxy/transparent/ProxyAdmin.test.js b/test/proxy/transparent/ProxyAdmin.test.js index 811dc5671..6d473d893 100644 --- a/test/proxy/transparent/ProxyAdmin.test.js +++ b/test/proxy/transparent/ProxyAdmin.test.js @@ -6,6 +6,7 @@ const ImplV1 = artifacts.require('DummyImplementation'); const ImplV2 = artifacts.require('DummyImplementationV2'); const ProxyAdmin = artifacts.require('ProxyAdmin'); const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy'); +const ITransparentUpgradeableProxy = artifacts.require('ITransparentUpgradeableProxy'); contract('ProxyAdmin', function (accounts) { const [proxyAdminOwner, newAdmin, anotherAccount] = accounts; @@ -18,12 +19,13 @@ contract('ProxyAdmin', function (accounts) { beforeEach(async function () { const initializeData = Buffer.from(''); this.proxyAdmin = await ProxyAdmin.new({ from: proxyAdminOwner }); - this.proxy = await TransparentUpgradeableProxy.new( + const proxy = await TransparentUpgradeableProxy.new( this.implementationV1.address, this.proxyAdmin.address, initializeData, { from: proxyAdminOwner }, ); + this.proxy = await ITransparentUpgradeableProxy.at(proxy.address); }); it('has an owner', async function () { diff --git a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js index 3a10357a9..66c0a406c 100644 --- a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js +++ b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js @@ -34,7 +34,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx describe('implementation', function () { it('returns the current implementation address', async function () { - const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + const implementation = await this.proxy.implementation({ from: proxyAdminAddress }); expect(implementation).to.be.equal(this.implementationV0); }); @@ -55,7 +55,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx it('upgrades to the requested implementation', async function () { await this.proxy.upgradeTo(this.implementationV1, { from }); - const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + const implementation = await this.proxy.implementation({ from: proxyAdminAddress }); expect(implementation).to.be.equal(this.implementationV1); }); @@ -103,7 +103,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx }); it('upgrades to the requested implementation', async function () { - const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + const implementation = await this.proxy.implementation({ from: proxyAdminAddress }); expect(implementation).to.be.equal(this.behavior.address); }); @@ -168,7 +168,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx }); it('upgrades to the requested version and emits an event', async function () { - const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + const implementation = await this.proxy.implementation({ from: proxyAdminAddress }); expect(implementation).to.be.equal(this.behaviorV1.address); expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV1.address }); }); @@ -196,7 +196,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx }); it('upgrades to the requested version and emits an event', async function () { - const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + const implementation = await this.proxy.implementation({ from: proxyAdminAddress }); expect(implementation).to.be.equal(this.behaviorV2.address); expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV2.address }); }); @@ -227,7 +227,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx }); it('upgrades to the requested version and emits an event', async function () { - const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + const implementation = await this.proxy.implementation({ from: proxyAdminAddress }); expect(implementation).to.be.equal(this.behaviorV3.address); expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV3.address }); }); @@ -271,7 +271,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx }); it('assigns new proxy admin', async function () { - const newProxyAdmin = await this.proxy.admin.call({ from: newAdmin }); + const newProxyAdmin = await this.proxy.admin({ from: newAdmin }); expect(newProxyAdmin).to.be.equal(anotherAccount); }); @@ -332,21 +332,21 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx describe('when function names clash', function () { it('when sender is proxy admin should run the proxy function', async function () { - const value = await this.proxy.admin.call({ from: proxyAdminAddress, value: 0 }); + const value = await this.proxy.admin({ from: proxyAdminAddress, value: 0 }); expect(value).to.be.equal(proxyAdminAddress); }); it('when sender is other should delegate to implementation', async function () { - const value = await this.proxy.admin.call({ from: anotherAccount, value: 0 }); + const value = await this.proxy.admin({ from: anotherAccount, value: 0 }); expect(value).to.be.equal('0x0000000000000000000000000000000011111142'); }); it('when sender is proxy admin value should not be accepted', async function () { - await expectRevert.unspecified(this.proxy.admin.call({ from: proxyAdminAddress, value: 1 })); + await expectRevert.unspecified(this.proxy.admin({ from: proxyAdminAddress, value: 1 })); }); it('when sender is other value should be accepted', async function () { - const value = await this.proxy.admin.call({ from: anotherAccount, value: 1 }); + const value = await this.proxy.admin({ from: anotherAccount, value: 1 }); expect(value).to.be.equal('0x0000000000000000000000000000000011111142'); }); }); diff --git a/test/proxy/transparent/TransparentUpgradeableProxy.test.js b/test/proxy/transparent/TransparentUpgradeableProxy.test.js index 86dd55d32..d60a31a21 100644 --- a/test/proxy/transparent/TransparentUpgradeableProxy.test.js +++ b/test/proxy/transparent/TransparentUpgradeableProxy.test.js @@ -2,12 +2,14 @@ const shouldBehaveLikeProxy = require('../Proxy.behaviour'); const shouldBehaveLikeTransparentUpgradeableProxy = require('./TransparentUpgradeableProxy.behaviour'); const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy'); +const ITransparentUpgradeableProxy = artifacts.require('ITransparentUpgradeableProxy'); contract('TransparentUpgradeableProxy', function (accounts) { const [proxyAdminAddress, proxyAdminOwner] = accounts; const createProxy = async function (logic, admin, initData, opts) { - return TransparentUpgradeableProxy.new(logic, admin, initData, opts); + const { address } = await TransparentUpgradeableProxy.new(logic, admin, initData, opts); + return ITransparentUpgradeableProxy.at(address); }; shouldBehaveLikeProxy(createProxy, proxyAdminAddress, proxyAdminOwner); From 31723ed608878c2e30710a9c46b51d529c0ba958 Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Wed, 5 Apr 2023 15:47:18 -0300 Subject: [PATCH 092/197] Reenable skipped TransparentUpgradeableProxy test (#4161) Co-authored-by: Francisco --- .../TransparentUpgradeableProxy.behaviour.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js index 66c0a406c..5e3f561d5 100644 --- a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js +++ b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js @@ -3,8 +3,8 @@ const { ZERO_ADDRESS } = constants; const { getSlot, ImplementationSlot, AdminSlot } = require('../../helpers/erc1967'); const { expect } = require('chai'); +const { web3 } = require('hardhat'); -const Proxy = artifacts.require('Proxy'); const Implementation1 = artifacts.require('Implementation1'); const Implementation2 = artifacts.require('Implementation2'); const Implementation3 = artifacts.require('Implementation3'); @@ -122,13 +122,11 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx expect(balance.toString()).to.be.bignumber.equal(value.toString()); }); - it.skip('uses the storage of the proxy', async function () { + it('uses the storage of the proxy', async function () { // storage layout should look as follows: - // - 0: Initializable storage - // - 1-50: Initailizable reserved storage (50 slots) - // - 51: initializerRan - // - 52: x - const storedValue = await Proxy.at(this.proxyAddress).getStorageAt(52); + // - 0: Initializable storage ++ initializerRan ++ onlyInitializingRan + // - 1: x + const storedValue = await web3.eth.getStorageAt(this.proxyAddress, 1); expect(parseInt(storedValue)).to.eq(42); }); }); From cf86fd9962701396457e50ab0d6cc78aa29a5ebc Mon Sep 17 00:00:00 2001 From: Francisco Date: Wed, 5 Apr 2023 17:20:34 -0300 Subject: [PATCH 093/197] Merge changesets for transparency improvements (#4165) --- .changeset/many-panthers-hide.md | 5 ----- .changeset/thirty-shrimps-mix.md | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 .changeset/many-panthers-hide.md diff --git a/.changeset/many-panthers-hide.md b/.changeset/many-panthers-hide.md deleted file mode 100644 index 5f04c99df..000000000 --- a/.changeset/many-panthers-hide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`TransparentUpgradeableProxy`: support value passthrough for all ifAdmin function. diff --git a/.changeset/thirty-shrimps-mix.md b/.changeset/thirty-shrimps-mix.md index 54256d52c..656edbf92 100644 --- a/.changeset/thirty-shrimps-mix.md +++ b/.changeset/thirty-shrimps-mix.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': patch --- -`TransparentUpgradeableProxy`: Fix transparency in case of selector clash with non-decodable calldata. +`TransparentUpgradeableProxy`: Fix transparency in case of selector clash with non-decodable calldata or payable mutability. From f2346b6749546707af883e2c0acbc8a487e16ae4 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 11 Apr 2023 11:21:53 +0200 Subject: [PATCH 094/197] Add fuzz tests for the Checkpoints library (#4146) Co-authored-by: Francisco --- scripts/generate/run.js | 28 +- scripts/generate/templates/Checkpoints.js | 23 +- .../generate/templates/Checkpoints.opts.js | 22 ++ scripts/generate/templates/Checkpoints.t.js | 252 +++++++++++++ test/utils/Checkpoints.t.sol | 341 ++++++++++++++++++ 5 files changed, 636 insertions(+), 30 deletions(-) create mode 100644 scripts/generate/templates/Checkpoints.opts.js create mode 100644 scripts/generate/templates/Checkpoints.t.js create mode 100644 test/utils/Checkpoints.t.sol diff --git a/scripts/generate/run.js b/scripts/generate/run.js index e68681e9d..368b53ad1 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -13,16 +13,10 @@ function getVersion(path) { } } -for (const [file, template] of Object.entries({ - 'utils/math/SafeCast.sol': './templates/SafeCast.js', - 'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js', - 'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js', - 'utils/Checkpoints.sol': './templates/Checkpoints.js', - 'utils/StorageSlot.sol': './templates/StorageSlot.js', -})) { +function generateFromTemplate(file, template, outputPrefix = '') { const script = path.relative(path.join(__dirname, '../..'), __filename); const input = path.join(path.dirname(script), template); - const output = `./contracts/${file}`; + const output = path.join(outputPrefix, file); const version = getVersion(output); const content = format( '// SPDX-License-Identifier: MIT', @@ -35,3 +29,21 @@ for (const [file, template] of Object.entries({ fs.writeFileSync(output, content); cp.execFileSync('prettier', ['--write', output]); } + +// Contracts +for (const [file, template] of Object.entries({ + 'utils/math/SafeCast.sol': './templates/SafeCast.js', + 'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js', + 'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js', + 'utils/Checkpoints.sol': './templates/Checkpoints.js', + 'utils/StorageSlot.sol': './templates/StorageSlot.js', +})) { + generateFromTemplate(file, template, './contracts/'); +} + +// Tests +for (const [file, template] of Object.entries({ + 'utils/Checkpoints.t.sol': './templates/Checkpoints.t.js', +})) { + generateFromTemplate(file, template, './test/'); +} diff --git a/scripts/generate/templates/Checkpoints.js b/scripts/generate/templates/Checkpoints.js index e51e8b8d1..18999682d 100644 --- a/scripts/generate/templates/Checkpoints.js +++ b/scripts/generate/templates/Checkpoints.js @@ -1,26 +1,5 @@ const format = require('../format-lines'); - -// OPTIONS -const defaultOpts = size => ({ - historyTypeName: `Trace${size}`, - checkpointTypeName: `Checkpoint${size}`, - checkpointFieldName: '_checkpoints', - keyTypeName: `uint${256 - size}`, - keyFieldName: '_key', - valueTypeName: `uint${size}`, - valueFieldName: '_value', -}); - -const VALUE_SIZES = [224, 160]; - -const OPTS = VALUE_SIZES.map(size => defaultOpts(size)); - -const LEGACY_OPTS = { - ...defaultOpts(224), - historyTypeName: 'History', - checkpointTypeName: 'Checkpoint', - keyFieldName: '_blockNumber', -}; +const { OPTS, LEGACY_OPTS } = require('./Checkpoints.opts.js'); // TEMPLATE const header = `\ diff --git a/scripts/generate/templates/Checkpoints.opts.js b/scripts/generate/templates/Checkpoints.opts.js new file mode 100644 index 000000000..03c5a9569 --- /dev/null +++ b/scripts/generate/templates/Checkpoints.opts.js @@ -0,0 +1,22 @@ +// OPTIONS +const VALUE_SIZES = [224, 160]; + +const defaultOpts = size => ({ + historyTypeName: `Trace${size}`, + checkpointTypeName: `Checkpoint${size}`, + checkpointFieldName: '_checkpoints', + keyTypeName: `uint${256 - size}`, + keyFieldName: '_key', + valueTypeName: `uint${size}`, + valueFieldName: '_value', +}); + +module.exports = { + OPTS: VALUE_SIZES.map(size => defaultOpts(size)), + LEGACY_OPTS: { + ...defaultOpts(224), + historyTypeName: 'History', + checkpointTypeName: 'Checkpoint', + keyFieldName: '_blockNumber', + }, +}; diff --git a/scripts/generate/templates/Checkpoints.t.js b/scripts/generate/templates/Checkpoints.t.js new file mode 100644 index 000000000..84b5992ad --- /dev/null +++ b/scripts/generate/templates/Checkpoints.t.js @@ -0,0 +1,252 @@ +const format = require('../format-lines'); +const { capitalize } = require('../../helpers'); +const { OPTS, LEGACY_OPTS } = require('./Checkpoints.opts.js'); + +// TEMPLATE +const header = `\ +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../../contracts/utils/Checkpoints.sol"; +import "../../contracts/utils/math/SafeCast.sol"; +`; + +/* eslint-disable max-len */ +const common = opts => `\ +using Checkpoints for Checkpoints.${opts.historyTypeName}; + +// Maximum gap between keys used during the fuzzing tests: the \`_prepareKeys\` function with make sure that +// key#n+1 is in the [key#n, key#n + _KEY_MAX_GAP] range. +uint8 internal constant _KEY_MAX_GAP = 64; + +Checkpoints.${opts.historyTypeName} internal _ckpts; + +// helpers +function _bound${capitalize(opts.keyTypeName)}( + ${opts.keyTypeName} x, + ${opts.keyTypeName} min, + ${opts.keyTypeName} max +) internal view returns (${opts.keyTypeName}) { + return SafeCast.to${capitalize(opts.keyTypeName)}(bound(uint256(x), uint256(min), uint256(max))); +} + +function _prepareKeys( + ${opts.keyTypeName}[] memory keys, + ${opts.keyTypeName} maxSpread +) internal view { + ${opts.keyTypeName} lastKey = 0; + for (uint256 i = 0; i < keys.length; ++i) { + ${opts.keyTypeName} key = _bound${capitalize(opts.keyTypeName)}(keys[i], lastKey, lastKey + maxSpread); + keys[i] = key; + lastKey = key; + } +} + +function _assertLatestCheckpoint( + bool exist, + ${opts.keyTypeName} key, + ${opts.valueTypeName} value +) internal { + (bool _exist, ${opts.keyTypeName} _key, ${opts.valueTypeName} _value) = _ckpts.latestCheckpoint(); + assertEq(_exist, exist); + assertEq(_key, key); + assertEq(_value, value); +} +`; + +const testTrace = opts => `\ +// tests +function testPush( + ${opts.keyTypeName}[] memory keys, + ${opts.valueTypeName}[] memory values, + ${opts.keyTypeName} pastKey +) public { + vm.assume(values.length > 0 && values.length <= keys.length); + _prepareKeys(keys, _KEY_MAX_GAP); + + // initial state + assertEq(_ckpts.length(), 0); + assertEq(_ckpts.latest(), 0); + _assertLatestCheckpoint(false, 0, 0); + + uint256 duplicates = 0; + for (uint256 i = 0; i < keys.length; ++i) { + ${opts.keyTypeName} key = keys[i]; + ${opts.valueTypeName} value = values[i % values.length]; + if (i > 0 && key == keys[i-1]) ++duplicates; + + // push + _ckpts.push(key, value); + + // check length & latest + assertEq(_ckpts.length(), i + 1 - duplicates); + assertEq(_ckpts.latest(), value); + _assertLatestCheckpoint(true, key, value); + } + + if (keys.length > 0) { + ${opts.keyTypeName} lastKey = keys[keys.length - 1]; + pastKey = _bound${capitalize(opts.keyTypeName)}(pastKey, 0, lastKey - 1); + + vm.expectRevert(); + this.push(pastKey, values[keys.length % values.length]); + } +} + +// used to test reverts +function push(${opts.keyTypeName} key, ${opts.valueTypeName} value) external { + _ckpts.push(key, value); +} + +function testLookup( + ${opts.keyTypeName}[] memory keys, + ${opts.valueTypeName}[] memory values, + ${opts.keyTypeName} lookup +) public { + vm.assume(values.length > 0 && values.length <= keys.length); + _prepareKeys(keys, _KEY_MAX_GAP); + + ${opts.keyTypeName} lastKey = keys.length == 0 ? 0 : keys[keys.length - 1]; + lookup = _bound${capitalize(opts.keyTypeName)}(lookup, 0, lastKey + _KEY_MAX_GAP); + + ${opts.valueTypeName} upper = 0; + ${opts.valueTypeName} lower = 0; + ${opts.keyTypeName} lowerKey = type(${opts.keyTypeName}).max; + for (uint256 i = 0; i < keys.length; ++i) { + ${opts.keyTypeName} key = keys[i]; + ${opts.valueTypeName} value = values[i % values.length]; + + // push + _ckpts.push(key, value); + + // track expected result of lookups + if (key <= lookup) { + upper = value; + } + // find the first key that is not smaller than the lookup key + if (key >= lookup && (i == 0 || keys[i-1] < lookup)) { + lowerKey = key; + } + if (key == lowerKey) { + lower = value; + } + } + + // check lookup + assertEq(_ckpts.lowerLookup(lookup), lower); + assertEq(_ckpts.upperLookup(lookup), upper); + assertEq(_ckpts.upperLookupRecent(lookup), upper); +} +`; + +const testHistory = opts => `\ +// tests +function testPush( + ${opts.keyTypeName}[] memory keys, + ${opts.valueTypeName}[] memory values, + ${opts.keyTypeName} pastKey +) public { + vm.assume(values.length > 0 && values.length <= keys.length); + _prepareKeys(keys, _KEY_MAX_GAP); + + // initial state + assertEq(_ckpts.length(), 0); + assertEq(_ckpts.latest(), 0); + _assertLatestCheckpoint(false, 0, 0); + + uint256 duplicates = 0; + for (uint256 i = 0; i < keys.length; ++i) { + ${opts.keyTypeName} key = keys[i]; + ${opts.valueTypeName} value = values[i % values.length]; + if (i > 0 && key == keys[i - 1]) ++duplicates; + + // push + vm.roll(key); + _ckpts.push(value); + + // check length & latest + assertEq(_ckpts.length(), i + 1 - duplicates); + assertEq(_ckpts.latest(), value); + _assertLatestCheckpoint(true, key, value); + } + + // Can't push any key in the past + if (keys.length > 0) { + ${opts.keyTypeName} lastKey = keys[keys.length - 1]; + pastKey = _bound${capitalize(opts.keyTypeName)}(pastKey, 0, lastKey - 1); + + vm.roll(pastKey); + vm.expectRevert(); + this.push(values[keys.length % values.length]); + } +} + +// used to test reverts +function push(${opts.valueTypeName} value) external { + _ckpts.push(value); +} + +function testLookup( + ${opts.keyTypeName}[] memory keys, + ${opts.valueTypeName}[] memory values, + ${opts.keyTypeName} lookup +) public { + vm.assume(keys.length > 0); + vm.assume(values.length > 0 && values.length <= keys.length); + _prepareKeys(keys, _KEY_MAX_GAP); + + ${opts.keyTypeName} lastKey = keys[keys.length - 1]; + vm.assume(lastKey > 0); + lookup = _bound${capitalize(opts.keyTypeName)}(lookup, 0, lastKey - 1); + + ${opts.valueTypeName} upper = 0; + for (uint256 i = 0; i < keys.length; ++i) { + ${opts.keyTypeName} key = keys[i]; + ${opts.valueTypeName} value = values[i % values.length]; + + // push + vm.roll(key); + _ckpts.push(value); + + // track expected result of lookups + if (key <= lookup) { + upper = value; + } + } + + // check lookup + assertEq(_ckpts.getAtBlock(lookup), upper); + assertEq(_ckpts.getAtProbablyRecentBlock(lookup), upper); + + vm.expectRevert(); this.getAtBlock(lastKey); + vm.expectRevert(); this.getAtBlock(lastKey + 1); + vm.expectRevert(); this.getAtProbablyRecentBlock(lastKey); + vm.expectRevert(); this.getAtProbablyRecentBlock(lastKey + 1); +} + +// used to test reverts +function getAtBlock(${opts.keyTypeName} key) external view { + _ckpts.getAtBlock(key); +} + +// used to test reverts +function getAtProbablyRecentBlock(${opts.keyTypeName} key) external view { + _ckpts.getAtProbablyRecentBlock(key); +} +`; +/* eslint-enable max-len */ + +// GENERATE +module.exports = format( + header, + // HISTORY + `contract Checkpoints${LEGACY_OPTS.historyTypeName}Test is Test {`, + [common(LEGACY_OPTS), testHistory(LEGACY_OPTS)], + '}', + // TRACEXXX + ...OPTS.flatMap(opts => [ + `contract Checkpoints${opts.historyTypeName}Test is Test {`, + [common(opts), testTrace(opts)], + '}', + ]), +); diff --git a/test/utils/Checkpoints.t.sol b/test/utils/Checkpoints.t.sol new file mode 100644 index 000000000..abdf8b436 --- /dev/null +++ b/test/utils/Checkpoints.t.sol @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: MIT +// This file was procedurally generated from scripts/generate/templates/Checkpoints.t.js. + +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../../contracts/utils/Checkpoints.sol"; +import "../../contracts/utils/math/SafeCast.sol"; + +contract CheckpointsHistoryTest is Test { + using Checkpoints for Checkpoints.History; + + // Maximum gap between keys used during the fuzzing tests: the `_prepareKeys` function with make sure that + // key#n+1 is in the [key#n, key#n + _KEY_MAX_GAP] range. + uint8 internal constant _KEY_MAX_GAP = 64; + + Checkpoints.History internal _ckpts; + + // helpers + function _boundUint32(uint32 x, uint32 min, uint32 max) internal view returns (uint32) { + return SafeCast.toUint32(bound(uint256(x), uint256(min), uint256(max))); + } + + function _prepareKeys(uint32[] memory keys, uint32 maxSpread) internal view { + uint32 lastKey = 0; + for (uint256 i = 0; i < keys.length; ++i) { + uint32 key = _boundUint32(keys[i], lastKey, lastKey + maxSpread); + keys[i] = key; + lastKey = key; + } + } + + function _assertLatestCheckpoint(bool exist, uint32 key, uint224 value) internal { + (bool _exist, uint32 _key, uint224 _value) = _ckpts.latestCheckpoint(); + assertEq(_exist, exist); + assertEq(_key, key); + assertEq(_value, value); + } + + // tests + function testPush(uint32[] memory keys, uint224[] memory values, uint32 pastKey) public { + vm.assume(values.length > 0 && values.length <= keys.length); + _prepareKeys(keys, _KEY_MAX_GAP); + + // initial state + assertEq(_ckpts.length(), 0); + assertEq(_ckpts.latest(), 0); + _assertLatestCheckpoint(false, 0, 0); + + uint256 duplicates = 0; + for (uint256 i = 0; i < keys.length; ++i) { + uint32 key = keys[i]; + uint224 value = values[i % values.length]; + if (i > 0 && key == keys[i - 1]) ++duplicates; + + // push + vm.roll(key); + _ckpts.push(value); + + // check length & latest + assertEq(_ckpts.length(), i + 1 - duplicates); + assertEq(_ckpts.latest(), value); + _assertLatestCheckpoint(true, key, value); + } + + // Can't push any key in the past + if (keys.length > 0) { + uint32 lastKey = keys[keys.length - 1]; + pastKey = _boundUint32(pastKey, 0, lastKey - 1); + + vm.roll(pastKey); + vm.expectRevert(); + this.push(values[keys.length % values.length]); + } + } + + // used to test reverts + function push(uint224 value) external { + _ckpts.push(value); + } + + function testLookup(uint32[] memory keys, uint224[] memory values, uint32 lookup) public { + vm.assume(keys.length > 0); + vm.assume(values.length > 0 && values.length <= keys.length); + _prepareKeys(keys, _KEY_MAX_GAP); + + uint32 lastKey = keys[keys.length - 1]; + vm.assume(lastKey > 0); + lookup = _boundUint32(lookup, 0, lastKey - 1); + + uint224 upper = 0; + for (uint256 i = 0; i < keys.length; ++i) { + uint32 key = keys[i]; + uint224 value = values[i % values.length]; + + // push + vm.roll(key); + _ckpts.push(value); + + // track expected result of lookups + if (key <= lookup) { + upper = value; + } + } + + // check lookup + assertEq(_ckpts.getAtBlock(lookup), upper); + assertEq(_ckpts.getAtProbablyRecentBlock(lookup), upper); + + vm.expectRevert(); + this.getAtBlock(lastKey); + vm.expectRevert(); + this.getAtBlock(lastKey + 1); + vm.expectRevert(); + this.getAtProbablyRecentBlock(lastKey); + vm.expectRevert(); + this.getAtProbablyRecentBlock(lastKey + 1); + } + + // used to test reverts + function getAtBlock(uint32 key) external view { + _ckpts.getAtBlock(key); + } + + // used to test reverts + function getAtProbablyRecentBlock(uint32 key) external view { + _ckpts.getAtProbablyRecentBlock(key); + } +} + +contract CheckpointsTrace224Test is Test { + using Checkpoints for Checkpoints.Trace224; + + // Maximum gap between keys used during the fuzzing tests: the `_prepareKeys` function with make sure that + // key#n+1 is in the [key#n, key#n + _KEY_MAX_GAP] range. + uint8 internal constant _KEY_MAX_GAP = 64; + + Checkpoints.Trace224 internal _ckpts; + + // helpers + function _boundUint32(uint32 x, uint32 min, uint32 max) internal view returns (uint32) { + return SafeCast.toUint32(bound(uint256(x), uint256(min), uint256(max))); + } + + function _prepareKeys(uint32[] memory keys, uint32 maxSpread) internal view { + uint32 lastKey = 0; + for (uint256 i = 0; i < keys.length; ++i) { + uint32 key = _boundUint32(keys[i], lastKey, lastKey + maxSpread); + keys[i] = key; + lastKey = key; + } + } + + function _assertLatestCheckpoint(bool exist, uint32 key, uint224 value) internal { + (bool _exist, uint32 _key, uint224 _value) = _ckpts.latestCheckpoint(); + assertEq(_exist, exist); + assertEq(_key, key); + assertEq(_value, value); + } + + // tests + function testPush(uint32[] memory keys, uint224[] memory values, uint32 pastKey) public { + vm.assume(values.length > 0 && values.length <= keys.length); + _prepareKeys(keys, _KEY_MAX_GAP); + + // initial state + assertEq(_ckpts.length(), 0); + assertEq(_ckpts.latest(), 0); + _assertLatestCheckpoint(false, 0, 0); + + uint256 duplicates = 0; + for (uint256 i = 0; i < keys.length; ++i) { + uint32 key = keys[i]; + uint224 value = values[i % values.length]; + if (i > 0 && key == keys[i - 1]) ++duplicates; + + // push + _ckpts.push(key, value); + + // check length & latest + assertEq(_ckpts.length(), i + 1 - duplicates); + assertEq(_ckpts.latest(), value); + _assertLatestCheckpoint(true, key, value); + } + + if (keys.length > 0) { + uint32 lastKey = keys[keys.length - 1]; + pastKey = _boundUint32(pastKey, 0, lastKey - 1); + + vm.expectRevert(); + this.push(pastKey, values[keys.length % values.length]); + } + } + + // used to test reverts + function push(uint32 key, uint224 value) external { + _ckpts.push(key, value); + } + + function testLookup(uint32[] memory keys, uint224[] memory values, uint32 lookup) public { + vm.assume(values.length > 0 && values.length <= keys.length); + _prepareKeys(keys, _KEY_MAX_GAP); + + uint32 lastKey = keys.length == 0 ? 0 : keys[keys.length - 1]; + lookup = _boundUint32(lookup, 0, lastKey + _KEY_MAX_GAP); + + uint224 upper = 0; + uint224 lower = 0; + uint32 lowerKey = type(uint32).max; + for (uint256 i = 0; i < keys.length; ++i) { + uint32 key = keys[i]; + uint224 value = values[i % values.length]; + + // push + _ckpts.push(key, value); + + // track expected result of lookups + if (key <= lookup) { + upper = value; + } + // find the first key that is not smaller than the lookup key + if (key >= lookup && (i == 0 || keys[i - 1] < lookup)) { + lowerKey = key; + } + if (key == lowerKey) { + lower = value; + } + } + + // check lookup + assertEq(_ckpts.lowerLookup(lookup), lower); + assertEq(_ckpts.upperLookup(lookup), upper); + assertEq(_ckpts.upperLookupRecent(lookup), upper); + } +} + +contract CheckpointsTrace160Test is Test { + using Checkpoints for Checkpoints.Trace160; + + // Maximum gap between keys used during the fuzzing tests: the `_prepareKeys` function with make sure that + // key#n+1 is in the [key#n, key#n + _KEY_MAX_GAP] range. + uint8 internal constant _KEY_MAX_GAP = 64; + + Checkpoints.Trace160 internal _ckpts; + + // helpers + function _boundUint96(uint96 x, uint96 min, uint96 max) internal view returns (uint96) { + return SafeCast.toUint96(bound(uint256(x), uint256(min), uint256(max))); + } + + function _prepareKeys(uint96[] memory keys, uint96 maxSpread) internal view { + uint96 lastKey = 0; + for (uint256 i = 0; i < keys.length; ++i) { + uint96 key = _boundUint96(keys[i], lastKey, lastKey + maxSpread); + keys[i] = key; + lastKey = key; + } + } + + function _assertLatestCheckpoint(bool exist, uint96 key, uint160 value) internal { + (bool _exist, uint96 _key, uint160 _value) = _ckpts.latestCheckpoint(); + assertEq(_exist, exist); + assertEq(_key, key); + assertEq(_value, value); + } + + // tests + function testPush(uint96[] memory keys, uint160[] memory values, uint96 pastKey) public { + vm.assume(values.length > 0 && values.length <= keys.length); + _prepareKeys(keys, _KEY_MAX_GAP); + + // initial state + assertEq(_ckpts.length(), 0); + assertEq(_ckpts.latest(), 0); + _assertLatestCheckpoint(false, 0, 0); + + uint256 duplicates = 0; + for (uint256 i = 0; i < keys.length; ++i) { + uint96 key = keys[i]; + uint160 value = values[i % values.length]; + if (i > 0 && key == keys[i - 1]) ++duplicates; + + // push + _ckpts.push(key, value); + + // check length & latest + assertEq(_ckpts.length(), i + 1 - duplicates); + assertEq(_ckpts.latest(), value); + _assertLatestCheckpoint(true, key, value); + } + + if (keys.length > 0) { + uint96 lastKey = keys[keys.length - 1]; + pastKey = _boundUint96(pastKey, 0, lastKey - 1); + + vm.expectRevert(); + this.push(pastKey, values[keys.length % values.length]); + } + } + + // used to test reverts + function push(uint96 key, uint160 value) external { + _ckpts.push(key, value); + } + + function testLookup(uint96[] memory keys, uint160[] memory values, uint96 lookup) public { + vm.assume(values.length > 0 && values.length <= keys.length); + _prepareKeys(keys, _KEY_MAX_GAP); + + uint96 lastKey = keys.length == 0 ? 0 : keys[keys.length - 1]; + lookup = _boundUint96(lookup, 0, lastKey + _KEY_MAX_GAP); + + uint160 upper = 0; + uint160 lower = 0; + uint96 lowerKey = type(uint96).max; + for (uint256 i = 0; i < keys.length; ++i) { + uint96 key = keys[i]; + uint160 value = values[i % values.length]; + + // push + _ckpts.push(key, value); + + // track expected result of lookups + if (key <= lookup) { + upper = value; + } + // find the first key that is not smaller than the lookup key + if (key >= lookup && (i == 0 || keys[i - 1] < lookup)) { + lowerKey = key; + } + if (key == lowerKey) { + lower = value; + } + } + + // check lookup + assertEq(_ckpts.lowerLookup(lookup), lower); + assertEq(_ckpts.upperLookup(lookup), upper); + assertEq(_ckpts.upperLookupRecent(lookup), upper); + } +} From 473d0b6884ce538f534a0fc04c97b78e8cc0f48b Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Tue, 11 Apr 2023 20:36:58 -0300 Subject: [PATCH 095/197] Add Codecov token --- .github/workflows/checks.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 290c6a943..c6b42dd82 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -73,6 +73,8 @@ jobs: env: NODE_OPTIONS: --max_old_space_size=4096 - uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} slither: if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' From 661343f74ca798c0ac5037411fe39d8ef15f3377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 12 Apr 2023 04:17:10 +0200 Subject: [PATCH 096/197] Add DoubleEndedQueue FV (#4147) Co-authored-by: Francisco Co-authored-by: Hadrien Croubois --- certora/harnesses/DoubleEndedQueueHarness.sol | 59 +++ certora/specs.json | 5 + certora/specs/DoubleEndedQueue.spec | 366 ++++++++++++++++++ 3 files changed, 430 insertions(+) create mode 100644 certora/harnesses/DoubleEndedQueueHarness.sol create mode 100644 certora/specs/DoubleEndedQueue.spec diff --git a/certora/harnesses/DoubleEndedQueueHarness.sol b/certora/harnesses/DoubleEndedQueueHarness.sol new file mode 100644 index 000000000..b4f0bc841 --- /dev/null +++ b/certora/harnesses/DoubleEndedQueueHarness.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../patched/utils/structs/DoubleEndedQueue.sol"; + +contract DoubleEndedQueueHarness { + using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; + + DoubleEndedQueue.Bytes32Deque private _deque; + + function pushFront(bytes32 value) external { + _deque.pushFront(value); + } + + function pushBack(bytes32 value) external { + _deque.pushBack(value); + } + + function popFront() external returns (bytes32 value) { + return _deque.popFront(); + } + + function popBack() external returns (bytes32 value) { + return _deque.popBack(); + } + + function clear() external { + _deque.clear(); + } + + function begin() external view returns (int128) { + return _deque._begin; + } + + function end() external view returns (int128) { + return _deque._end; + } + + function length() external view returns (uint256) { + return _deque.length(); + } + + function empty() external view returns (bool) { + return _deque.empty(); + } + + function front() external view returns (bytes32 value) { + return _deque.front(); + } + + function back() external view returns (bytes32 value) { + return _deque.back(); + } + + function at_(uint256 index) external view returns (bytes32 value) { + return _deque.at(index); + } +} diff --git a/certora/specs.json b/certora/specs.json index 80fbf26af..cd68af65e 100644 --- a/certora/specs.json +++ b/certora/specs.json @@ -9,6 +9,11 @@ "contract": "AccessControlHarness", "files": ["certora/harnesses/AccessControlHarness.sol"] }, + { + "spec": "DoubleEndedQueue", + "contract": "DoubleEndedQueueHarness", + "files": ["certora/harnesses/DoubleEndedQueueHarness.sol"] + }, { "spec": "Ownable", "contract": "OwnableHarness", diff --git a/certora/specs/DoubleEndedQueue.spec b/certora/specs/DoubleEndedQueue.spec new file mode 100644 index 000000000..2412b4cc8 --- /dev/null +++ b/certora/specs/DoubleEndedQueue.spec @@ -0,0 +1,366 @@ +import "helpers.spec" + +methods { + pushFront(bytes32) envfree + pushBack(bytes32) envfree + popFront() returns (bytes32) envfree + popBack() returns (bytes32) envfree + clear() envfree + + // exposed for FV + begin() returns (int128) envfree + end() returns (int128) envfree + + // view + length() returns (uint256) envfree + empty() returns (bool) envfree + front() returns (bytes32) envfree + back() returns (bytes32) envfree + at_(uint256) returns (bytes32) envfree // at is a reserved word +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Helpers │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ + +function min_int128() returns mathint { + return -(1 << 127); +} + +function max_int128() returns mathint { + return (1 << 127) - 1; +} + +// Could be broken in theory, but not in practice +function boundedQueue() returns bool { + return + max_int128() > to_mathint(end()) && + min_int128() < to_mathint(begin()); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: end is larger or equal than begin │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant boundariesConsistency() + end() >= begin() + filtered { f -> !f.isView } + { preserved { require boundedQueue(); } } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: length is end minus begin │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant lengthConsistency() + length() == to_mathint(end()) - to_mathint(begin()) + filtered { f -> !f.isView } + { preserved { require boundedQueue(); } } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: empty() is length 0 and no element exists │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant emptiness() + empty() <=> length() == 0 + filtered { f -> !f.isView } + { preserved { require boundedQueue(); } } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: front points to the first index and back points to the last one │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant queueEndings() + at_(length() - 1) == back() && at_(0) == front() + filtered { f -> !f.isView } + { + preserved { + requireInvariant boundariesConsistency(); + require boundedQueue(); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: pushFront adds an element at the beginning of the queue │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pushFront(bytes32 value) { + require boundedQueue(); + + uint256 lengthBefore = length(); + + pushFront@withrevert(value); + + // liveness + assert !lastReverted, "never reverts"; + + // effect + assert front() == value, "front set to value"; + assert length() == lengthBefore + 1, "queue extended"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: pushFront preserves the previous values in the queue with a +1 offset │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pushFrontConsistency(uint256 index) { + require boundedQueue(); + + bytes32 beforeAt = at_(index); + + bytes32 value; + pushFront(value); + + // try to read value + bytes32 afterAt = at_@withrevert(index + 1); + + assert !lastReverted, "value still there"; + assert afterAt == beforeAt, "data is preserved"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: pushBack adds an element at the end of the queue │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pushBack(bytes32 value) { + require boundedQueue(); + + uint256 lengthBefore = length(); + + pushBack@withrevert(value); + + // liveness + assert !lastReverted, "never reverts"; + + // effect + assert back() == value, "back set to value"; + assert length() == lengthBefore + 1, "queue increased"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: pushBack preserves the previous values in the queue │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pushBackConsistency(uint256 index) { + require boundedQueue(); + + bytes32 beforeAt = at_(index); + + bytes32 value; + pushBack(value); + + // try to read value + bytes32 afterAt = at_@withrevert(index); + + assert !lastReverted, "value still there"; + assert afterAt == beforeAt, "data is preserved"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: popFront removes an element from the beginning of the queue │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule popFront { + requireInvariant boundariesConsistency(); + require boundedQueue(); + + uint256 lengthBefore = length(); + bytes32 frontBefore = front@withrevert(); + + bytes32 popped = popFront@withrevert(); + bool success = !lastReverted; + + // liveness + assert success <=> lengthBefore != 0, "never reverts if not previously empty"; + + // effect + assert success => frontBefore == popped, "previous front is returned"; + assert success => length() == lengthBefore - 1, "queue decreased"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: at(x) is preserved and offset to at(x - 1) after calling popFront | +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule popFrontConsistency(uint256 index) { + requireInvariant boundariesConsistency(); + require boundedQueue(); + + // Read (any) value that is not the front (this asserts the value exists / the queue is long enough) + require index > 1; + bytes32 before = at_(index); + + popFront(); + + // try to read value + bytes32 after = at_@withrevert(index - 1); + + assert !lastReverted, "value still exists in the queue"; + assert before == after, "values are offset and not modified"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: popBack removes an element from the end of the queue │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule popBack { + requireInvariant boundariesConsistency(); + require boundedQueue(); + + uint256 lengthBefore = length(); + bytes32 backBefore = back@withrevert(); + + bytes32 popped = popBack@withrevert(); + bool success = !lastReverted; + + // liveness + assert success <=> lengthBefore != 0, "never reverts if not previously empty"; + + // effect + assert success => backBefore == popped, "previous back is returned"; + assert success => length() == lengthBefore - 1, "queue decreased"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: at(x) is preserved after calling popBack | +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule popBackConsistency(uint256 index) { + requireInvariant boundariesConsistency(); + require boundedQueue(); + + // Read (any) value that is not the back (this asserts the value exists / the queue is long enough) + require index < length() - 1; + bytes32 before = at_(index); + + popBack(); + + // try to read value + bytes32 after = at_@withrevert(index); + + assert !lastReverted, "value still exists in the queue"; + assert before == after, "values are offset and not modified"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: clear sets length to 0 │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule clear { + clear@withrevert(); + + // liveness + assert !lastReverted, "never reverts"; + + // effect + assert length() == 0, "sets length to 0"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: front/back access reverts only if the queue is empty or querying out of bounds │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyEmptyRevert(env e) { + require nonpayable(e); + requireInvariant boundariesConsistency(); + require boundedQueue(); + + method f; + calldataarg args; + + bool emptyBefore = empty(); + + f@withrevert(e, args); + + assert lastReverted => ( + (f.selector == front().selector && emptyBefore) || + (f.selector == back().selector && emptyBefore) || + (f.selector == popFront().selector && emptyBefore) || + (f.selector == popBack().selector && emptyBefore) || + f.selector == at_(uint256).selector // revert conditions are verified in onlyOutOfBoundsRevert + ), "only revert if empty or out of bounds"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: at(index) only reverts if index is out of bounds | +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyOutOfBoundsRevert(uint256 index) { + requireInvariant boundariesConsistency(); + require boundedQueue(); + + at_@withrevert(index); + + assert lastReverted <=> index >= length(), "only reverts if index is out of bounds"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: only clear/push/pop operations can change the length of the queue │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noLengthChange(env e) { + requireInvariant boundariesConsistency(); + require boundedQueue(); + + method f; + calldataarg args; + + uint256 lengthBefore = length(); + f(e, args); + uint256 lengthAfter = length(); + + assert lengthAfter != lengthBefore => ( + (f.selector == pushFront(bytes32).selector && lengthAfter == lengthBefore + 1) || + (f.selector == pushBack(bytes32).selector && lengthAfter == lengthBefore + 1) || + (f.selector == popBack().selector && lengthAfter == lengthBefore - 1) || + (f.selector == popFront().selector && lengthAfter == lengthBefore - 1) || + (f.selector == clear().selector && lengthAfter == 0) + ), "length is only affected by clear/pop/push operations"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: only push/pop can change values bounded in the queue (outside values aren't cleared) │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noDataChange(env e) { + requireInvariant boundariesConsistency(); + require boundedQueue(); + + method f; + calldataarg args; + + uint256 index; + bytes32 atBefore = at_(index); + f(e, args); + bytes32 atAfter = at_@withrevert(index); + bool atAfterSuccess = !lastReverted; + + assert !atAfterSuccess <=> ( + f.selector == clear().selector || + (f.selector == popBack().selector && index == length()) || + (f.selector == popFront().selector && index == length()) + ), "indexes of the queue are only removed by clear or pop"; + + assert atAfterSuccess && atAfter != atBefore => ( + f.selector == popFront().selector || + f.selector == pushFront(bytes32).selector + ), "values of the queue are only changed by popFront or pushFront"; +} From 86f6eb2c9c0eb741de18ef75a290ff340acf7148 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 12 Apr 2023 05:29:36 +0200 Subject: [PATCH 097/197] Add FV specification for ERC721 (#4104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- certora/diff/token_ERC721_ERC721.sol.patch | 14 + certora/harnesses/ERC721Harness.sol | 37 ++ certora/harnesses/ERC721ReceiverHarness.sol | 11 + certora/specs.json | 6 + certora/specs/ERC721.spec | 589 ++++++++++++++++++++ certora/specs/methods/IERC721.spec | 20 + 6 files changed, 677 insertions(+) create mode 100644 certora/diff/token_ERC721_ERC721.sol.patch create mode 100644 certora/harnesses/ERC721Harness.sol create mode 100644 certora/harnesses/ERC721ReceiverHarness.sol create mode 100644 certora/specs/ERC721.spec create mode 100644 certora/specs/methods/IERC721.spec diff --git a/certora/diff/token_ERC721_ERC721.sol.patch b/certora/diff/token_ERC721_ERC721.sol.patch new file mode 100644 index 000000000..c3eae357a --- /dev/null +++ b/certora/diff/token_ERC721_ERC721.sol.patch @@ -0,0 +1,14 @@ +--- token/ERC721/ERC721.sol 2023-03-07 10:48:47.736822221 +0100 ++++ token/ERC721/ERC721.sol 2023-03-09 19:49:39.669338673 +0100 +@@ -199,6 +199,11 @@ + return _owners[tokenId]; + } + ++ // FV ++ function _getApproved(uint256 tokenId) internal view returns (address) { ++ return _tokenApprovals[tokenId]; ++ } ++ + /** + * @dev Returns whether `tokenId` exists. + * diff --git a/certora/harnesses/ERC721Harness.sol b/certora/harnesses/ERC721Harness.sol new file mode 100644 index 000000000..3307369a8 --- /dev/null +++ b/certora/harnesses/ERC721Harness.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../patched/token/ERC721/ERC721.sol"; + +contract ERC721Harness is ERC721 { + constructor(string memory name, string memory symbol) ERC721(name, symbol) {} + + function mint(address account, uint256 tokenId) external { + _mint(account, tokenId); + } + + function safeMint(address to, uint256 tokenId) external { + _safeMint(to, tokenId); + } + + function safeMint(address to, uint256 tokenId, bytes memory data) external { + _safeMint(to, tokenId, data); + } + + function burn(uint256 tokenId) external { + _burn(tokenId); + } + + function tokenExists(uint256 tokenId) external view returns (bool) { + return _exists(tokenId); + } + + function unsafeOwnerOf(uint256 tokenId) external view returns (address) { + return _ownerOf(tokenId); + } + + function unsafeGetApproved(uint256 tokenId) external view returns (address) { + return _getApproved(tokenId); + } +} diff --git a/certora/harnesses/ERC721ReceiverHarness.sol b/certora/harnesses/ERC721ReceiverHarness.sol new file mode 100644 index 000000000..7e5739ee3 --- /dev/null +++ b/certora/harnesses/ERC721ReceiverHarness.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../patched/interfaces/IERC721Receiver.sol"; + +contract ERC721ReceiverHarness is IERC721Receiver { + function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { + return this.onERC721Received.selector; + } +} diff --git a/certora/specs.json b/certora/specs.json index cd68af65e..39ba8c235 100644 --- a/certora/specs.json +++ b/certora/specs.json @@ -51,6 +51,12 @@ "--optimistic_loop" ] }, + { + "spec": "ERC721", + "contract": "ERC721Harness", + "files": ["certora/harnesses/ERC721Harness.sol", "certora/harnesses/ERC721ReceiverHarness.sol"], + "options": ["--optimistic_loop"] + }, { "spec": "Initializable", "contract": "InitializableHarness", diff --git a/certora/specs/ERC721.spec b/certora/specs/ERC721.spec new file mode 100644 index 000000000..48503469b --- /dev/null +++ b/certora/specs/ERC721.spec @@ -0,0 +1,589 @@ +import "helpers.spec" +import "methods/IERC721.spec" + +methods { + // exposed for FV + mint(address,uint256) + safeMint(address,uint256) + safeMint(address,uint256,bytes) + burn(uint256) + + tokenExists(uint256) returns (bool) envfree + unsafeOwnerOf(uint256) returns (address) envfree + unsafeGetApproved(uint256) returns (address) envfree +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Helpers │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ + +// Could be broken in theory, but not in practice +function balanceLimited(address account) returns bool { + return balanceOf(account) < max_uint256; +} + +function helperTransferWithRevert(env e, method f, address from, address to, uint256 tokenId) { + if (f.selector == transferFrom(address,address,uint256).selector) { + transferFrom@withrevert(e, from, to, tokenId); + } else if (f.selector == safeTransferFrom(address,address,uint256).selector) { + safeTransferFrom@withrevert(e, from, to, tokenId); + } else if (f.selector == safeTransferFrom(address,address,uint256,bytes).selector) { + bytes params; + require params.length < 0xffff; + safeTransferFrom@withrevert(e, from, to, tokenId, params); + } else { + calldataarg args; + f@withrevert(e, args); + } +} + +function helperMintWithRevert(env e, method f, address to, uint256 tokenId) { + if (f.selector == mint(address,uint256).selector) { + mint@withrevert(e, to, tokenId); + } else if (f.selector == safeMint(address,uint256).selector) { + safeMint@withrevert(e, to, tokenId); + } else if (f.selector == safeMint(address,uint256,bytes).selector) { + bytes params; + require params.length < 0xffff; + safeMint@withrevert(e, to, tokenId, params); + } else { + require false; + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Ghost & hooks: ownership count │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +ghost ownedTotal() returns uint256 { + init_state axiom ownedTotal() == 0; +} + +ghost mapping(address => uint256) ownedByUser { + init_state axiom forall address a. ownedByUser[a] == 0; +} + +hook Sstore _owners[KEY uint256 tokenId] address newOwner (address oldOwner) STORAGE { + ownedByUser[newOwner] = ownedByUser[newOwner] + to_uint256(newOwner != 0 ? 1 : 0); + ownedByUser[oldOwner] = ownedByUser[oldOwner] - to_uint256(oldOwner != 0 ? 1 : 0); + + havoc ownedTotal assuming ownedTotal@new() == ownedTotal@old() + + to_uint256(newOwner != 0 ? 1 : 0) + - to_uint256(oldOwner != 0 ? 1 : 0); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Ghost & hooks: sum of all balances │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +ghost sumOfBalances() returns uint256 { + init_state axiom sumOfBalances() == 0; +} + +hook Sstore _balances[KEY address addr] uint256 newValue (uint256 oldValue) STORAGE { + havoc sumOfBalances assuming sumOfBalances@new() == sumOfBalances@old() + newValue - oldValue; +} + +ghost mapping(address => uint256) ghostBalanceOf { + init_state axiom forall address a. ghostBalanceOf[a] == 0; +} + +hook Sload uint256 value _balances[KEY address user] STORAGE { + require ghostBalanceOf[user] == value; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: ownedTotal is the sum of all balances │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant ownedTotalIsSumOfBalances() + ownedTotal() == sumOfBalances() + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: balanceOf is the number of tokens owned │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant balanceOfConsistency(address user) + balanceOf(user) == ownedByUser[user] && + balanceOf(user) == ghostBalanceOf[user] + { + preserved { + require balanceLimited(user); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: owner of a token must have some balance │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant ownerHasBalance(uint256 tokenId) + balanceOf(ownerOf(tokenId)) > 0 + { + preserved { + requireInvariant balanceOfConsistency(ownerOf(tokenId)); + require balanceLimited(ownerOf(tokenId)); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: tokens that do not exist are not owned and not approved │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant notMintedUnset(uint256 tokenId) + (!tokenExists(tokenId) <=> unsafeOwnerOf(tokenId) == 0) && + (!tokenExists(tokenId) => unsafeGetApproved(tokenId) == 0) + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: ownerOf and getApproved revert if token does not exist │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule notMintedRevert(uint256 tokenId) { + requireInvariant notMintedUnset(tokenId); + + bool e = tokenExists(tokenId); + + address owner = ownerOf@withrevert(tokenId); + assert e <=> !lastReverted; + assert e => owner == unsafeOwnerOf(tokenId); // notMintedUnset tells us this is non-zero + + address approved = getApproved@withrevert(tokenId); + assert e <=> !lastReverted; + assert e => approved == unsafeGetApproved(tokenId); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: unsafeOwnerOf and unsafeGetApproved don't revert │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule unsafeDontRevert(uint256 tokenId) { + unsafeOwnerOf@withrevert(tokenId); + assert !lastReverted; + + unsafeGetApproved@withrevert(tokenId); + assert !lastReverted; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: balance of address(0) is 0 │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule zeroAddressBalanceRevert() { + balanceOf@withrevert(0); + assert lastReverted; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: total supply can only change through mint and burn │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule supplyChange(env e) { + uint256 supplyBefore = ownedTotal(); + method f; calldataarg args; f(e, args); + uint256 supplyAfter = ownedTotal(); + + assert supplyAfter > supplyBefore => ( + supplyAfter == supplyBefore + 1 && + ( + f.selector == mint(address,uint256).selector || + f.selector == safeMint(address,uint256).selector || + f.selector == safeMint(address,uint256,bytes).selector + ) + ); + assert supplyAfter < supplyBefore => ( + supplyAfter == supplyBefore - 1 && + f.selector == burn(uint256).selector + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: balanceOf can only change through mint, burn or transfers. balanceOf cannot change by more than 1. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule balanceChange(env e, address account) { + requireInvariant balanceOfConsistency(account); + require balanceLimited(account); + + uint256 balanceBefore = balanceOf(account); + method f; calldataarg args; f(e, args); + uint256 balanceAfter = balanceOf(account); + + // balance can change by at most 1 + assert balanceBefore != balanceAfter => ( + balanceAfter == balanceBefore - 1 || + balanceAfter == balanceBefore + 1 + ); + + // only selected function can change balances + assert balanceBefore != balanceAfter => ( + f.selector == transferFrom(address,address,uint256).selector || + f.selector == safeTransferFrom(address,address,uint256).selector || + f.selector == safeTransferFrom(address,address,uint256,bytes).selector || + f.selector == mint(address,uint256).selector || + f.selector == safeMint(address,uint256).selector || + f.selector == safeMint(address,uint256,bytes).selector || + f.selector == burn(uint256).selector + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: ownership can only change through mint, burn or transfers. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule ownershipChange(env e, uint256 tokenId) { + address ownerBefore = unsafeOwnerOf(tokenId); + method f; calldataarg args; f(e, args); + address ownerAfter = unsafeOwnerOf(tokenId); + + assert ownerBefore == 0 && ownerAfter != 0 => ( + f.selector == mint(address,uint256).selector || + f.selector == safeMint(address,uint256).selector || + f.selector == safeMint(address,uint256,bytes).selector + ); + + assert ownerBefore != 0 && ownerAfter == 0 => ( + f.selector == burn(uint256).selector + ); + + assert (ownerBefore != ownerAfter && ownerBefore != 0 && ownerAfter != 0) => ( + f.selector == transferFrom(address,address,uint256).selector || + f.selector == safeTransferFrom(address,address,uint256).selector || + f.selector == safeTransferFrom(address,address,uint256,bytes).selector + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: token approval can only change through approve or transfers (implicitly). │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule approvalChange(env e, uint256 tokenId) { + address approvalBefore = unsafeGetApproved(tokenId); + method f; calldataarg args; f(e, args); + address approvalAfter = unsafeGetApproved(tokenId); + + // approve can set any value, other functions reset + assert approvalBefore != approvalAfter => ( + f.selector == approve(address,uint256).selector || + ( + ( + f.selector == transferFrom(address,address,uint256).selector || + f.selector == safeTransferFrom(address,address,uint256).selector || + f.selector == safeTransferFrom(address,address,uint256,bytes).selector || + f.selector == burn(uint256).selector + ) && approvalAfter == 0 + ) + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: approval for all tokens can only change through isApprovedForAll. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule approvedForAllChange(env e, address owner, address spender) { + bool approvedForAllBefore = isApprovedForAll(owner, spender); + method f; calldataarg args; f(e, args); + bool approvedForAllAfter = isApprovedForAll(owner, spender); + + assert approvedForAllBefore != approvedForAllAfter => f.selector == setApprovalForAll(address,bool).selector; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: transferFrom behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule transferFrom(env e, address from, address to, uint256 tokenId) { + require nonpayable(e); + + address operator = e.msg.sender; + uint256 otherTokenId; + address otherAccount; + + requireInvariant ownerHasBalance(tokenId); + require balanceLimited(to); + + uint256 balanceOfFromBefore = balanceOf(from); + uint256 balanceOfToBefore = balanceOf(to); + uint256 balanceOfOtherBefore = balanceOf(otherAccount); + address ownerBefore = unsafeOwnerOf(tokenId); + address otherOwnerBefore = unsafeOwnerOf(otherTokenId); + address approvalBefore = unsafeGetApproved(tokenId); + address otherApprovalBefore = unsafeGetApproved(otherTokenId); + + transferFrom@withrevert(e, from, to, tokenId); + bool success = !lastReverted; + + // liveness + assert success <=> ( + from == ownerBefore && + from != 0 && + to != 0 && + (operator == from || operator == approvalBefore || isApprovedForAll(ownerBefore, operator)) + ); + + // effect + assert success => ( + balanceOf(from) == balanceOfFromBefore - to_uint256(from != to ? 1 : 0) && + balanceOf(to) == balanceOfToBefore + to_uint256(from != to ? 1 : 0) && + unsafeOwnerOf(tokenId) == to && + unsafeGetApproved(tokenId) == 0 + ); + + // no side effect + assert balanceOf(otherAccount) != balanceOfOtherBefore => (otherAccount == from || otherAccount == to); + assert unsafeOwnerOf(otherTokenId) != otherOwnerBefore => otherTokenId == tokenId; + assert unsafeGetApproved(otherTokenId) != otherApprovalBefore => otherTokenId == tokenId; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: safeTransferFrom behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule safeTransferFrom(env e, method f, address from, address to, uint256 tokenId) filtered { f -> + f.selector == safeTransferFrom(address,address,uint256).selector || + f.selector == safeTransferFrom(address,address,uint256,bytes).selector +} { + require nonpayable(e); + + address operator = e.msg.sender; + uint256 otherTokenId; + address otherAccount; + + requireInvariant ownerHasBalance(tokenId); + require balanceLimited(to); + + uint256 balanceOfFromBefore = balanceOf(from); + uint256 balanceOfToBefore = balanceOf(to); + uint256 balanceOfOtherBefore = balanceOf(otherAccount); + address ownerBefore = unsafeOwnerOf(tokenId); + address otherOwnerBefore = unsafeOwnerOf(otherTokenId); + address approvalBefore = unsafeGetApproved(tokenId); + address otherApprovalBefore = unsafeGetApproved(otherTokenId); + + helperTransferWithRevert(e, f, from, to, tokenId); + bool success = !lastReverted; + + assert success <=> ( + from == ownerBefore && + from != 0 && + to != 0 && + (operator == from || operator == approvalBefore || isApprovedForAll(ownerBefore, operator)) + ); + + // effect + assert success => ( + balanceOf(from) == balanceOfFromBefore - to_uint256(from != to ? 1: 0) && + balanceOf(to) == balanceOfToBefore + to_uint256(from != to ? 1: 0) && + unsafeOwnerOf(tokenId) == to && + unsafeGetApproved(tokenId) == 0 + ); + + // no side effect + assert balanceOf(otherAccount) != balanceOfOtherBefore => (otherAccount == from || otherAccount == to); + assert unsafeOwnerOf(otherTokenId) != otherOwnerBefore => otherTokenId == tokenId; + assert unsafeGetApproved(otherTokenId) != otherApprovalBefore => otherTokenId == tokenId; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: mint behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule mint(env e, address to, uint256 tokenId) { + require nonpayable(e); + requireInvariant notMintedUnset(tokenId); + + uint256 otherTokenId; + address otherAccount; + + require balanceLimited(to); + + uint256 supplyBefore = ownedTotal(); + uint256 balanceOfToBefore = balanceOf(to); + uint256 balanceOfOtherBefore = balanceOf(otherAccount); + address ownerBefore = unsafeOwnerOf(tokenId); + address otherOwnerBefore = unsafeOwnerOf(otherTokenId); + + mint@withrevert(e, to, tokenId); + bool success = !lastReverted; + + // liveness + assert success <=> ( + ownerBefore == 0 && + to != 0 + ); + + // effect + assert success => ( + ownedTotal() == supplyBefore + 1 && + balanceOf(to) == balanceOfToBefore + 1 && + unsafeOwnerOf(tokenId) == to + ); + + // no side effect + assert balanceOf(otherAccount) != balanceOfOtherBefore => otherAccount == to; + assert unsafeOwnerOf(otherTokenId) != otherOwnerBefore => otherTokenId == tokenId; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: safeMint behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule safeMint(env e, method f, address to, uint256 tokenId) filtered { f -> + f.selector == safeMint(address,uint256).selector || + f.selector == safeMint(address,uint256,bytes).selector +} { + require nonpayable(e); + requireInvariant notMintedUnset(tokenId); + + uint256 otherTokenId; + address otherAccount; + + require balanceLimited(to); + + uint256 supplyBefore = ownedTotal(); + uint256 balanceOfToBefore = balanceOf(to); + uint256 balanceOfOtherBefore = balanceOf(otherAccount); + address ownerBefore = unsafeOwnerOf(tokenId); + address otherOwnerBefore = unsafeOwnerOf(otherTokenId); + + helperMintWithRevert(e, f, to, tokenId); + bool success = !lastReverted; + + assert success <=> ( + ownerBefore == 0 && + to != 0 + ); + + // effect + assert success => ( + ownedTotal() == supplyBefore + 1 && + balanceOf(to) == balanceOfToBefore + 1 && + unsafeOwnerOf(tokenId) == to + ); + + // no side effect + assert balanceOf(otherAccount) != balanceOfOtherBefore => otherAccount == to; + assert unsafeOwnerOf(otherTokenId) != otherOwnerBefore => otherTokenId == tokenId; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: burn behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule burn(env e, uint256 tokenId) { + require nonpayable(e); + + address from = unsafeOwnerOf(tokenId); + uint256 otherTokenId; + address otherAccount; + + requireInvariant ownerHasBalance(tokenId); + + uint256 supplyBefore = ownedTotal(); + uint256 balanceOfFromBefore = balanceOf(from); + uint256 balanceOfOtherBefore = balanceOf(otherAccount); + address ownerBefore = unsafeOwnerOf(tokenId); + address otherOwnerBefore = unsafeOwnerOf(otherTokenId); + address otherApprovalBefore = unsafeGetApproved(otherTokenId); + + burn@withrevert(e, tokenId); + bool success = !lastReverted; + + // liveness + assert success <=> ( + ownerBefore != 0 + ); + + // effect + assert success => ( + ownedTotal() == supplyBefore - 1 && + balanceOf(from) == balanceOfFromBefore - 1 && + unsafeOwnerOf(tokenId) == 0 && + unsafeGetApproved(tokenId) == 0 + ); + + // no side effect + assert balanceOf(otherAccount) != balanceOfOtherBefore => otherAccount == from; + assert unsafeOwnerOf(otherTokenId) != otherOwnerBefore => otherTokenId == tokenId; + assert unsafeGetApproved(otherTokenId) != otherApprovalBefore => otherTokenId == tokenId; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: approve behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule approve(env e, address spender, uint256 tokenId) { + require nonpayable(e); + + address caller = e.msg.sender; + address owner = unsafeOwnerOf(tokenId); + uint256 otherTokenId; + + address otherApprovalBefore = unsafeGetApproved(otherTokenId); + + approve@withrevert(e, spender, tokenId); + bool success = !lastReverted; + + // liveness + assert success <=> ( + owner != 0 && + owner != spender && + (owner == caller || isApprovedForAll(owner, caller)) + ); + + // effect + assert success => unsafeGetApproved(tokenId) == spender; + + // no side effect + assert unsafeGetApproved(otherTokenId) != otherApprovalBefore => otherTokenId == tokenId; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: setApprovalForAll behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule setApprovalForAll(env e, address operator, bool approved) { + require nonpayable(e); + + address owner = e.msg.sender; + address otherOwner; + address otherOperator; + + bool otherIsApprovedForAllBefore = isApprovedForAll(otherOwner, otherOperator); + + setApprovalForAll@withrevert(e, operator, approved); + bool success = !lastReverted; + + // liveness + assert success <=> owner != operator; + + // effect + assert success => isApprovedForAll(owner, operator) == approved; + + // no side effect + assert isApprovedForAll(otherOwner, otherOperator) != otherIsApprovedForAllBefore => ( + otherOwner == owner && + otherOperator == operator + ); +} diff --git a/certora/specs/methods/IERC721.spec b/certora/specs/methods/IERC721.spec new file mode 100644 index 000000000..e6d4e1e04 --- /dev/null +++ b/certora/specs/methods/IERC721.spec @@ -0,0 +1,20 @@ +methods { + // IERC721 + balanceOf(address) returns (uint256) envfree => DISPATCHER(true) + ownerOf(uint256) returns (address) envfree => DISPATCHER(true) + getApproved(uint256) returns (address) envfree => DISPATCHER(true) + isApprovedForAll(address,address) returns (bool) envfree => DISPATCHER(true) + safeTransferFrom(address,address,uint256,bytes) => DISPATCHER(true) + safeTransferFrom(address,address,uint256) => DISPATCHER(true) + transferFrom(address,address,uint256) => DISPATCHER(true) + approve(address,uint256) => DISPATCHER(true) + setApprovalForAll(address,bool) => DISPATCHER(true) + + // IERC721Metadata + name() returns (string) => DISPATCHER(true) + symbol() returns (string) => DISPATCHER(true) + tokenURI(uint256) returns (string) => DISPATCHER(true) + + // IERC721Receiver + onERC721Received(address,address,uint256,bytes) returns (bytes4) => DISPATCHER(true) +} From 788d6a129a342d7a23a91ad553479e861845a9b2 Mon Sep 17 00:00:00 2001 From: Francisco Date: Wed, 12 Apr 2023 12:09:30 -0300 Subject: [PATCH 098/197] Add fuzz tests for ShortString (#4175) --- test/utils/ShortStrings.t.sol | 55 +++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 test/utils/ShortStrings.t.sol diff --git a/test/utils/ShortStrings.t.sol b/test/utils/ShortStrings.t.sol new file mode 100644 index 000000000..7c4faa89d --- /dev/null +++ b/test/utils/ShortStrings.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; + +import "../../contracts/utils/ShortStrings.sol"; + +contract ShortStringsTest is Test { + string _fallback; + + function testRoundtripShort(string memory input) external { + vm.assume(_isShort(input)); + ShortString short = ShortStrings.toShortString(input); + string memory output = ShortStrings.toString(short); + assertEq(input, output); + } + + function testRoundtripWithFallback(string memory input, string memory fallbackInitial) external { + _fallback = fallbackInitial; // Make sure that the initial value has no effect + ShortString short = ShortStrings.toShortStringWithFallback(input, _fallback); + string memory output = ShortStrings.toStringWithFallback(short, _fallback); + assertEq(input, output); + } + + function testRevertLong(string memory input) external { + vm.assume(!_isShort(input)); + vm.expectRevert(abi.encodeWithSelector(ShortStrings.StringTooLong.selector, input)); + this.toShortString(input); + } + + function testLengthShort(string memory input) external { + vm.assume(_isShort(input)); + uint256 inputLength = bytes(input).length; + ShortString short = ShortStrings.toShortString(input); + uint256 shortLength = ShortStrings.byteLength(short); + assertEq(inputLength, shortLength); + } + + function testLengthWithFallback(string memory input, string memory fallbackInitial) external { + _fallback = fallbackInitial; + uint256 inputLength = bytes(input).length; + ShortString short = ShortStrings.toShortStringWithFallback(input, _fallback); + uint256 shortLength = ShortStrings.byteLengthWithFallback(short, _fallback); + assertEq(inputLength, shortLength); + } + + function toShortString(string memory input) external pure returns (ShortString) { + return ShortStrings.toShortString(input); + } + + function _isShort(string memory input) internal pure returns (bool) { + return bytes(input).length < 32; + } +} From dd1265cb1dc834b129c6daf6aae7bdae28521d9e Mon Sep 17 00:00:00 2001 From: Pascal Marco Caversaccio Date: Wed, 12 Apr 2023 22:33:50 +0200 Subject: [PATCH 099/197] Improve `ERC4626` test coverage (#4134) Signed-off-by: Pascal Marco Caversaccio --- .../mocks/token/ERC20ExcessDecimalsMock.sol | 9 +++++ remappings.txt | 1 + test/token/ERC20/extensions/ERC4626.t.sol | 34 ++++++++++++++++--- test/token/ERC20/extensions/ERC4626.test.js | 23 +++++++++++++ 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 contracts/mocks/token/ERC20ExcessDecimalsMock.sol create mode 100644 remappings.txt diff --git a/contracts/mocks/token/ERC20ExcessDecimalsMock.sol b/contracts/mocks/token/ERC20ExcessDecimalsMock.sol new file mode 100644 index 000000000..0fb35a607 --- /dev/null +++ b/contracts/mocks/token/ERC20ExcessDecimalsMock.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +contract ERC20ExcessDecimalsMock { + function decimals() public pure returns (uint256) { + return type(uint256).max; + } +} diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 000000000..2479e3d26 --- /dev/null +++ b/remappings.txt @@ -0,0 +1 @@ +openzeppelin/=contracts/ diff --git a/test/token/ERC20/extensions/ERC4626.t.sol b/test/token/ERC20/extensions/ERC4626.t.sol index 95514531c..f220086d1 100644 --- a/test/token/ERC20/extensions/ERC4626.t.sol +++ b/test/token/ERC20/extensions/ERC4626.t.sol @@ -1,18 +1,42 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import "erc4626-tests/ERC4626.test.sol"; +import {ERC4626Test} from "erc4626-tests/ERC4626.test.sol"; -import {SafeCast} from "../../../../contracts/utils/math/SafeCast.sol"; -import {ERC20Mock} from "../../../../contracts/mocks/ERC20Mock.sol"; -import {ERC4626Mock} from "../../../../contracts/mocks/ERC4626Mock.sol"; +import {SafeCast} from "openzeppelin/utils/math/SafeCast.sol"; +import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol"; +import {ERC4626} from "openzeppelin/token/ERC20/extensions/ERC4626.sol"; + +import {ERC20Mock} from "openzeppelin/mocks/ERC20Mock.sol"; +import {ERC4626Mock} from "openzeppelin/mocks/ERC4626Mock.sol"; +import {ERC4626OffsetMock} from "openzeppelin/mocks/token/ERC4626OffsetMock.sol"; + +contract ERC4626VaultOffsetMock is ERC4626OffsetMock { + constructor( + ERC20 underlying_, + uint8 offset_ + ) ERC20("My Token Vault", "MTKNV") ERC4626(underlying_) ERC4626OffsetMock(offset_) {} +} contract ERC4626StdTest is ERC4626Test { + ERC20 private _underlying = new ERC20Mock(); + function setUp() public override { - _underlying_ = address(new ERC20Mock()); + _underlying_ = address(_underlying); _vault_ = address(new ERC4626Mock(_underlying_)); _delta_ = 0; _vaultMayBeEmpty = true; _unlimitedAmount = true; } + + /** + * @dev Check the case where calculated `decimals` value overflows the `uint8` type. + */ + function testFuzzDecimalsOverflow(uint8 offset) public { + /// @dev Remember that the `_underlying` exhibits a `decimals` value of 18. + offset = uint8(bound(uint256(offset), 238, uint256(type(uint8).max))); + ERC4626VaultOffsetMock erc4626VaultOffsetMock = new ERC4626VaultOffsetMock(_underlying, offset); + vm.expectRevert(); + erc4626VaultOffsetMock.decimals(); + } } diff --git a/test/token/ERC20/extensions/ERC4626.test.js b/test/token/ERC20/extensions/ERC4626.test.js index c9e5a4098..ee0998717 100644 --- a/test/token/ERC20/extensions/ERC4626.test.js +++ b/test/token/ERC20/extensions/ERC4626.test.js @@ -5,6 +5,7 @@ const ERC20Decimals = artifacts.require('$ERC20DecimalsMock'); const ERC4626 = artifacts.require('$ERC4626'); const ERC4626OffsetMock = artifacts.require('$ERC4626OffsetMock'); const ERC4626FeesMock = artifacts.require('$ERC4626FeesMock'); +const ERC20ExcessDecimalsMock = artifacts.require('ERC20ExcessDecimalsMock'); contract('ERC4626', function (accounts) { const [holder, recipient, spender, other, user1, user2] = accounts; @@ -21,6 +22,28 @@ contract('ERC4626', function (accounts) { } }); + it('asset has not yet been created', async function () { + const vault = await ERC4626.new('', '', other); + expect(await vault.decimals()).to.be.bignumber.equal(decimals); + }); + + it('underlying excess decimals', async function () { + const token = await ERC20ExcessDecimalsMock.new(); + const vault = await ERC4626.new('', '', token.address); + expect(await vault.decimals()).to.be.bignumber.equal(decimals); + }); + + it('decimals overflow', async function () { + for (const offset of [243, 250, 255].map(web3.utils.toBN)) { + const token = await ERC20Decimals.new('', '', decimals); + const vault = await ERC4626OffsetMock.new(name + ' Vault', symbol + 'V', token.address, offset); + await expectRevert( + vault.decimals(), + 'reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)', + ); + } + }); + for (const offset of [0, 6, 18].map(web3.utils.toBN)) { const parseToken = token => web3.utils.toBN(10).pow(decimals).muln(token); const parseShare = share => web3.utils.toBN(10).pow(decimals.add(offset)).muln(share); From 3b117992e17ae67fcd19ea2bd988f64520813cd1 Mon Sep 17 00:00:00 2001 From: Francisco Date: Thu, 13 Apr 2023 11:04:04 -0300 Subject: [PATCH 100/197] Improve docs for transparent proxy (#4181) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- contracts/proxy/README.adoc | 2 ++ .../TransparentUpgradeableProxy.sol | 24 +++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/contracts/proxy/README.adoc b/contracts/proxy/README.adoc index 5ada16e38..89717a7bf 100644 --- a/contracts/proxy/README.adoc +++ b/contracts/proxy/README.adoc @@ -56,6 +56,8 @@ The current implementation of this security mechanism uses https://eips.ethereum == ERC1967 +{{IERC1967}} + {{ERC1967Proxy}} {{ERC1967Upgrade}} diff --git a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol index a89b233f2..d8765fc9e 100644 --- a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol @@ -6,10 +6,10 @@ pragma solidity ^0.8.0; import "../ERC1967/ERC1967Proxy.sol"; /** - * @dev Interface for the {TransparentUpgradeableProxy}. This is useful because {TransparentUpgradeableProxy} uses a - * custom call-routing mechanism, the compiler is unaware of the functions being exposed, and cannot list them. Also - * {TransparentUpgradeableProxy} does not inherit from this interface because it's implemented in a way that the - * compiler doesn't understand and cannot verify. + * @dev Interface for {TransparentUpgradeableProxy}. In order to implement transparency, {TransparentUpgradeableProxy} + * does not implement this interface directly, and some of its functions are implemented by an internal dispatch + * mechanism. The compiler is unaware that these functions are implemented by {TransparentUpgradeableProxy} and will not + * include them in the ABI so this interface must be used to interact with it. */ interface ITransparentUpgradeableProxy is IERC1967 { function admin() external view returns (address); @@ -44,12 +44,16 @@ interface ITransparentUpgradeableProxy is IERC1967 { * Our recommendation is for the dedicated account to be an instance of the {ProxyAdmin} contract. If set up this way, * you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy. * - * WARNING: This contract does not inherit from {ITransparentUpgradeableProxy}, and the admin function is implicitly - * implemented using a custom call-routing mechanism in `_fallback`. Consequently, the compiler will not produce an - * ABI for this contract. Also, if you inherit from this contract and add additional functions, the compiler will not - * check that there are no selector conflicts. A selector clash between any new function and the functions declared in - * {ITransparentUpgradeableProxy} will be resolved in favor of the new one. This could render the admin operations - * inaccessible, which could prevent upgradeability. + * NOTE: The real interface of this proxy is that defined in `ITransparentUpgradeableProxy`. This contract does not + * inherit from that interface, and instead the admin functions are implicitly implemented using a custom dispatch + * mechanism in `_fallback`. Consequently, the compiler will not produce an ABI for this contract. This is necessary to + * fully implement transparency without decoding reverts caused by selector clashes between the proxy and the + * implementation. + * + * WARNING: It is not recommended to extend this contract to add additional external functions. If you do so, the compiler + * will not check that there are no selector conflicts, due to the note above. A selector clash between any new function + * and the functions declared in {ITransparentUpgradeableProxy} will be resolved in favor of the new one. This could + * render the admin operations inaccessible, which could prevent upgradeability. Transparency may also be compromised. */ contract TransparentUpgradeableProxy is ERC1967Proxy { /** From 8d633cb7d169f2f8595b273660b00b69e845c2fe Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 13 Apr 2023 20:47:51 +0200 Subject: [PATCH 101/197] Merge pull request from GHSA-93hq-5wgc-jc82 Co-authored-by: Francisco --- .changeset/silent-pugs-scream.md | 5 +++++ .../compatibility/GovernorCompatibilityBravo.sol | 4 ++-- .../GovernorCompatibilityBravo.test.js | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 .changeset/silent-pugs-scream.md diff --git a/.changeset/silent-pugs-scream.md b/.changeset/silent-pugs-scream.md new file mode 100644 index 000000000..c92d12486 --- /dev/null +++ b/.changeset/silent-pugs-scream.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`GovernorCompatibilityBravo`: Fix encoding of proposal data when signatures are missing. diff --git a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol index 25f404403..7fd8ce0c4 100644 --- a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol @@ -70,6 +70,7 @@ 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"); // 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 @@ -149,8 +150,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp bytes[] memory calldatas ) private pure returns (bytes[] memory) { bytes[] memory fullcalldatas = new bytes[](calldatas.length); - - for (uint256 i = 0; i < signatures.length; ++i) { + for (uint256 i = 0; i < fullcalldatas.length; ++i) { fullcalldatas[i] = bytes(signatures[i]).length == 0 ? calldatas[i] : abi.encodePacked(bytes4(keccak256(bytes(signatures[i]))), calldatas[i]); diff --git a/test/governance/compatibility/GovernorCompatibilityBravo.test.js b/test/governance/compatibility/GovernorCompatibilityBravo.test.js index 9e551949f..5a8104a98 100644 --- a/test/governance/compatibility/GovernorCompatibilityBravo.test.js +++ b/test/governance/compatibility/GovernorCompatibilityBravo.test.js @@ -224,6 +224,21 @@ contract('GovernorCompatibilityBravo', function (accounts) { }); }); + it('with inconsistent array size for selector and arguments', async function () { + const target = this.receiver.address; + 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 + }, + '', + ); + + await expectRevert(this.helper.propose({ from: proposer }), 'GovernorBravo: invalid signatures length'); + }); + describe('should revert', function () { describe('on propose', function () { it('if proposal does not meet proposalThreshold', async function () { From 91df66c4a9dfd0425ff923cbeb3a20155f1355ea Mon Sep 17 00:00:00 2001 From: Francisco Date: Fri, 21 Apr 2023 12:35:07 +0100 Subject: [PATCH 102/197] Implement suggestions from audit of 4.9 (#4176) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- contracts/governance/Governor.sol | 54 +++++++++++-------- contracts/governance/IGovernor.sol | 8 ++- contracts/governance/TimelockController.sol | 25 +++++---- .../GovernorCompatibilityBravo.sol | 10 ++-- .../IGovernorCompatibilityBravo.sol | 5 ++ .../extensions/GovernorPreventLateQuorum.sol | 12 ++--- .../extensions/GovernorTimelockCompound.sol | 14 +++-- .../extensions/GovernorTimelockControl.sol | 10 ++-- .../GovernorVotesQuorumFraction.sol | 7 ++- contracts/governance/utils/IVotes.sol | 4 +- contracts/governance/utils/Votes.sol | 6 +-- .../token/ERC20/extensions/ERC20Votes.sol | 2 +- .../token/ERC721/extensions/ERC721Votes.sol | 2 + test/governance/Governor.test.js | 6 +-- .../SupportsInterface.behavior.js | 8 ++- 15 files changed, 97 insertions(+), 76 deletions(-) diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index a2911bf98..241d6139b 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -17,7 +17,7 @@ import "./IGovernor.sol"; /** * @dev Core of the governance system, designed to be extended though various modules. * - * This contract is abstract and requires several function to be implemented in various modules: + * This contract is abstract and requires several functions to be implemented in various modules: * * - A counting module must implement {quorum}, {_quorumReached}, {_voteSucceeded} and {_countVote} * - A voting module must implement {_getVotes} @@ -27,7 +27,6 @@ import "./IGovernor.sol"; */ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receiver, IERC1155Receiver { using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; - using SafeCast for uint256; bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,uint8 support)"); bytes32 public constant EXTENDED_BALLOT_TYPEHASH = @@ -90,25 +89,34 @@ 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)); + require(_executor() == address(this), "Governor: must send to executor"); } /** * @dev See {IERC165-supportsInterface}. */ function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { - // In addition to the current interfaceId, also support previous version of the interfaceId that did not - // include the castVoteWithReasonAndParams() function as standard + bytes4 governorCancelId = this.cancel.selector ^ this.proposalProposer.selector; + + bytes4 governorParamsId = this.castVoteWithReasonAndParams.selector ^ + this.castVoteWithReasonAndParamsBySig.selector ^ + this.getVotesWithParams.selector; + + // The original interface id in v4.3. + bytes4 governor43Id = type(IGovernor).interfaceId ^ + type(IERC6372).interfaceId ^ + governorCancelId ^ + governorParamsId; + + // An updated interface id in v4.6, with params added. + bytes4 governor46Id = type(IGovernor).interfaceId ^ type(IERC6372).interfaceId ^ governorCancelId; + + // For the updated interface id in v4.9, we use governorCancelId directly. + return - interfaceId == - (type(IGovernor).interfaceId ^ - type(IERC6372).interfaceId ^ - this.cancel.selector ^ - this.castVoteWithReasonAndParams.selector ^ - this.castVoteWithReasonAndParamsBySig.selector ^ - this.getVotesWithParams.selector) || - // Previous interface for backwards compatibility - interfaceId == (type(IGovernor).interfaceId ^ type(IERC6372).interfaceId ^ this.cancel.selector) || + interfaceId == governor43Id || + interfaceId == governor46Id || + interfaceId == governorCancelId || interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); } @@ -210,9 +218,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive } /** - * @dev Address of the proposer + * @dev Returns the account that created a given proposal. */ - function _proposalProposer(uint256 proposalId) internal view virtual returns (address) { + function proposalProposer(uint256 proposalId) public view virtual override returns (address) { return _proposals[proposalId].proposer; } @@ -283,8 +291,8 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive _proposals[proposalId] = ProposalCore({ proposer: proposer, - voteStart: snapshot.toUint64(), - voteEnd: deadline.toUint64(), + voteStart: SafeCast.toUint64(snapshot), + voteEnd: SafeCast.toUint64(deadline), executed: false, canceled: false, __gap_unused0: 0, @@ -317,9 +325,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive ) public payable virtual override returns (uint256) { uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); - ProposalState status = state(proposalId); + ProposalState currentState = state(proposalId); require( - status == ProposalState.Succeeded || status == ProposalState.Queued, + currentState == ProposalState.Succeeded || currentState == ProposalState.Queued, "Governor: proposal not successful" ); _proposals[proposalId].executed = true; @@ -415,10 +423,12 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive ) internal virtual returns (uint256) { uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); - ProposalState status = state(proposalId); + ProposalState currentState = state(proposalId); require( - status != ProposalState.Canceled && status != ProposalState.Expired && status != ProposalState.Executed, + currentState != ProposalState.Canceled && + currentState != ProposalState.Expired && + currentState != ProposalState.Executed, "Governor: proposal not active" ); _proposals[proposalId].canceled = true; diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 70f81efe8..e4ad83e87 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -152,6 +152,12 @@ abstract contract IGovernor is IERC165, IERC6372 { */ function proposalDeadline(uint256 proposalId) public view virtual returns (uint256); + /** + * @notice module:core + * @dev The account that created a proposal. + */ + function proposalProposer(uint256 proposalId) public view virtual returns (address); + /** * @notice module:user-config * @dev Delay, between the proposal is created and the vote starts. The unit this duration is expressed in depends @@ -164,7 +170,7 @@ abstract contract IGovernor is IERC165, IERC6372 { /** * @notice module:user-config - * @dev Delay, between the vote start and vote ends. The unit this duration is expressed in depends on the clock + * @dev Delay between the vote start and vote end. The unit this duration is expressed in depends on the clock * (see EIP-6372) this contract uses. * * NOTE: The {votingDelay} can delay the start of the vote. This must be considered when setting the voting diff --git a/contracts/governance/TimelockController.sol b/contracts/governance/TimelockController.sol index 18ca81e5f..e330cca06 100644 --- a/contracts/governance/TimelockController.sol +++ b/contracts/governance/TimelockController.sol @@ -6,7 +6,6 @@ pragma solidity ^0.8.0; 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 @@ -137,21 +136,21 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver * @dev Returns whether an id correspond to a registered operation. This * includes both Pending, Ready and Done operations. */ - function isOperation(bytes32 id) public view virtual returns (bool registered) { + function isOperation(bytes32 id) public view virtual returns (bool) { return getTimestamp(id) > 0; } /** - * @dev Returns whether an operation is pending or not. + * @dev Returns whether an operation is pending or not. Note that a "pending" operation may also be "ready". */ - function isOperationPending(bytes32 id) public view virtual returns (bool pending) { + function isOperationPending(bytes32 id) public view virtual returns (bool) { return getTimestamp(id) > _DONE_TIMESTAMP; } /** - * @dev Returns whether an operation is ready or not. + * @dev Returns whether an operation is ready for execution. Note that a "ready" operation is also "pending". */ - function isOperationReady(bytes32 id) public view virtual returns (bool ready) { + function isOperationReady(bytes32 id) public view virtual returns (bool) { uint256 timestamp = getTimestamp(id); return timestamp > _DONE_TIMESTAMP && timestamp <= block.timestamp; } @@ -159,7 +158,7 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver /** * @dev Returns whether an operation is done or not. */ - function isOperationDone(bytes32 id) public view virtual returns (bool done) { + function isOperationDone(bytes32 id) public view virtual returns (bool) { return getTimestamp(id) == _DONE_TIMESTAMP; } @@ -167,7 +166,7 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver * @dev Returns the timestamp at which an operation becomes ready (0 for * unset operations, 1 for done operations). */ - function getTimestamp(bytes32 id) public view virtual returns (uint256 timestamp) { + function getTimestamp(bytes32 id) public view virtual returns (uint256) { return _timestamps[id]; } @@ -176,7 +175,7 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver * * This value can be changed by executing an operation that calls `updateDelay`. */ - function getMinDelay() public view virtual returns (uint256 duration) { + function getMinDelay() public view virtual returns (uint256) { return _minDelay; } @@ -190,7 +189,7 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver bytes calldata data, bytes32 predecessor, bytes32 salt - ) public pure virtual returns (bytes32 hash) { + ) public pure virtual returns (bytes32) { return keccak256(abi.encode(target, value, data, predecessor, salt)); } @@ -204,14 +203,14 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver bytes[] calldata payloads, bytes32 predecessor, bytes32 salt - ) public pure virtual returns (bytes32 hash) { + ) public pure virtual returns (bytes32) { return keccak256(abi.encode(targets, values, payloads, predecessor, salt)); } /** * @dev Schedule an operation containing a single transaction. * - * Emits events {CallScheduled} and {CallSalt}. + * Emits {CallSalt} if salt is nonzero, and {CallScheduled}. * * Requirements: * @@ -236,7 +235,7 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver /** * @dev Schedule an operation containing a batch of transactions. * - * Emits a {CallSalt} event and one {CallScheduled} event per transaction in the batch. + * Emits {CallSalt} if salt is nonzero, and one {CallScheduled} event per transaction in the batch. * * Requirements: * diff --git a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol index 7fd8ce0c4..0fbb4e17c 100644 --- a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol @@ -74,7 +74,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp // 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 - // is added their is also executed when calling this alternative interface. + // is added there is also executed when calling this alternative interface. _storeProposal(_msgSender(), targets, values, signatures, calldatas, description); return propose(targets, values, _encodeCalldata(signatures, calldatas), description); } @@ -110,7 +110,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp /** * @dev Cancel a proposal with GovernorBravo logic. */ - function cancel(uint256 proposalId) public virtual { + function cancel(uint256 proposalId) public virtual override { ( address[] memory targets, uint256[] memory values, @@ -238,9 +238,9 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp againstVotes = details.againstVotes; abstainVotes = details.abstainVotes; - ProposalState status = state(proposalId); - canceled = status == ProposalState.Canceled; - executed = status == ProposalState.Executed; + ProposalState currentState = state(proposalId); + canceled = currentState == ProposalState.Canceled; + executed = currentState == ProposalState.Executed; } /** diff --git a/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol b/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol index 159c55100..7aa806a18 100644 --- a/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol @@ -90,6 +90,11 @@ abstract contract IGovernorCompatibilityBravo is IGovernor { */ function execute(uint256 proposalId) public payable virtual; + /** + * @dev Cancels a proposal only if the sender is the proposer or the proposer delegates' voting power dropped below the proposal threshold. + */ + function cancel(uint256 proposalId) public virtual; + /** * @dev Part of the Governor Bravo's interface: _"Gets actions of a proposal"_. */ diff --git a/contracts/governance/extensions/GovernorPreventLateQuorum.sol b/contracts/governance/extensions/GovernorPreventLateQuorum.sol index 676d2b13e..68496ca1e 100644 --- a/contracts/governance/extensions/GovernorPreventLateQuorum.sol +++ b/contracts/governance/extensions/GovernorPreventLateQuorum.sol @@ -12,14 +12,12 @@ import "../../utils/math/Math.sol"; * and try to oppose the decision. * * If a vote causes quorum to be reached, the proposal's voting period may be extended so that it does not end before at - * least a given number of blocks have passed (the "vote extension" parameter). This parameter can be set by the - * governance executor (e.g. through a governance proposal). + * least a specified time has passed (the "vote extension" parameter). This parameter can be set through a governance + * proposal. * * _Available since v4.5._ */ abstract contract GovernorPreventLateQuorum is Governor { - using SafeCast for uint256; - uint64 private _voteExtension; /// @custom:oz-retyped-from mapping(uint256 => Timers.BlockNumber) @@ -32,9 +30,9 @@ abstract contract GovernorPreventLateQuorum is Governor { event LateQuorumVoteExtensionSet(uint64 oldVoteExtension, uint64 newVoteExtension); /** - * @dev Initializes the vote extension parameter: the number of blocks that are required to pass since a proposal - * reaches quorum until its voting period ends. If necessary the voting period will be extended beyond the one set - * at proposal creation. + * @dev Initializes the vote extension parameter: the time in either number of blocks or seconds (depending on the governor + * clock mode) that is required to pass since the moment a proposal reaches quorum until its voting period ends. If + * necessary the voting period will be extended beyond the one set during proposal creation. */ constructor(uint64 initialVoteExtension) { _setLateQuorumVoteExtension(initialVoteExtension); diff --git a/contracts/governance/extensions/GovernorTimelockCompound.sol b/contracts/governance/extensions/GovernorTimelockCompound.sol index 629f8f800..912171cc3 100644 --- a/contracts/governance/extensions/GovernorTimelockCompound.sol +++ b/contracts/governance/extensions/GovernorTimelockCompound.sol @@ -21,8 +21,6 @@ import "../../vendor/compound/ICompoundTimelock.sol"; * _Available since v4.3._ */ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { - using SafeCast for uint256; - ICompoundTimelock private _timelock; /// @custom:oz-retyped-from mapping(uint256 => GovernorTimelockCompound.ProposalTimelock) @@ -48,18 +46,18 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { } /** - * @dev Overridden version of the {Governor-state} function with added support for the `Queued` and `Expired` status. + * @dev Overridden version of the {Governor-state} function with added support for the `Queued` and `Expired` state. */ function state(uint256 proposalId) public view virtual override(IGovernor, Governor) returns (ProposalState) { - ProposalState status = super.state(proposalId); + ProposalState currentState = super.state(proposalId); - if (status != ProposalState.Succeeded) { - return status; + if (currentState != ProposalState.Succeeded) { + return currentState; } uint256 eta = proposalEta(proposalId); if (eta == 0) { - return status; + return currentState; } else if (block.timestamp >= eta + _timelock.GRACE_PERIOD()) { return ProposalState.Expired; } else { @@ -95,7 +93,7 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { require(state(proposalId) == ProposalState.Succeeded, "Governor: proposal not successful"); uint256 eta = block.timestamp + _timelock.delay(); - _proposalTimelocks[proposalId] = eta.toUint64(); + _proposalTimelocks[proposalId] = SafeCast.toUint64(eta); for (uint256 i = 0; i < targets.length; ++i) { require( diff --git a/contracts/governance/extensions/GovernorTimelockControl.sol b/contracts/governance/extensions/GovernorTimelockControl.sol index 6aa2556ab..0cf2ea5f0 100644 --- a/contracts/governance/extensions/GovernorTimelockControl.sol +++ b/contracts/governance/extensions/GovernorTimelockControl.sol @@ -47,19 +47,19 @@ abstract contract GovernorTimelockControl is IGovernorTimelock, Governor { } /** - * @dev Overridden version of the {Governor-state} function with added support for the `Queued` status. + * @dev Overridden version of the {Governor-state} function with added support for the `Queued` state. */ function state(uint256 proposalId) public view virtual override(IGovernor, Governor) returns (ProposalState) { - ProposalState status = super.state(proposalId); + ProposalState currentState = super.state(proposalId); - if (status != ProposalState.Succeeded) { - return status; + if (currentState != ProposalState.Succeeded) { + return currentState; } // core tracks execution, so we just have to check if successful proposal have been queued. bytes32 queueid = _timelockIds[proposalId]; if (queueid == bytes32(0)) { - return status; + return currentState; } else if (_timelock.isOperationDone(queueid)) { return ProposalState.Executed; } else if (_timelock.isOperationPending(queueid)) { diff --git a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol index 19c5b194d..403570260 100644 --- a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol +++ b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol @@ -14,7 +14,6 @@ import "../../utils/math/SafeCast.sol"; * _Available since v4.3._ */ abstract contract GovernorVotesQuorumFraction is GovernorVotes { - using SafeCast for *; using Checkpoints for Checkpoints.Trace224; uint256 private _quorumNumerator; // DEPRECATED in favor of _quorumNumeratorHistory @@ -59,7 +58,7 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes { } // Otherwise, do the binary search - return _quorumNumeratorHistory.upperLookupRecent(timepoint.toUint32()); + return _quorumNumeratorHistory.upperLookupRecent(SafeCast.toUint32(timepoint)); } /** @@ -110,12 +109,12 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes { // Make sure we keep track of the original numerator in contracts upgraded from a version without checkpoints. if (oldQuorumNumerator != 0 && _quorumNumeratorHistory._checkpoints.length == 0) { _quorumNumeratorHistory._checkpoints.push( - Checkpoints.Checkpoint224({_key: 0, _value: oldQuorumNumerator.toUint224()}) + Checkpoints.Checkpoint224({_key: 0, _value: SafeCast.toUint224(oldQuorumNumerator)}) ); } // Set new quorum for future proposals - _quorumNumeratorHistory.push(clock().toUint32(), newQuorumNumerator.toUint224()); + _quorumNumeratorHistory.push(SafeCast.toUint32(clock()), SafeCast.toUint224(newQuorumNumerator)); emit QuorumNumeratorUpdated(oldQuorumNumerator, newQuorumNumerator); } diff --git a/contracts/governance/utils/IVotes.sol b/contracts/governance/utils/IVotes.sol index c73d0732b..4f2b7eb85 100644 --- a/contracts/governance/utils/IVotes.sol +++ b/contracts/governance/utils/IVotes.sol @@ -25,13 +25,13 @@ interface IVotes { /** * @dev Returns the amount of votes that `account` had at a specific moment in the past. If the `clock()` is - * configured to use block numbers, this will return the value the end of the corresponding block. + * configured to use block numbers, this will return the value at the end of the corresponding block. */ function getPastVotes(address account, uint256 timepoint) external view returns (uint256); /** * @dev Returns the total supply of votes available at a specific moment in the past. If the `clock()` is - * configured to use block numbers, this will return the value the end of the corresponding block. + * configured to use block numbers, this will return the value at the end of the corresponding block. * * NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. * Votes that have not been delegated are still part of total supply, even though they would not participate in a diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index f70cf3831..b24ce824a 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -59,7 +59,7 @@ abstract contract Votes is Context, EIP712, IERC5805 { // solhint-disable-next-line func-name-mixedcase function CLOCK_MODE() public view virtual override returns (string memory) { // Check that the clock was not modified - require(clock() == block.number); + require(clock() == block.number, "Votes: broken clock mode"); return "mode=blocknumber&from=default"; } @@ -72,7 +72,7 @@ abstract contract Votes is Context, EIP712, IERC5805 { /** * @dev Returns the amount of votes that `account` had at a specific moment in the past. If the `clock()` is - * configured to use block numbers, this will return the value the end of the corresponding block. + * configured to use block numbers, this will return the value at the end of the corresponding block. * * Requirements: * @@ -85,7 +85,7 @@ abstract contract Votes is Context, EIP712, IERC5805 { /** * @dev Returns the total supply of votes available at a specific moment in the past. If the `clock()` is - * configured to use block numbers, this will return the value the end of the corresponding block. + * configured to use block numbers, this will return the value at the end of the corresponding block. * * NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. * Votes that have not been delegated are still part of total supply, even though they would not participate in a diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index f78938e6d..d72d34f81 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -50,7 +50,7 @@ abstract contract ERC20Votes is ERC20Permit, IERC5805 { // solhint-disable-next-line func-name-mixedcase function CLOCK_MODE() public view virtual override returns (string memory) { // Check that the clock was not modified - require(clock() == block.number); + require(clock() == block.number, "ERC20Votes: broken clock mode"); return "mode=blocknumber&from=default"; } diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 8e6500ec3..31397f107 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -34,6 +34,8 @@ abstract contract ERC721Votes is ERC721, Votes { /** * @dev Returns the balance of `account`. + * + * WARNING: Overriding this function will likely result in incorrect vote tracking. */ function _getVotingUnits(address account) internal view virtual override returns (uint256) { return balanceOf(account); diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js index 1eabaccf0..f867958b5 100644 --- a/test/governance/Governor.test.js +++ b/test/governance/Governor.test.js @@ -70,7 +70,7 @@ contract('Governor', function (accounts) { ); }); - shouldSupportInterfaces(['ERC165', 'ERC1155Receiver', 'Governor', 'GovernorWithParams']); + shouldSupportInterfaces(['ERC165', 'ERC1155Receiver', 'Governor', 'GovernorWithParams', 'GovernorCancel']); shouldBehaveLikeEIP6372(mode); it('deployment check', async function () { @@ -84,7 +84,7 @@ contract('Governor', function (accounts) { it('nominal workflow', async function () { // Before - expect(await this.mock.$_proposalProposer(this.proposal.id)).to.be.equal(constants.ZERO_ADDRESS); + expect(await this.mock.proposalProposer(this.proposal.id)).to.be.equal(constants.ZERO_ADDRESS); expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); @@ -149,7 +149,7 @@ contract('Governor', function (accounts) { await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); // After - expect(await this.mock.$_proposalProposer(this.proposal.id)).to.be.equal(proposer); + expect(await this.mock.proposalProposer(this.proposal.id)).to.be.equal(proposer); expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index 541f6c611..201a55f47 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -90,6 +90,7 @@ const INTERFACES = { 'castVoteBySig(uint256,uint8,uint8,bytes32,bytes32)', 'castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32)', ], + GovernorCancel: ['proposalProposer(uint256)', 'cancel(address[],uint256[],bytes[],bytes32)'], GovernorTimelock: ['timelock()', 'proposalEta(uint256)', 'queue(address[],uint256[],bytes[],bytes32)'], ERC2981: ['royaltyInfo(uint256,uint256)'], }; @@ -120,7 +121,7 @@ function shouldSupportInterfaces(interfaces = []) { it('all interfaces are reported as supported', async function () { for (const k of interfaces) { const interfaceId = INTERFACE_IDS[k] ?? k; - expect(await this.contractUnderTest.supportsInterface(interfaceId)).to.equal(true); + expect(await this.contractUnderTest.supportsInterface(interfaceId)).to.equal(true, `does not support ${k}`); } }); @@ -130,7 +131,10 @@ function shouldSupportInterfaces(interfaces = []) { if (INTERFACES[k] === undefined) continue; for (const fnName of INTERFACES[k]) { const fnSig = FN_SIGNATURES[fnName]; - expect(this.contractUnderTest.abi.filter(fn => fn.signature === fnSig).length).to.equal(1); + expect(this.contractUnderTest.abi.filter(fn => fn.signature === fnSig).length).to.equal( + 1, + `did not find ${fnName}`, + ); } } }); From 6aac66d065dc39795fbad1f680f80e3a3d70f2d1 Mon Sep 17 00:00:00 2001 From: Francisco Date: Mon, 24 Apr 2023 13:18:27 +0100 Subject: [PATCH 103/197] Merge release-v4.8 (#4188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hadrien Croubois Co-authored-by: Benjamin Co-authored-by: Owen Co-authored-by: Hadrien Croubois Co-authored-by: JulissaDantes Co-authored-by: Ernesto García Co-authored-by: Yamen Merhi Co-authored-by: Pascal Marco Caversaccio Co-authored-by: alpharush <0xalpharush@protonmail.com> Co-authored-by: Paul Razvan Berg --- .changeset/silent-pugs-scream.md | 5 ----- .changeset/thirty-shrimps-mix.md | 5 ----- CHANGELOG.md | 5 +++++ .../governance/compatibility/GovernorCompatibilityBravo.sol | 2 +- contracts/proxy/ERC1967/ERC1967Upgrade.sol | 2 +- contracts/proxy/transparent/ProxyAdmin.sol | 2 +- contracts/proxy/transparent/TransparentUpgradeableProxy.sol | 2 +- 7 files changed, 9 insertions(+), 14 deletions(-) delete mode 100644 .changeset/silent-pugs-scream.md delete mode 100644 .changeset/thirty-shrimps-mix.md diff --git a/.changeset/silent-pugs-scream.md b/.changeset/silent-pugs-scream.md deleted file mode 100644 index c92d12486..000000000 --- a/.changeset/silent-pugs-scream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`GovernorCompatibilityBravo`: Fix encoding of proposal data when signatures are missing. diff --git a/.changeset/thirty-shrimps-mix.md b/.changeset/thirty-shrimps-mix.md deleted file mode 100644 index 656edbf92..000000000 --- a/.changeset/thirty-shrimps-mix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`TransparentUpgradeableProxy`: Fix transparency in case of selector clash with non-decodable calldata or payable mutability. diff --git a/CHANGELOG.md b/CHANGELOG.md index 333b6d4a3..ffbd3acea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ - `ERC777`: The `ERC777` token standard is no longer supported by OpenZeppelin. Our implementation is now deprecated and will be removed in the next major release. The corresponding standard interfaces remain available. ([#4066](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4066)) - `ERC1820Implementer`: The `ERC1820` pseudo-introspection mechanism is no longer supported by OpenZeppelin. Our implementation is now deprecated and will be removed in the next major release. The corresponding standard interfaces remain available. ([#4066](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4066)) +## 4.8.3 (2023-04-13) + +- `GovernorCompatibilityBravo`: Fix encoding of proposal data when signatures are missing. +- `TransparentUpgradeableProxy`: Fix transparency in case of selector clash with non-decodable calldata or payable mutability. ([#4154](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4154)) + ## 4.8.2 (2023-03-02) - `ERC721Consecutive`: Fixed a bug when `_mintConsecutive` is used for batches of size 1 that could lead to balance overflow. Refer to the breaking changes section in the changelog for a note on the behavior of `ERC721._beforeTokenTransfer`. diff --git a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol index 0fbb4e17c..1332ac79d 100644 --- a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (governance/compatibility/GovernorCompatibilityBravo.sol) +// OpenZeppelin Contracts (last updated v4.8.3) (governance/compatibility/GovernorCompatibilityBravo.sol) pragma solidity ^0.8.0; diff --git a/contracts/proxy/ERC1967/ERC1967Upgrade.sol b/contracts/proxy/ERC1967/ERC1967Upgrade.sol index a79c10502..3942ca699 100644 --- a/contracts/proxy/ERC1967/ERC1967Upgrade.sol +++ b/contracts/proxy/ERC1967/ERC1967Upgrade.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (proxy/ERC1967/ERC1967Upgrade.sol) +// OpenZeppelin Contracts (last updated v4.8.3) (proxy/ERC1967/ERC1967Upgrade.sol) pragma solidity ^0.8.2; diff --git a/contracts/proxy/transparent/ProxyAdmin.sol b/contracts/proxy/transparent/ProxyAdmin.sol index 1a9698196..571530595 100644 --- a/contracts/proxy/transparent/ProxyAdmin.sol +++ b/contracts/proxy/transparent/ProxyAdmin.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (proxy/transparent/ProxyAdmin.sol) +// OpenZeppelin Contracts (last updated v4.8.3) (proxy/transparent/ProxyAdmin.sol) pragma solidity ^0.8.0; diff --git a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol index d8765fc9e..4e2b0c759 100644 --- a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (proxy/transparent/TransparentUpgradeableProxy.sol) +// OpenZeppelin Contracts (last updated v4.8.3) (proxy/transparent/TransparentUpgradeableProxy.sol) pragma solidity ^0.8.0; From 1a079d258b15409970f181702669fa8738d0edef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 25 Apr 2023 13:31:01 +0200 Subject: [PATCH 104/197] Improve Address tests (#4191) --- contracts/mocks/CallReceiverMock.sol | 4 +++ contracts/utils/Address.sol | 2 +- test/utils/Address.test.js | 40 ++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/contracts/mocks/CallReceiverMock.sol b/contracts/mocks/CallReceiverMock.sol index 344a1054b..492adbe92 100644 --- a/contracts/mocks/CallReceiverMock.sol +++ b/contracts/mocks/CallReceiverMock.sol @@ -14,6 +14,10 @@ contract CallReceiverMock { return "0x1234"; } + function mockFunctionEmptyReturn() public payable { + emit MockFunctionCalled(); + } + function mockFunctionWithArgs(uint256 a, uint256 b) public payable returns (string memory) { emit MockFunctionCalledWithArgs(a, b); diff --git a/contracts/utils/Address.sol b/contracts/utils/Address.sol index 433a866d7..5ff14140a 100644 --- a/contracts/utils/Address.sol +++ b/contracts/utils/Address.sol @@ -59,7 +59,7 @@ library Address { * IMPORTANT: because control is transferred to `recipient`, care must be * taken to not create reentrancy vulnerabilities. Consider using * {ReentrancyGuard} or the - * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + * 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"); diff --git a/test/utils/Address.test.js b/test/utils/Address.test.js index a78ae14e6..4f9f9eea1 100644 --- a/test/utils/Address.test.js +++ b/test/utils/Address.test.js @@ -107,6 +107,14 @@ contract('Address', function (accounts) { await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); }); + it('calls the requested empty return function', async function () { + const abiEncodedCall = this.target.contract.methods.mockFunctionEmptyReturn().encodeABI(); + + const receipt = await this.mock.$functionCall(this.target.address, abiEncodedCall); + + await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); + }); + it('reverts when the called function reverts with no reason', async function () { const abiEncodedCall = this.target.contract.methods.mockFunctionRevertsNoReason().encodeABI(); @@ -137,6 +145,11 @@ contract('Address', function (accounts) { await expectRevert.unspecified(this.mock.$functionCall(this.target.address, abiEncodedCall)); }); + it('bubbles up error message if specified', async function () { + const errorMsg = 'Address: expected error'; + await expectRevert(this.mock.$functionCall(this.target.address, '0x12345678', errorMsg), errorMsg); + }); + it('reverts when function does not exist', async function () { const abiEncodedCall = web3.eth.abi.encodeFunctionCall( { @@ -237,6 +250,11 @@ contract('Address', function (accounts) { 'Address: low-level call with value failed', ); }); + + it('bubbles up error message if specified', async function () { + const errorMsg = 'Address: expected error'; + await expectRevert(this.mock.$functionCallWithValue(this.target.address, '0x12345678', 0, errorMsg), errorMsg); + }); }); }); @@ -277,6 +295,11 @@ contract('Address', function (accounts) { await expectRevert(this.mock.$functionStaticCall(recipient, abiEncodedCall), 'Address: call to non-contract'); }); + + it('bubbles up error message if specified', async function () { + const errorMsg = 'Address: expected error'; + await expectRevert(this.mock.$functionCallWithValue(this.target.address, '0x12345678', 0, errorMsg), errorMsg); + }); }); describe('functionDelegateCall', function () { @@ -317,5 +340,22 @@ contract('Address', function (accounts) { await expectRevert(this.mock.$functionDelegateCall(recipient, abiEncodedCall), 'Address: call to non-contract'); }); + + it('bubbles up error message if specified', async function () { + const errorMsg = 'Address: expected error'; + await expectRevert(this.mock.$functionCallWithValue(this.target.address, '0x12345678', 0, errorMsg), errorMsg); + }); + }); + + describe('verifyCallResult', function () { + it('returns returndata on success', async function () { + const returndata = '0x123abc'; + expect(await this.mock.$verifyCallResult(true, returndata, '')).to.equal(returndata); + }); + + it('reverts with return data and error m', async function () { + const errorMsg = 'Address: expected error'; + await expectRevert(this.mock.$verifyCallResult(false, '0x', errorMsg), errorMsg); + }); }); }); From 8f14d52b7366eb8dcdbf40a92b2d1cb14ded9a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 25 Apr 2023 17:57:16 +0200 Subject: [PATCH 105/197] Fix Checkpoints fuzz overflow (#4184) Co-authored-by: Francisco --- scripts/generate/templates/Checkpoints.t.js | 22 ++++++++++------- test/utils/Checkpoints.t.sol | 26 +++++++++++++-------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/scripts/generate/templates/Checkpoints.t.js b/scripts/generate/templates/Checkpoints.t.js index 84b5992ad..b3da933a1 100644 --- a/scripts/generate/templates/Checkpoints.t.js +++ b/scripts/generate/templates/Checkpoints.t.js @@ -86,10 +86,12 @@ function testPush( if (keys.length > 0) { ${opts.keyTypeName} lastKey = keys[keys.length - 1]; - pastKey = _bound${capitalize(opts.keyTypeName)}(pastKey, 0, lastKey - 1); - - vm.expectRevert(); - this.push(pastKey, values[keys.length % values.length]); + if (lastKey > 0) { + pastKey = _bound${capitalize(opts.keyTypeName)}(pastKey, 0, lastKey - 1); + + vm.expectRevert(); + this.push(pastKey, values[keys.length % values.length]); + } } } @@ -173,11 +175,13 @@ function testPush( // Can't push any key in the past if (keys.length > 0) { ${opts.keyTypeName} lastKey = keys[keys.length - 1]; - pastKey = _bound${capitalize(opts.keyTypeName)}(pastKey, 0, lastKey - 1); - - vm.roll(pastKey); - vm.expectRevert(); - this.push(values[keys.length % values.length]); + if (lastKey > 0) { + pastKey = _bound${capitalize(opts.keyTypeName)}(pastKey, 0, lastKey - 1); + + vm.roll(pastKey); + vm.expectRevert(); + this.push(values[keys.length % values.length]); + } } } diff --git a/test/utils/Checkpoints.t.sol b/test/utils/Checkpoints.t.sol index abdf8b436..f2cb587d5 100644 --- a/test/utils/Checkpoints.t.sol +++ b/test/utils/Checkpoints.t.sol @@ -66,11 +66,13 @@ contract CheckpointsHistoryTest is Test { // Can't push any key in the past if (keys.length > 0) { uint32 lastKey = keys[keys.length - 1]; - pastKey = _boundUint32(pastKey, 0, lastKey - 1); + if (lastKey > 0) { + pastKey = _boundUint32(pastKey, 0, lastKey - 1); - vm.roll(pastKey); - vm.expectRevert(); - this.push(values[keys.length % values.length]); + vm.roll(pastKey); + vm.expectRevert(); + this.push(values[keys.length % values.length]); + } } } @@ -185,10 +187,12 @@ contract CheckpointsTrace224Test is Test { if (keys.length > 0) { uint32 lastKey = keys[keys.length - 1]; - pastKey = _boundUint32(pastKey, 0, lastKey - 1); + if (lastKey > 0) { + pastKey = _boundUint32(pastKey, 0, lastKey - 1); - vm.expectRevert(); - this.push(pastKey, values[keys.length % values.length]); + vm.expectRevert(); + this.push(pastKey, values[keys.length % values.length]); + } } } @@ -291,10 +295,12 @@ contract CheckpointsTrace160Test is Test { if (keys.length > 0) { uint96 lastKey = keys[keys.length - 1]; - pastKey = _boundUint96(pastKey, 0, lastKey - 1); + if (lastKey > 0) { + pastKey = _boundUint96(pastKey, 0, lastKey - 1); - vm.expectRevert(); - this.push(pastKey, values[keys.length % values.length]); + vm.expectRevert(); + this.push(pastKey, values[keys.length % values.length]); + } } } From f959d7e4e6ee0b022b41e5b644c79369869d8411 Mon Sep 17 00:00:00 2001 From: Pascal Marco Caversaccio Date: Tue, 25 Apr 2023 17:57:50 +0200 Subject: [PATCH 106/197] Fix release note in `IERC1967` (#4183) --- contracts/interfaces/IERC1967.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/interfaces/IERC1967.sol b/contracts/interfaces/IERC1967.sol index e5deebee9..ab4450eec 100644 --- a/contracts/interfaces/IERC1967.sol +++ b/contracts/interfaces/IERC1967.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; /** * @dev ERC-1967: Proxy Storage Slots. This interface contains the events defined in the ERC. * - * _Available since v4.9._ + * _Available since v4.8.3._ */ interface IERC1967 { /** From 44d6053b4376421a5b45ace021934b72d649d09b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 28 Apr 2023 14:01:41 +0200 Subject: [PATCH 107/197] Only run FV on new or updated specs (#4195) --- .github/workflows/formal-verification.yml | 14 ++++- certora/run.js | 64 ++++++++++++++++------- certora/specs/Ownable.spec | 4 +- 3 files changed, 61 insertions(+), 21 deletions(-) diff --git a/.github/workflows/formal-verification.yml b/.github/workflows/formal-verification.yml index 29c02541c..f94152a52 100644 --- a/.github/workflows/formal-verification.yml +++ b/.github/workflows/formal-verification.yml @@ -33,8 +33,20 @@ jobs: if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'formal-verification') steps: - uses: actions/checkout@v3 + with: + fetch-depth: 0 - name: Set up environment uses: ./.github/actions/setup + - name: identify specs that need to be run + id: arguments + run: | + if [[ ${{ github.event_name }} = 'pull_request' ]]; + then + RESULT=$(git diff ${{ github.event.pull_request.head.sha }}..${{ github.event.pull_request.base.sha }} --name-only certora/specs/*.spec | while IFS= read -r file; do [[ -f $file ]] && basename "${file%.spec}"; done | tr "\n" " ") + else + RESULT='--all' + fi + echo "result=$RESULT" >> "$GITHUB_OUTPUT" - name: Install python uses: actions/setup-python@v4 with: @@ -55,6 +67,6 @@ jobs: - name: Verify specification run: | make -C certora apply - node certora/run.js >> "$GITHUB_STEP_SUMMARY" + node certora/run.js ${{ steps.arguments.outputs.result }} >> "$GITHUB_STEP_SUMMARY" env: CERTORAKEY: ${{ secrets.CERTORAKEY }} diff --git a/certora/run.js b/certora/run.js index f3234c1a3..2dc7d0181 100644 --- a/certora/run.js +++ b/certora/run.js @@ -1,37 +1,65 @@ #!/usr/bin/env node // USAGE: -// node certora/run.js [[CONTRACT_NAME:]SPEC_NAME] [OPTIONS...] +// node certora/run.js [[CONTRACT_NAME:]SPEC_NAME]* [--all] [--options OPTIONS...] [--specs PATH] // EXAMPLES: +// node certora/run.js --all // node certora/run.js AccessControl // node certora/run.js AccessControlHarness:AccessControl -const MAX_PARALLEL = 4; - -let specs = require(__dirname + '/specs.json'); - const proc = require('child_process'); const { PassThrough } = require('stream'); const events = require('events'); -const limit = require('p-limit')(MAX_PARALLEL); -let [, , request = '', ...extraOptions] = process.argv; -if (request.startsWith('-')) { - extraOptions.unshift(request); - request = ''; +const argv = require('yargs') + .env('') + .options({ + all: { + alias: 'a', + type: 'boolean', + }, + spec: { + alias: 's', + type: 'string', + default: __dirname + '/specs.json', + }, + parallel: { + alias: 'p', + type: 'number', + default: 4, + }, + options: { + alias: 'o', + type: 'array', + default: [], + }, + }).argv; + +function match(entry, request) { + const [reqSpec, reqContract] = request.split(':').reverse(); + return entry.spec == reqSpec && (!reqContract || entry.contract == reqContract); } -if (request) { - const [reqSpec, reqContract] = request.split(':').reverse(); - specs = Object.values(specs).filter(s => reqSpec === s.spec && (!reqContract || reqContract === s.contract)); - if (specs.length === 0) { - console.error(`Error: Requested spec '${request}' not found in specs.json`); - process.exit(1); +const specs = require(argv.spec).filter(s => argv.all || argv._.some(r => match(s, r))); +const limit = require('p-limit')(argv.parallel); + +if (argv._.length == 0 && !argv.all) { + console.error(`Warning: No specs requested. Did you forgot to toggle '--all'?`); +} + +for (const r of argv._) { + if (!specs.some(s => match(s, r))) { + console.error(`Error: Requested spec '${r}' not found in ${argv.spec}`); + process.exitCode = 1; } } -for (const { spec, contract, files, options = [] } of Object.values(specs)) { - limit(runCertora, spec, contract, files, [...options.flatMap(opt => opt.split(' ')), ...extraOptions]); +if (process.exitCode) { + process.exit(process.exitCode); +} + +for (const { spec, contract, files, options = [] } of specs) { + limit(runCertora, spec, contract, files, [...options.flatMap(opt => opt.split(' ')), ...argv.options]); } // Run certora, aggregate the output and print it at the end diff --git a/certora/specs/Ownable.spec b/certora/specs/Ownable.spec index 48bd84d13..4fdfeb09c 100644 --- a/certora/specs/Ownable.spec +++ b/certora/specs/Ownable.spec @@ -62,10 +62,10 @@ rule onlyCurrentOwnerCanCallOnlyOwner(env e) { │ Rule: ownership can only change in specific ways │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule onlyOwnerOrPendingOwnerCanChangeOwnership(env e, method f) { +rule onlyOwnerOrPendingOwnerCanChangeOwnership(env e) { address oldCurrent = owner(); - calldataarg args; + method f; calldataarg args; f(e, args); address newCurrent = owner(); From d23f818a59b47e496a431e3e7c8b89dffe4f74d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 28 Apr 2023 15:09:58 +0200 Subject: [PATCH 108/197] Fix AccessControlDefaultAdminRules admin consistency (#4177) Co-authored-by: Francisco Co-authored-by: Hadrien Croubois --- .../access/AccessControlDefaultAdminRules.sol | 2 +- test/access/AccessControl.behavior.js | 30 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/contracts/access/AccessControlDefaultAdminRules.sol b/contracts/access/AccessControlDefaultAdminRules.sol index 43fca9350..0c640fb99 100644 --- a/contracts/access/AccessControlDefaultAdminRules.sol +++ b/contracts/access/AccessControlDefaultAdminRules.sol @@ -136,7 +136,7 @@ abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRu * @dev See {AccessControl-_revokeRole}. */ function _revokeRole(bytes32 role, address account) internal virtual override { - if (role == DEFAULT_ADMIN_ROLE) { + if (role == DEFAULT_ADMIN_ROLE && account == _currentDefaultAdmin) { delete _currentDefaultAdmin; } super._revokeRole(role, account); diff --git a/test/access/AccessControl.behavior.js b/test/access/AccessControl.behavior.js index 6c88aa274..49ab44b58 100644 --- a/test/access/AccessControl.behavior.js +++ b/test/access/AccessControl.behavior.js @@ -513,15 +513,12 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa }); describe('when caller is pending default admin and delay has passed', function () { - let from; - beforeEach(async function () { await time.setNextBlockTimestamp(acceptSchedule.addn(1)); - from = newDefaultAdmin; }); it('accepts a transfer and changes default admin', async function () { - const receipt = await this.accessControl.acceptDefaultAdminTransfer({ from }); + const receipt = await this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }); // Storage changes expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, defaultAdmin)).to.be.false; @@ -625,10 +622,9 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa describe('renounces admin', function () { let delayPassed; - let from = defaultAdmin; beforeEach(async function () { - await this.accessControl.beginDefaultAdminTransfer(constants.ZERO_ADDRESS, { from }); + await this.accessControl.beginDefaultAdminTransfer(constants.ZERO_ADDRESS, { from: defaultAdmin }); delayPassed = web3.utils .toBN(await time.latest()) .add(delay) @@ -638,27 +634,37 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa it('reverts if caller is not default admin', async function () { await time.setNextBlockTimestamp(delayPassed); await expectRevert( - this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, other, { from }), + this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, other, { from: defaultAdmin }), `${errorPrefix}: can only renounce roles for self`, ); }); + it('keeps defaultAdmin consistent with hasRole if another non-defaultAdmin user renounces the DEFAULT_ADMIN_ROLE', async function () { + await time.setNextBlockTimestamp(delayPassed); + + // This passes because it's a noop + await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, other, { from: other }); + + expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, defaultAdmin)).to.be.true; + expect(await this.accessControl.defaultAdmin()).to.be.equal(defaultAdmin); + }); + it('renounces role', async function () { await time.setNextBlockTimestamp(delayPassed); - const receipt = await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, from, { from }); + const receipt = await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }); expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, defaultAdmin)).to.be.false; - expect(await this.accessControl.hasRole(constants.ZERO_ADDRESS, defaultAdmin)).to.be.false; + expect(await this.accessControl.defaultAdmin()).to.be.equal(constants.ZERO_ADDRESS); expectEvent(receipt, 'RoleRevoked', { role: DEFAULT_ADMIN_ROLE, - account: from, + account: defaultAdmin, }); expect(await this.accessControl.owner()).to.equal(constants.ZERO_ADDRESS); }); it('allows to recover access using the internal _grantRole', async function () { await time.setNextBlockTimestamp(delayPassed); - await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, from, { from }); + await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }); const grantRoleReceipt = await this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, other); expectEvent(grantRoleReceipt, 'RoleGranted', { @@ -681,7 +687,7 @@ function shouldBehaveLikeAccessControlDefaultAdminRules(errorPrefix, delay, defa it(`reverts if block.timestamp is ${tag} to schedule`, async function () { await time.setNextBlockTimestamp(delayNotPassed.toNumber() + fromSchedule); await expectRevert( - this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from }), + this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }), `${errorPrefix}: only can renounce in two delayed steps`, ); }); From 6ff415de6be00f52900c8573cc761b807acfd88b Mon Sep 17 00:00:00 2001 From: Francisco Date: Fri, 28 Apr 2023 21:15:15 +0100 Subject: [PATCH 109/197] Downgrade Node for Slither (#4202) --- .github/workflows/checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index c6b42dd82..2d5c15159 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -86,7 +86,7 @@ jobs: - run: rm foundry.toml - uses: crytic/slither-action@v0.3.0 with: - node-version: 18 + node-version: 18.15 codespell: if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' From ab2604ac5b791adf3c5e2397e65128cb56954edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 2 May 2023 11:36:56 +0200 Subject: [PATCH 110/197] Add reentrancy test cases for TimelockController (#4200) Co-authored-by: Francisco --- contracts/mocks/TimelockReentrant.sol | 26 ++++ test/governance/TimelockController.test.js | 155 +++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 contracts/mocks/TimelockReentrant.sol diff --git a/contracts/mocks/TimelockReentrant.sol b/contracts/mocks/TimelockReentrant.sol new file mode 100644 index 000000000..a9344f50d --- /dev/null +++ b/contracts/mocks/TimelockReentrant.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../utils/Address.sol"; + +contract TimelockReentrant { + address private _reenterTarget; + bytes private _reenterData; + bool _reentered; + + function disableReentrancy() external { + _reentered = true; + } + + function enableRentrancy(address target, bytes calldata data) external { + _reenterTarget = target; + _reenterData = data; + } + + function reenter() external { + if (!_reentered) { + _reentered = true; + Address.functionCall(_reenterTarget, _reenterData); + } + } +} diff --git a/test/governance/TimelockController.test.js b/test/governance/TimelockController.test.js index dde923564..82a84746e 100644 --- a/test/governance/TimelockController.test.js +++ b/test/governance/TimelockController.test.js @@ -10,6 +10,7 @@ const CallReceiverMock = artifacts.require('CallReceiverMock'); const Implementation2 = artifacts.require('Implementation2'); const ERC721 = artifacts.require('$ERC721'); const ERC1155 = artifacts.require('$ERC1155'); +const TimelockReentrant = artifacts.require('$TimelockReentrant'); const MINDELAY = time.duration.days(1); @@ -345,6 +346,82 @@ contract('TimelockController', function (accounts) { `AccessControl: account ${other.toLowerCase()} is missing role ${EXECUTOR_ROLE}`, ); }); + + it('prevents reentrancy execution', async function () { + // Create operation + const reentrant = await TimelockReentrant.new(); + const reentrantOperation = genOperation( + reentrant.address, + 0, + reentrant.contract.methods.reenter().encodeABI(), + ZERO_BYTES32, + salt, + ); + + // Schedule so it can be executed + await this.mock.schedule( + reentrantOperation.target, + reentrantOperation.value, + reentrantOperation.data, + reentrantOperation.predecessor, + reentrantOperation.salt, + MINDELAY, + { from: proposer }, + ); + + // Advance on time to make the operation executable + const timestamp = await this.mock.getTimestamp(reentrantOperation.id); + await time.increaseTo(timestamp); + + // Grant executor role to the reentrant contract + await this.mock.grantRole(EXECUTOR_ROLE, reentrant.address, { from: admin }); + + // Prepare reenter + const data = this.mock.contract.methods + .execute( + reentrantOperation.target, + reentrantOperation.value, + reentrantOperation.data, + reentrantOperation.predecessor, + reentrantOperation.salt, + ) + .encodeABI(); + await reentrant.enableRentrancy(this.mock.address, data); + + // Expect to fail + await expectRevert( + this.mock.execute( + reentrantOperation.target, + reentrantOperation.value, + reentrantOperation.data, + reentrantOperation.predecessor, + reentrantOperation.salt, + { from: executor }, + ), + 'TimelockController: operation is not ready', + ); + + // Disable reentrancy + await reentrant.disableReentrancy(); + const nonReentrantOperation = reentrantOperation; // Not anymore + + // Try again successfully + const receipt = await this.mock.execute( + nonReentrantOperation.target, + nonReentrantOperation.value, + nonReentrantOperation.data, + nonReentrantOperation.predecessor, + nonReentrantOperation.salt, + { from: executor }, + ); + expectEvent(receipt, 'CallExecuted', { + id: nonReentrantOperation.id, + index: web3.utils.toBN(0), + target: nonReentrantOperation.target, + value: web3.utils.toBN(nonReentrantOperation.value), + data: nonReentrantOperation.data, + }); + }); }); }); }); @@ -632,6 +709,84 @@ contract('TimelockController', function (accounts) { 'TimelockController: length mismatch', ); }); + + it('prevents reentrancy execution', async function () { + // Create operation + const reentrant = await TimelockReentrant.new(); + const reentrantBatchOperation = genOperationBatch( + [reentrant.address], + [0], + [reentrant.contract.methods.reenter().encodeABI()], + ZERO_BYTES32, + salt, + ); + + // Schedule so it can be executed + await this.mock.scheduleBatch( + reentrantBatchOperation.targets, + reentrantBatchOperation.values, + reentrantBatchOperation.payloads, + reentrantBatchOperation.predecessor, + reentrantBatchOperation.salt, + MINDELAY, + { from: proposer }, + ); + + // Advance on time to make the operation executable + const timestamp = await this.mock.getTimestamp(reentrantBatchOperation.id); + await time.increaseTo(timestamp); + + // Grant executor role to the reentrant contract + await this.mock.grantRole(EXECUTOR_ROLE, reentrant.address, { from: admin }); + + // Prepare reenter + const data = this.mock.contract.methods + .executeBatch( + reentrantBatchOperation.targets, + reentrantBatchOperation.values, + reentrantBatchOperation.payloads, + reentrantBatchOperation.predecessor, + reentrantBatchOperation.salt, + ) + .encodeABI(); + await reentrant.enableRentrancy(this.mock.address, data); + + // Expect to fail + await expectRevert( + this.mock.executeBatch( + reentrantBatchOperation.targets, + reentrantBatchOperation.values, + reentrantBatchOperation.payloads, + reentrantBatchOperation.predecessor, + reentrantBatchOperation.salt, + { from: executor }, + ), + 'TimelockController: operation is not ready', + ); + + // Disable reentrancy + await reentrant.disableReentrancy(); + const nonReentrantBatchOperation = reentrantBatchOperation; // Not anymore + + // Try again successfully + const receipt = await this.mock.executeBatch( + nonReentrantBatchOperation.targets, + nonReentrantBatchOperation.values, + nonReentrantBatchOperation.payloads, + nonReentrantBatchOperation.predecessor, + nonReentrantBatchOperation.salt, + { from: executor }, + ); + for (const i in nonReentrantBatchOperation.targets) { + expectEvent(receipt, 'CallExecuted', { + id: nonReentrantBatchOperation.id, + index: web3.utils.toBN(i), + target: nonReentrantBatchOperation.targets[i], + value: web3.utils.toBN(nonReentrantBatchOperation.values[i]), + data: nonReentrantBatchOperation.payloads[i], + }); + } + }); }); }); From 0a2a33be301720ae6a0aabcd739e74606bc35ef6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 3 May 2023 04:54:23 +0200 Subject: [PATCH 111/197] Add formal verification specs for EnumerableSet & EnumerableMap (#4167) Co-authored-by: Francisco --- certora/harnesses/EnumerableMapHarness.sol | 55 ++++ certora/harnesses/EnumerableSetHarness.sol | 35 +++ certora/run.js | 0 certora/specs.json | 10 + certora/specs/EnumerableMap.spec | 334 +++++++++++++++++++++ certora/specs/EnumerableSet.spec | 247 +++++++++++++++ 6 files changed, 681 insertions(+) create mode 100644 certora/harnesses/EnumerableMapHarness.sol create mode 100644 certora/harnesses/EnumerableSetHarness.sol mode change 100644 => 100755 certora/run.js create mode 100644 certora/specs/EnumerableMap.spec create mode 100644 certora/specs/EnumerableSet.spec diff --git a/certora/harnesses/EnumerableMapHarness.sol b/certora/harnesses/EnumerableMapHarness.sol new file mode 100644 index 000000000..3bcf1b50a --- /dev/null +++ b/certora/harnesses/EnumerableMapHarness.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../patched/utils/structs/EnumerableMap.sol"; + +contract EnumerableMapHarness { + using EnumerableMap for EnumerableMap.Bytes32ToBytes32Map; + + EnumerableMap.Bytes32ToBytes32Map private _map; + + function set(bytes32 key, bytes32 value) public returns (bool) { + return _map.set(key, value); + } + + function remove(bytes32 key) public returns (bool) { + return _map.remove(key); + } + + function contains(bytes32 key) public view returns (bool) { + return _map.contains(key); + } + + function length() public view returns (uint256) { + return _map.length(); + } + + function key_at(uint256 index) public view returns (bytes32) { + (bytes32 key,) = _map.at(index); + return key; + } + + function value_at(uint256 index) public view returns (bytes32) { + (,bytes32 value) = _map.at(index); + return value; + } + + function tryGet_contains(bytes32 key) public view returns (bool) { + (bool contained,) = _map.tryGet(key); + return contained; + } + + function tryGet_value(bytes32 key) public view returns (bytes32) { + (,bytes32 value) = _map.tryGet(key); + return value; + } + + function get(bytes32 key) public view returns (bytes32) { + return _map.get(key); + } + + function _indexOf(bytes32 key) public view returns (uint256) { + return _map._keys._inner._indexes[key]; + } +} diff --git a/certora/harnesses/EnumerableSetHarness.sol b/certora/harnesses/EnumerableSetHarness.sol new file mode 100644 index 000000000..64383e6a4 --- /dev/null +++ b/certora/harnesses/EnumerableSetHarness.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../patched/utils/structs/EnumerableSet.sol"; + +contract EnumerableSetHarness { + using EnumerableSet for EnumerableSet.Bytes32Set; + + EnumerableSet.Bytes32Set private _set; + + function add(bytes32 value) public returns (bool) { + return _set.add(value); + } + + function remove(bytes32 value) public returns (bool) { + return _set.remove(value); + } + + function contains(bytes32 value) public view returns (bool) { + return _set.contains(value); + } + + function length() public view returns (uint256) { + return _set.length(); + } + + function at_(uint256 index) public view returns (bytes32) { + return _set.at(index); + } + + function _indexOf(bytes32 value) public view returns (uint256) { + return _set._inner._indexes[value]; + } +} diff --git a/certora/run.js b/certora/run.js old mode 100644 new mode 100755 diff --git a/certora/specs.json b/certora/specs.json index 39ba8c235..6f8f57bdf 100644 --- a/certora/specs.json +++ b/certora/specs.json @@ -62,6 +62,16 @@ "contract": "InitializableHarness", "files": ["certora/harnesses/InitializableHarness.sol"] }, + { + "spec": "EnumerableSet", + "contract": "EnumerableSetHarness", + "files": ["certora/harnesses/EnumerableSetHarness.sol"] + }, + { + "spec": "EnumerableMap", + "contract": "EnumerableMapHarness", + "files": ["certora/harnesses/EnumerableMapHarness.sol"] + }, { "spec": "TimelockController", "contract": "TimelockControllerHarness", diff --git a/certora/specs/EnumerableMap.spec b/certora/specs/EnumerableMap.spec new file mode 100644 index 000000000..56ef854c6 --- /dev/null +++ b/certora/specs/EnumerableMap.spec @@ -0,0 +1,334 @@ +import "helpers.spec" + +methods { + // library + set(bytes32,bytes32) returns (bool) envfree + remove(bytes32) returns (bool) envfree + contains(bytes32) returns (bool) envfree + length() returns (uint256) envfree + key_at(uint256) returns (bytes32) envfree + value_at(uint256) returns (bytes32) envfree + tryGet_contains(bytes32) returns (bool) envfree + tryGet_value(bytes32) returns (bytes32) envfree + get(bytes32) returns (bytes32) envfree + + // FV + _indexOf(bytes32) returns (uint256) envfree +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Helpers │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +function sanity() returns bool { + return length() < max_uint256; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: the value mapping is empty for keys that are not in the EnumerableMap. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant noValueIfNotContained(bytes32 key) + !contains(key) => tryGet_value(key) == 0 + { + preserved set(bytes32 otherKey, bytes32 someValue) { + require sanity(); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: All indexed keys are contained │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant indexedContained(uint256 index) + index < length() => contains(key_at(index)) + { + preserved { + requireInvariant consistencyIndex(index); + requireInvariant consistencyIndex(to_uint256(length() - 1)); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: A value can only be stored at a single location │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant atUniqueness(uint256 index1, uint256 index2) + index1 == index2 <=> key_at(index1) == key_at(index2) + { + preserved remove(bytes32 key) { + requireInvariant atUniqueness(index1, to_uint256(length() - 1)); + requireInvariant atUniqueness(index2, to_uint256(length() - 1)); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: index <> value relationship is consistent │ +│ │ +│ Note that the two consistencyXxx invariants, put together, prove that at_ and _indexOf are inverse of one another. │ +│ This proves that we have a bijection between indices (the enumerability part) and keys (the entries that are set │ +│ and removed from the EnumerableMap). │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant consistencyIndex(uint256 index) + index < length() => _indexOf(key_at(index)) == index + 1 + { + preserved remove(bytes32 key) { + requireInvariant consistencyIndex(to_uint256(length() - 1)); + } + } + +invariant consistencyKey(bytes32 key) + contains(key) => ( + _indexOf(key) > 0 && + _indexOf(key) <= length() && + key_at(to_uint256(_indexOf(key) - 1)) == key + ) + { + preserved remove(bytes32 otherKey) { + requireInvariant consistencyKey(otherKey); + requireInvariant atUniqueness( + to_uint256(_indexOf(key) - 1), + to_uint256(_indexOf(otherKey) - 1) + ); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: state only changes by setting or removing elements │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule stateChange(env e, bytes32 key) { + require sanity(); + requireInvariant consistencyKey(key); + + uint256 lengthBefore = length(); + bool containsBefore = contains(key); + bytes32 valueBefore = tryGet_value(key); + + method f; + calldataarg args; + f(e, args); + + uint256 lengthAfter = length(); + bool containsAfter = contains(key); + bytes32 valueAfter = tryGet_value(key); + + assert lengthBefore != lengthAfter => ( + (f.selector == set(bytes32,bytes32).selector && lengthAfter == lengthBefore + 1) || + (f.selector == remove(bytes32).selector && lengthAfter == lengthBefore - 1) + ); + + assert containsBefore != containsAfter => ( + (f.selector == set(bytes32,bytes32).selector && containsAfter) || + (f.selector == remove(bytes32).selector && !containsAfter) + ); + + assert valueBefore != valueAfter => ( + (f.selector == set(bytes32,bytes32).selector && containsAfter) || + (f.selector == remove(bytes32).selector && !containsAfter && valueAfter == 0) + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: check liveness of view functions. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule liveness_1(bytes32 key) { + requireInvariant consistencyKey(key); + + // contains never revert + bool contains = contains@withrevert(key); + assert !lastReverted; + + // tryGet never reverts (key) + tryGet_contains@withrevert(key); + assert !lastReverted; + + // tryGet never reverts (value) + tryGet_value@withrevert(key); + assert !lastReverted; + + // get reverts iff the key is not in the map + get@withrevert(key); + assert !lastReverted <=> contains; +} + +rule liveness_2(uint256 index) { + requireInvariant consistencyIndex(index); + + // length never revert + uint256 length = length@withrevert(); + assert !lastReverted; + + // key_at reverts iff the index is out of bound + key_at@withrevert(index); + assert !lastReverted <=> index < length; + + // value_at reverts iff the index is out of bound + value_at@withrevert(index); + assert !lastReverted <=> index < length; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: get and tryGet return the expected values. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule getAndTryGet(bytes32 key) { + requireInvariant noValueIfNotContained(key); + + bool contained = contains(key); + bool tryContained = tryGet_contains(key); + bytes32 tryValue = tryGet_value(key); + bytes32 value = get@withrevert(key); // revert is not contained + + assert contained == tryContained; + assert contained => tryValue == value; + assert !contained => tryValue == 0; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: set key-value in EnumerableMap │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule set(bytes32 key, bytes32 value, bytes32 otherKey) { + require sanity(); + + uint256 lengthBefore = length(); + bool containsBefore = contains(key); + bool containsOtherBefore = contains(otherKey); + bytes32 otherValueBefore = tryGet_value(otherKey); + + bool added = set@withrevert(key, value); + bool success = !lastReverted; + + assert success && contains(key) && get(key) == value, + "liveness & immediate effect"; + + assert added <=> !containsBefore, + "return value: added iff not contained"; + + assert length() == lengthBefore + to_mathint(added ? 1 : 0), + "effect: length increases iff added"; + + assert added => (key_at(lengthBefore) == key && value_at(lengthBefore) == value), + "effect: add at the end"; + + assert containsOtherBefore != contains(otherKey) => (added && key == otherKey), + "side effect: other keys are not affected"; + + assert otherValueBefore != tryGet_value(otherKey) => key == otherKey, + "side effect: values attached to other keys are not affected"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: remove key from EnumerableMap │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule remove(bytes32 key, bytes32 otherKey) { + requireInvariant consistencyKey(key); + requireInvariant consistencyKey(otherKey); + + uint256 lengthBefore = length(); + bool containsBefore = contains(key); + bool containsOtherBefore = contains(otherKey); + bytes32 otherValueBefore = tryGet_value(otherKey); + + bool removed = remove@withrevert(key); + bool success = !lastReverted; + + assert success && !contains(key), + "liveness & immediate effect"; + + assert removed <=> containsBefore, + "return value: removed iff contained"; + + assert length() == lengthBefore - to_mathint(removed ? 1 : 0), + "effect: length decreases iff removed"; + + assert containsOtherBefore != contains(otherKey) => (removed && key == otherKey), + "side effect: other keys are not affected"; + + assert otherValueBefore != tryGet_value(otherKey) => key == otherKey, + "side effect: values attached to other keys are not affected"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: when adding a new key, the other keys remain in set, at the same index. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule setEnumerability(bytes32 key, bytes32 value, uint256 index) { + require sanity(); + + bytes32 atKeyBefore = key_at(index); + bytes32 atValueBefore = value_at(index); + + set(key, value); + + bytes32 atKeyAfter = key_at@withrevert(index); + assert !lastReverted; + + bytes32 atValueAfter = value_at@withrevert(index); + assert !lastReverted; + + assert atKeyAfter == atKeyBefore; + assert atValueAfter != atValueBefore => ( + key == atKeyBefore && + value == atValueAfter + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: when removing a existing key, the other keys remain in set, at the same index (except for the last one). │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule removeEnumerability(bytes32 key, uint256 index) { + uint256 last = length() - 1; + + requireInvariant consistencyKey(key); + requireInvariant consistencyIndex(index); + requireInvariant consistencyIndex(last); + + bytes32 atKeyBefore = key_at(index); + bytes32 atValueBefore = value_at(index); + bytes32 lastKeyBefore = key_at(last); + bytes32 lastValueBefore = value_at(last); + + remove(key); + + // can't read last value & keys (length decreased) + bytes32 atKeyAfter = key_at@withrevert(index); + assert lastReverted <=> index == last; + + bytes32 atValueAfter = value_at@withrevert(index); + assert lastReverted <=> index == last; + + // One value that is allowed to change is if previous value was removed, + // in that case the last value before took its place. + assert ( + index != last && + atKeyBefore != atKeyAfter + ) => ( + atKeyBefore == key && + atKeyAfter == lastKeyBefore + ); + + assert ( + index != last && + atValueBefore != atValueAfter + ) => ( + atValueAfter == lastValueBefore + ); +} diff --git a/certora/specs/EnumerableSet.spec b/certora/specs/EnumerableSet.spec new file mode 100644 index 000000000..c94ba2437 --- /dev/null +++ b/certora/specs/EnumerableSet.spec @@ -0,0 +1,247 @@ +import "helpers.spec" + +methods { + // library + add(bytes32) returns (bool) envfree + remove(bytes32) returns (bool) envfree + contains(bytes32) returns (bool) envfree + length() returns (uint256) envfree + at_(uint256) returns (bytes32) envfree + + // FV + _indexOf(bytes32) returns (uint256) envfree +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Helpers │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +function sanity() returns bool { + return length() < max_uint256; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: All indexed keys are contained │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant indexedContained(uint256 index) + index < length() => contains(at_(index)) + { + preserved { + requireInvariant consistencyIndex(index); + requireInvariant consistencyIndex(to_uint256(length() - 1)); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: A value can only be stored at a single location │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant atUniqueness(uint256 index1, uint256 index2) + index1 == index2 <=> at_(index1) == at_(index2) + { + preserved remove(bytes32 key) { + requireInvariant atUniqueness(index1, to_uint256(length() - 1)); + requireInvariant atUniqueness(index2, to_uint256(length() - 1)); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: index <> key relationship is consistent │ +│ │ +│ Note that the two consistencyXxx invariants, put together, prove that at_ and _indexOf are inverse of one another. │ +│ This proves that we have a bijection between indices (the enumerability part) and keys (the entries that are added │ +│ and removed from the EnumerableSet). │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant consistencyIndex(uint256 index) + index < length() => _indexOf(at_(index)) == index + 1 + { + preserved remove(bytes32 key) { + requireInvariant consistencyIndex(to_uint256(length() - 1)); + } + } + +invariant consistencyKey(bytes32 key) + contains(key) => ( + _indexOf(key) > 0 && + _indexOf(key) <= length() && + at_(to_uint256(_indexOf(key) - 1)) == key + ) + { + preserved remove(bytes32 otherKey) { + requireInvariant consistencyKey(otherKey); + requireInvariant atUniqueness( + to_uint256(_indexOf(key) - 1), + to_uint256(_indexOf(otherKey) - 1) + ); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: state only changes by adding or removing elements │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule stateChange(env e, bytes32 key) { + require sanity(); + requireInvariant consistencyKey(key); + + uint256 lengthBefore = length(); + bool containsBefore = contains(key); + + method f; + calldataarg args; + f(e, args); + + uint256 lengthAfter = length(); + bool containsAfter = contains(key); + + assert lengthBefore != lengthAfter => ( + (f.selector == add(bytes32).selector && lengthAfter == lengthBefore + 1) || + (f.selector == remove(bytes32).selector && lengthAfter == lengthBefore - 1) + ); + + assert containsBefore != containsAfter => ( + (f.selector == add(bytes32).selector && containsAfter) || + (f.selector == remove(bytes32).selector && containsBefore) + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: check liveness of view functions. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule liveness_1(bytes32 key) { + requireInvariant consistencyKey(key); + + // contains never revert + contains@withrevert(key); + assert !lastReverted; +} + +rule liveness_2(uint256 index) { + requireInvariant consistencyIndex(index); + + // length never revert + uint256 length = length@withrevert(); + assert !lastReverted; + + // at reverts iff the index is out of bound + at_@withrevert(index); + assert !lastReverted <=> index < length; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: add key to EnumerableSet if not already contained │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule add(bytes32 key, bytes32 otherKey) { + require sanity(); + + uint256 lengthBefore = length(); + bool containsBefore = contains(key); + bool containsOtherBefore = contains(otherKey); + + bool added = add@withrevert(key); + bool success = !lastReverted; + + assert success && contains(key), + "liveness & immediate effect"; + + assert added <=> !containsBefore, + "return value: added iff not contained"; + + assert length() == lengthBefore + to_mathint(added ? 1 : 0), + "effect: length increases iff added"; + + assert added => at_(lengthBefore) == key, + "effect: add at the end"; + + assert containsOtherBefore != contains(otherKey) => (added && key == otherKey), + "side effect: other keys are not affected"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: remove key from EnumerableSet if already contained │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule remove(bytes32 key, bytes32 otherKey) { + requireInvariant consistencyKey(key); + requireInvariant consistencyKey(otherKey); + + uint256 lengthBefore = length(); + bool containsBefore = contains(key); + bool containsOtherBefore = contains(otherKey); + + bool removed = remove@withrevert(key); + bool success = !lastReverted; + + assert success && !contains(key), + "liveness & immediate effect"; + + assert removed <=> containsBefore, + "return value: removed iff contained"; + + assert length() == lengthBefore - to_mathint(removed ? 1 : 0), + "effect: length decreases iff removed"; + + assert containsOtherBefore != contains(otherKey) => (removed && key == otherKey), + "side effect: other keys are not affected"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: when adding a new key, the other keys remain in set, at the same index. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule addEnumerability(bytes32 key, uint256 index) { + require sanity(); + + bytes32 atBefore = at_(index); + add(key); + bytes32 atAfter = at_@withrevert(index); + bool atAfterSuccess = !lastReverted; + + assert atAfterSuccess; + assert atBefore == atAfter; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: when removing a existing key, the other keys remain in set, at the same index (except for the last one). │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule removeEnumerability(bytes32 key, uint256 index) { + uint256 last = length() - 1; + + requireInvariant consistencyKey(key); + requireInvariant consistencyIndex(index); + requireInvariant consistencyIndex(last); + + bytes32 atBefore = at_(index); + bytes32 lastBefore = at_(last); + + remove(key); + + // can't read last value (length decreased) + bytes32 atAfter = at_@withrevert(index); + assert lastReverted <=> index == last; + + // One value that is allowed to change is if previous value was removed, + // in that case the last value before took its place. + assert ( + index != last && + atBefore != atAfter + ) => ( + atBefore == key && + atAfter == lastBefore + ); +} From 538655c3c06b615143141552b2d6d6f8515e4499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 3 May 2023 09:35:48 +0200 Subject: [PATCH 112/197] Add reentrancy test cases for ERC4626 (#4197) Co-authored-by: Hadrien Croubois Co-authored-by: Francisco Giordano --- contracts/mocks/ERC20Reentrant.sol | 43 +++++ test/token/ERC20/extensions/ERC4626.test.js | 175 ++++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 contracts/mocks/ERC20Reentrant.sol diff --git a/contracts/mocks/ERC20Reentrant.sol b/contracts/mocks/ERC20Reentrant.sol new file mode 100644 index 000000000..c0184b77b --- /dev/null +++ b/contracts/mocks/ERC20Reentrant.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../token/ERC20/ERC20.sol"; +import "../token/ERC20/extensions/ERC4626.sol"; + +contract ERC20Reentrant is ERC20("TEST", "TST") { + enum Type { + No, + Before, + After + } + + Type private _reenterType; + address private _reenterTarget; + bytes private _reenterData; + + function scheduleReenter(Type when, address target, bytes calldata data) external { + _reenterType = when; + _reenterTarget = target; + _reenterData = data; + } + + function functionCall(address target, bytes memory data) public returns (bytes memory) { + return Address.functionCall(target, data); + } + + function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { + if (_reenterType == Type.Before) { + _reenterType = Type.No; + functionCall(_reenterTarget, _reenterData); + } + super._beforeTokenTransfer(from, to, amount); + } + + function _afterTokenTransfer(address from, address to, uint256 amount) internal override { + super._afterTokenTransfer(from, to, amount); + if (_reenterType == Type.After) { + _reenterType = Type.No; + functionCall(_reenterTarget, _reenterData); + } + } +} diff --git a/test/token/ERC20/extensions/ERC4626.test.js b/test/token/ERC20/extensions/ERC4626.test.js index ee0998717..55b3e5d20 100644 --- a/test/token/ERC20/extensions/ERC4626.test.js +++ b/test/token/ERC20/extensions/ERC4626.test.js @@ -1,11 +1,14 @@ const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { Enum } = require('../../../helpers/enums'); + const ERC20Decimals = artifacts.require('$ERC20DecimalsMock'); const ERC4626 = artifacts.require('$ERC4626'); const ERC4626OffsetMock = artifacts.require('$ERC4626OffsetMock'); const ERC4626FeesMock = artifacts.require('$ERC4626FeesMock'); const ERC20ExcessDecimalsMock = artifacts.require('ERC20ExcessDecimalsMock'); +const ERC20Reentrant = artifacts.require('$ERC20Reentrant'); contract('ERC4626', function (accounts) { const [holder, recipient, spender, other, user1, user2] = accounts; @@ -44,6 +47,178 @@ contract('ERC4626', function (accounts) { } }); + describe('reentrancy', async function () { + const reenterType = Enum('No', 'Before', 'After'); + + const amount = web3.utils.toBN(1000000000000000000); + const reenterAmount = web3.utils.toBN(1000000000); + let token; + let vault; + + beforeEach(async function () { + token = await ERC20Reentrant.new(); + // Use offset 1 so the rate is not 1:1 and we can't possibly confuse assets and shares + vault = await ERC4626OffsetMock.new('', '', token.address, 1); + // Funds and approval for tests + await token.$_mint(holder, amount); + await token.$_mint(other, amount); + await token.$_approve(holder, vault.address, constants.MAX_UINT256); + await token.$_approve(other, vault.address, constants.MAX_UINT256); + await token.$_approve(token.address, vault.address, constants.MAX_UINT256); + }); + + // During a `_deposit`, the vault does `transferFrom(depositor, vault, assets)` -> `_mint(receiver, shares)` + // such that a reentrancy BEFORE the transfer guarantees the price is kept the same. + // If the order of transfer -> mint is changed to mint -> transfer, the reentrancy could be triggered on an + // intermediate state in which the ratio of assets/shares has been decreased (more shares than assets). + it('correct share price is observed during reentrancy before deposit', async function () { + // mint token for deposit + await token.$_mint(token.address, reenterAmount); + + // Schedules a reentrancy from the token contract + await token.scheduleReenter( + reenterType.Before, + vault.address, + vault.contract.methods.deposit(reenterAmount, holder).encodeABI(), + ); + + // Initial share price + const sharesForDeposit = await vault.previewDeposit(amount, { from: holder }); + const sharesForReenter = await vault.previewDeposit(reenterAmount, { from: holder }); + + // Do deposit normally, triggering the _beforeTokenTransfer hook + const receipt = await vault.deposit(amount, holder, { from: holder }); + + // Main deposit event + await expectEvent(receipt, 'Deposit', { + sender: holder, + owner: holder, + assets: amount, + shares: sharesForDeposit, + }); + // Reentrant deposit event → uses the same price + await expectEvent(receipt, 'Deposit', { + sender: token.address, + owner: holder, + assets: reenterAmount, + shares: sharesForReenter, + }); + + // Assert prices is kept + const sharesAfter = await vault.previewDeposit(amount, { from: holder }); + expect(sharesForDeposit).to.be.bignumber.eq(sharesAfter); + }); + + // During a `_withdraw`, the vault does `_burn(owner, shares)` -> `transfer(receiver, assets)` + // such that a reentrancy AFTER the transfer guarantees the price is kept the same. + // If the order of burn -> transfer is changed to transfer -> burn, the reentrancy could be triggered on an + // intermediate state in which the ratio of shares/assets has been decreased (more assets than shares). + it('correct share price is observed during reentrancy after withdraw', async function () { + // Deposit into the vault: holder gets `amount` share, token.address gets `reenterAmount` shares + await vault.deposit(amount, holder, { from: holder }); + await vault.deposit(reenterAmount, token.address, { from: other }); + + // Schedules a reentrancy from the token contract + await token.scheduleReenter( + reenterType.After, + vault.address, + vault.contract.methods.withdraw(reenterAmount, holder, token.address).encodeABI(), + ); + + // Initial share price + const sharesForWithdraw = await vault.previewWithdraw(amount, { from: holder }); + const sharesForReenter = await vault.previewWithdraw(reenterAmount, { from: holder }); + + // Do withdraw normally, triggering the _afterTokenTransfer hook + const receipt = await vault.withdraw(amount, holder, holder, { from: holder }); + + // Main withdraw event + await expectEvent(receipt, 'Withdraw', { + sender: holder, + receiver: holder, + owner: holder, + assets: amount, + shares: sharesForWithdraw, + }); + // Reentrant withdraw event → uses the same price + await expectEvent(receipt, 'Withdraw', { + sender: token.address, + receiver: holder, + owner: token.address, + assets: reenterAmount, + shares: sharesForReenter, + }); + + // Assert price is kept + const sharesAfter = await vault.previewWithdraw(amount, { from: holder }); + expect(sharesForWithdraw).to.be.bignumber.eq(sharesAfter); + }); + + // Donate newly minted tokens to the vault during the reentracy causes the share price to increase. + // Still, the deposit that trigger the reentracy is not affected and get the previewed price. + // Further deposits will get a different price (getting fewer shares for the same amount of assets) + it('share price change during reentracy does not affect deposit', async function () { + // Schedules a reentrancy from the token contract that mess up the share price + await token.scheduleReenter( + reenterType.Before, + token.address, + token.contract.methods.$_mint(vault.address, reenterAmount).encodeABI(), + ); + + // Price before + const sharesBefore = await vault.previewDeposit(amount); + + // Deposit, triggering the _beforeTokenTransfer hook + const receipt = await vault.deposit(amount, holder, { from: holder }); + + // Price is as previewed + await expectEvent(receipt, 'Deposit', { + sender: holder, + owner: holder, + assets: amount, + shares: sharesBefore, + }); + + // Price was modified during reentrancy + const sharesAfter = await vault.previewDeposit(amount); + expect(sharesAfter).to.be.bignumber.lt(sharesBefore); + }); + + // Burn some tokens from the vault during the reentracy causes the share price to drop. + // Still, the withdraw that trigger the reentracy is not affected and get the previewed price. + // Further withdraw will get a different price (needing more shares for the same amount of assets) + it('share price change during reentracy does not affect withdraw', async function () { + await vault.deposit(amount, other, { from: other }); + await vault.deposit(amount, holder, { from: holder }); + + // Schedules a reentrancy from the token contract that mess up the share price + await token.scheduleReenter( + reenterType.After, + token.address, + token.contract.methods.$_burn(vault.address, reenterAmount).encodeABI(), + ); + + // Price before + const sharesBefore = await vault.previewWithdraw(amount); + + // Withdraw, triggering the _afterTokenTransfer hook + const receipt = await vault.withdraw(amount, holder, holder, { from: holder }); + + // Price is as previewed + await expectEvent(receipt, 'Withdraw', { + sender: holder, + receiver: holder, + owner: holder, + assets: amount, + shares: sharesBefore, + }); + + // Price was modified during reentrancy + const sharesAfter = await vault.previewWithdraw(amount); + expect(sharesAfter).to.be.bignumber.gt(sharesBefore); + }); + }); + for (const offset of [0, 6, 18].map(web3.utils.toBN)) { const parseToken = token => web3.utils.toBN(10).pow(decimals).muln(token); const parseShare = share => web3.utils.toBN(10).pow(decimals.add(offset)).muln(share); From 10022da83d75a5ae0cae5222dced463a6678cf61 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 3 May 2023 16:13:42 +0200 Subject: [PATCH 113/197] Disable automatic formal verification workflow on push (#4208) --- .github/workflows/formal-verification.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/formal-verification.yml b/.github/workflows/formal-verification.yml index f94152a52..ae5eba006 100644 --- a/.github/workflows/formal-verification.yml +++ b/.github/workflows/formal-verification.yml @@ -1,10 +1,6 @@ name: formal verification on: - push: - branches: - - master - - release-v* pull_request: types: - opened From a7ee03565b4ee14265f4406f9e38a04e0143656f Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 3 May 2023 16:34:14 +0200 Subject: [PATCH 114/197] Move certora helpers to a dedicated folder (#4211) --- certora/specs/AccessControl.spec | 2 +- certora/specs/DoubleEndedQueue.spec | 36 ++++++++++++------------ certora/specs/ERC20.spec | 2 +- certora/specs/ERC20FlashMint.spec | 2 +- certora/specs/ERC20Wrapper.spec | 2 +- certora/specs/ERC721.spec | 2 +- certora/specs/EnumerableMap.spec | 2 +- certora/specs/EnumerableSet.spec | 2 +- certora/specs/Initializable.spec | 2 +- certora/specs/Ownable.spec | 2 +- certora/specs/Ownable2Step.spec | 2 +- certora/specs/Pausable.spec | 8 +++--- certora/specs/TimelockController.spec | 2 +- certora/specs/{ => helpers}/helpers.spec | 0 14 files changed, 33 insertions(+), 33 deletions(-) rename certora/specs/{ => helpers}/helpers.spec (100%) diff --git a/certora/specs/AccessControl.spec b/certora/specs/AccessControl.spec index 35927c5d3..81b6d3604 100644 --- a/certora/specs/AccessControl.spec +++ b/certora/specs/AccessControl.spec @@ -1,4 +1,4 @@ -import "helpers.spec" +import "helpers/helpers.spec" import "methods/IAccessControl.spec" /* diff --git a/certora/specs/DoubleEndedQueue.spec b/certora/specs/DoubleEndedQueue.spec index 2412b4cc8..2a196772d 100644 --- a/certora/specs/DoubleEndedQueue.spec +++ b/certora/specs/DoubleEndedQueue.spec @@ -1,4 +1,4 @@ -import "helpers.spec" +import "helpers/helpers.spec" methods { pushFront(bytes32) envfree @@ -10,7 +10,7 @@ methods { // exposed for FV begin() returns (int128) envfree end() returns (int128) envfree - + // view length() returns (uint256) envfree empty() returns (bool) envfree @@ -35,7 +35,7 @@ function max_int128() returns mathint { // Could be broken in theory, but not in practice function boundedQueue() returns bool { - return + return max_int128() > to_mathint(end()) && min_int128() < to_mathint(begin()); } @@ -78,11 +78,11 @@ invariant emptiness() invariant queueEndings() at_(length() - 1) == back() && at_(0) == front() filtered { f -> !f.isView } - { - preserved { - requireInvariant boundariesConsistency(); - require boundedQueue(); - } + { + preserved { + requireInvariant boundariesConsistency(); + require boundedQueue(); + } } /* @@ -94,12 +94,12 @@ rule pushFront(bytes32 value) { require boundedQueue(); uint256 lengthBefore = length(); - + pushFront@withrevert(value); - + // liveness assert !lastReverted, "never reverts"; - + // effect assert front() == value, "front set to value"; assert length() == lengthBefore + 1, "queue extended"; @@ -134,12 +134,12 @@ rule pushBack(bytes32 value) { require boundedQueue(); uint256 lengthBefore = length(); - + pushBack@withrevert(value); - + // liveness assert !lastReverted, "never reverts"; - + // effect assert back() == value, "back set to value"; assert length() == lengthBefore + 1, "queue increased"; @@ -205,7 +205,7 @@ rule popFrontConsistency(uint256 index) { // try to read value bytes32 after = at_@withrevert(index - 1); - + assert !lastReverted, "value still exists in the queue"; assert before == after, "values are offset and not modified"; } @@ -250,7 +250,7 @@ rule popBackConsistency(uint256 index) { // try to read value bytes32 after = at_@withrevert(index); - + assert !lastReverted, "value still exists in the queue"; assert before == after, "values are offset and not modified"; } @@ -262,10 +262,10 @@ rule popBackConsistency(uint256 index) { */ rule clear { clear@withrevert(); - + // liveness assert !lastReverted, "never reverts"; - + // effect assert length() == 0, "sets length to 0"; } diff --git a/certora/specs/ERC20.spec b/certora/specs/ERC20.spec index 85f95e706..3bd2b38ba 100644 --- a/certora/specs/ERC20.spec +++ b/certora/specs/ERC20.spec @@ -1,4 +1,4 @@ -import "helpers.spec" +import "helpers/helpers.spec" import "methods/IERC20.spec" import "methods/IERC2612.spec" diff --git a/certora/specs/ERC20FlashMint.spec b/certora/specs/ERC20FlashMint.spec index 64d97342a..70a7c0795 100644 --- a/certora/specs/ERC20FlashMint.spec +++ b/certora/specs/ERC20FlashMint.spec @@ -1,4 +1,4 @@ -import "helpers.spec" +import "helpers/helpers.spec" import "methods/IERC20.spec" import "methods/IERC3156.spec" diff --git a/certora/specs/ERC20Wrapper.spec b/certora/specs/ERC20Wrapper.spec index c10173766..badfa7a28 100644 --- a/certora/specs/ERC20Wrapper.spec +++ b/certora/specs/ERC20Wrapper.spec @@ -1,4 +1,4 @@ -import "helpers.spec" +import "helpers/helpers.spec" import "ERC20.spec" methods { diff --git a/certora/specs/ERC721.spec b/certora/specs/ERC721.spec index 48503469b..9db13f45c 100644 --- a/certora/specs/ERC721.spec +++ b/certora/specs/ERC721.spec @@ -1,4 +1,4 @@ -import "helpers.spec" +import "helpers/helpers.spec" import "methods/IERC721.spec" methods { diff --git a/certora/specs/EnumerableMap.spec b/certora/specs/EnumerableMap.spec index 56ef854c6..dea5d85ec 100644 --- a/certora/specs/EnumerableMap.spec +++ b/certora/specs/EnumerableMap.spec @@ -1,4 +1,4 @@ -import "helpers.spec" +import "helpers/helpers.spec" methods { // library diff --git a/certora/specs/EnumerableSet.spec b/certora/specs/EnumerableSet.spec index c94ba2437..d63c556aa 100644 --- a/certora/specs/EnumerableSet.spec +++ b/certora/specs/EnumerableSet.spec @@ -1,4 +1,4 @@ -import "helpers.spec" +import "helpers/helpers.spec" methods { // library diff --git a/certora/specs/Initializable.spec b/certora/specs/Initializable.spec index 1ba8d54e8..0e0b1b714 100644 --- a/certora/specs/Initializable.spec +++ b/certora/specs/Initializable.spec @@ -1,4 +1,4 @@ -import "helpers.spec" +import "helpers/helpers.spec" methods { // initialize, reinitialize, disable diff --git a/certora/specs/Ownable.spec b/certora/specs/Ownable.spec index 4fdfeb09c..4bf9e3005 100644 --- a/certora/specs/Ownable.spec +++ b/certora/specs/Ownable.spec @@ -1,4 +1,4 @@ -import "helpers.spec" +import "helpers/helpers.spec" import "methods/IOwnable.spec" methods { diff --git a/certora/specs/Ownable2Step.spec b/certora/specs/Ownable2Step.spec index 70c520a03..47b1b8d75 100644 --- a/certora/specs/Ownable2Step.spec +++ b/certora/specs/Ownable2Step.spec @@ -1,4 +1,4 @@ -import "helpers.spec" +import "helpers/helpers.spec" import "methods/IOwnable2Step.spec" methods { diff --git a/certora/specs/Pausable.spec b/certora/specs/Pausable.spec index e49293ffc..aea38003f 100644 --- a/certora/specs/Pausable.spec +++ b/certora/specs/Pausable.spec @@ -1,4 +1,4 @@ -import "helpers.spec" +import "helpers/helpers.spec" methods { paused() returns (bool) envfree @@ -22,7 +22,7 @@ rule pause(env e) { bool success = !lastReverted; bool pausedAfter = paused(); - + // liveness assert success <=> !pausedBefore, "works if and only if the contract was not paused before"; @@ -44,7 +44,7 @@ rule unpause(env e) { bool success = !lastReverted; bool pausedAfter = paused(); - + // liveness assert success <=> pausedBefore, "works if and only if the contract was paused before"; @@ -71,7 +71,7 @@ rule whenPaused(env e) { */ rule whenNotPaused(env e) { require nonpayable(e); - + onlyWhenNotPaused@withrevert(e); assert !lastReverted <=> !paused(), "works if and only if the contract is not paused"; } diff --git a/certora/specs/TimelockController.spec b/certora/specs/TimelockController.spec index e140c11de..05ecb1340 100644 --- a/certora/specs/TimelockController.spec +++ b/certora/specs/TimelockController.spec @@ -1,4 +1,4 @@ -import "helpers.spec" +import "helpers/helpers.spec" import "methods/IAccessControl.spec" methods { diff --git a/certora/specs/helpers.spec b/certora/specs/helpers/helpers.spec similarity index 100% rename from certora/specs/helpers.spec rename to certora/specs/helpers/helpers.spec From 9e8b74a0e2c8a6e87f7fa88318f15017d4b14524 Mon Sep 17 00:00:00 2001 From: Francisco Date: Thu, 4 May 2023 14:33:57 +0100 Subject: [PATCH 115/197] Add more test cases for EIP712 (#4212) --- contracts/utils/cryptography/EIP712.sol | 2 + test/utils/cryptography/EIP712.test.js | 137 +++++++++++++++--------- 2 files changed, 87 insertions(+), 52 deletions(-) diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol index e06d0066b..56dfceaf4 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -111,6 +111,8 @@ abstract contract EIP712 is IERC5267 { /** * @dev See {EIP-5267}. + * + * _Available since v4.9._ */ function eip712Domain() public diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js index f12f22673..52e322d3d 100644 --- a/test/utils/cryptography/EIP712.test.js +++ b/test/utils/cryptography/EIP712.test.js @@ -6,65 +6,98 @@ const { getChainId } = require('../../helpers/chainid'); const { mapValues } = require('../../helpers/map-values'); const EIP712Verifier = artifacts.require('$EIP712Verifier'); +const Clones = artifacts.require('$Clones'); contract('EIP712', function (accounts) { const [mailTo] = accounts; - const name = 'A Name'; - const version = '1'; + const shortName = 'A Name'; + const shortVersion = '1'; - beforeEach('deploying', async function () { - this.eip712 = await EIP712Verifier.new(name, version); + const longName = 'A'.repeat(40); + const longVersion = 'B'.repeat(40); - this.domain = { - name, - version, - chainId: await getChainId(), - verifyingContract: this.eip712.address, - }; - this.domainType = domainType(this.domain); - }); + const cases = [ + ['short', shortName, shortVersion], + ['long', longName, longVersion], + ]; - describe('domain separator', function () { - it('is internally available', async function () { - const expected = await domainSeparator(this.domain); + for (const [shortOrLong, name, version] of cases) { + describe(`with ${shortOrLong} name and version`, function () { + beforeEach('deploying', async function () { + this.eip712 = await EIP712Verifier.new(name, version); - expect(await this.eip712.$_domainSeparatorV4()).to.equal(expected); + this.domain = { + name, + version, + chainId: await getChainId(), + verifyingContract: this.eip712.address, + }; + this.domainType = domainType(this.domain); + }); + + describe('domain separator', function () { + it('is internally available', async function () { + const expected = await domainSeparator(this.domain); + + expect(await this.eip712.$_domainSeparatorV4()).to.equal(expected); + }); + + it("can be rebuilt using EIP-5267's eip712Domain", async function () { + const rebuildDomain = await getDomain(this.eip712); + expect(mapValues(rebuildDomain, String)).to.be.deep.equal(mapValues(this.domain, String)); + }); + + if (shortOrLong === 'short') { + // Long strings are in storage, and the proxy will not be properly initialized unless + // the upgradeable contract variant is used and the initializer is invoked. + + it('adjusts when behind proxy', async function () { + const factory = await Clones.new(); + const cloneReceipt = await factory.$clone(this.eip712.address); + const cloneAddress = cloneReceipt.logs.find(({ event }) => event === 'return$clone').args.instance; + const clone = new EIP712Verifier(cloneAddress); + + const cloneDomain = { ...this.domain, verifyingContract: clone.address }; + + const expectedSeparator = await domainSeparator(cloneDomain); + expect(await clone.$_domainSeparatorV4()).to.equal(expectedSeparator); + + const reportedDomain = await getDomain(clone); + expect(mapValues(reportedDomain, String)).to.be.deep.equal(mapValues(cloneDomain, String)); + }); + } + }); + + it('hash digest', async function () { + const structhash = web3.utils.randomHex(32); + expect(await this.eip712.$_hashTypedDataV4(structhash)).to.be.equal(hashTypedData(this.domain, structhash)); + }); + + it('digest', async function () { + const message = { + to: mailTo, + contents: 'very interesting', + }; + + const data = { + types: { + EIP712Domain: this.domainType, + Mail: [ + { name: 'to', type: 'address' }, + { name: 'contents', type: 'string' }, + ], + }, + domain: this.domain, + primaryType: 'Mail', + message, + }; + + const wallet = Wallet.generate(); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + + await this.eip712.verify(signature, wallet.getAddressString(), message.to, message.contents); + }); }); - - it("can be rebuilt using EIP-5267's eip712Domain", async function () { - const rebuildDomain = await getDomain(this.eip712); - expect(mapValues(rebuildDomain, String)).to.be.deep.equal(mapValues(this.domain, String)); - }); - }); - - it('hash digest', async function () { - const structhash = web3.utils.randomHex(32); - expect(await this.eip712.$_hashTypedDataV4(structhash)).to.be.equal(hashTypedData(this.domain, structhash)); - }); - - it('digest', async function () { - const message = { - to: mailTo, - contents: 'very interesting', - }; - - const data = { - types: { - EIP712Domain: this.domainType, - Mail: [ - { name: 'to', type: 'address' }, - { name: 'contents', type: 'string' }, - ], - }, - domain: this.domain, - primaryType: 'Mail', - message, - }; - - const wallet = Wallet.generate(); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - - await this.eip712.verify(signature, wallet.getAddressString(), message.to, message.contents); - }); + } }); From 8b2ed0f5706fe1a7ab6ade84b27bf875dc611fda Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 4 May 2023 18:54:22 +0200 Subject: [PATCH 116/197] Fix early reporting of FV prover's output (#4213) Co-authored-by: Francisco --- certora/run.js | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/certora/run.js b/certora/run.js index 2dc7d0181..fdee42d2e 100755 --- a/certora/run.js +++ b/certora/run.js @@ -73,15 +73,18 @@ async function runCertora(spec, contract, files, options = []) { child.stdout.pipe(stream, { end: false }); child.stderr.pipe(stream, { end: false }); - // as soon as we have a jobStatus link, print it + // as soon as we have a job id, print the output link stream.on('data', function logStatusUrl(data) { - const urls = data.toString('utf8').match(/https?:\S*/g); - for (const url of urls ?? []) { - if (url.includes('/jobStatus/')) { - console.error(`[${spec}] ${url.replace('/jobStatus/', '/output/')}`); - stream.off('data', logStatusUrl); - break; - } + const { '-DjobId': jobId, '-DuserId': userId } = Object.fromEntries( + data + .toString('utf8') + .match(/-D\S+=\S+/g) + ?.map(s => s.split('=')) || [], + ); + + if (jobId && userId) { + console.error(`[${spec}] https://prover.certora.com/output/${userId}/${jobId}/`); + stream.off('data', logStatusUrl); } }); @@ -98,7 +101,7 @@ async function runCertora(spec, contract, files, options = []) { stream.end(); // write results in markdown format - writeEntry(spec, contract, code || signal, (await output).match(/https:\S*/)?.[0]); + writeEntry(spec, contract, code || signal, (await output).match(/https:\/\/prover.certora.com\/output\/\S*/)?.[0]); // write all details console.error(`+ certoraRun ${args.join(' ')}\n` + (await output)); @@ -136,8 +139,8 @@ function writeEntry(spec, contract, success, url) { spec, contract, success ? ':x:' : ':heavy_check_mark:', + url ? `[link](${url?.replace('/output/', '/jobStatus/')})` : 'error', url ? `[link](${url})` : 'error', - url ? `[link](${url?.replace('/jobStatus/', '/output/')})` : 'error', ), ); } From dcba9f995ffa83433cc523a8e3d1fcfeac426377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 5 May 2023 21:27:43 +0200 Subject: [PATCH 117/197] Add AccessControlDefaultAdminRules FV (#4180) Co-authored-by: Hadrien Croubois Co-authored-by: Francisco --- .../AccessControlDefaultAdminRulesHarness.sol | 47 ++ certora/specs.json | 5 + certora/specs/AccessControl.spec | 20 +- .../specs/AccessControlDefaultAdminRules.spec | 500 ++++++++++++++++++ .../IAccessControlDefaultAdminRules.spec | 36 ++ certora/specs/methods/IERC5313.spec | 3 + 6 files changed, 603 insertions(+), 8 deletions(-) create mode 100644 certora/harnesses/AccessControlDefaultAdminRulesHarness.sol create mode 100644 certora/specs/AccessControlDefaultAdminRules.spec create mode 100644 certora/specs/methods/IAccessControlDefaultAdminRules.spec create mode 100644 certora/specs/methods/IERC5313.spec diff --git a/certora/harnesses/AccessControlDefaultAdminRulesHarness.sol b/certora/harnesses/AccessControlDefaultAdminRulesHarness.sol new file mode 100644 index 000000000..29fd3a709 --- /dev/null +++ b/certora/harnesses/AccessControlDefaultAdminRulesHarness.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../patched/access/AccessControlDefaultAdminRules.sol"; + +contract AccessControlDefaultAdminRulesHarness is AccessControlDefaultAdminRules { + uint48 private _delayIncreaseWait; + + constructor( + uint48 initialDelay, + address initialDefaultAdmin, + uint48 delayIncreaseWait + ) AccessControlDefaultAdminRules(initialDelay, initialDefaultAdmin) { + _delayIncreaseWait = delayIncreaseWait; + } + + // FV + function pendingDefaultAdmin_() external view returns (address) { + (address newAdmin, ) = pendingDefaultAdmin(); + return newAdmin; + } + + function pendingDefaultAdminSchedule_() external view returns (uint48) { + (, uint48 schedule) = pendingDefaultAdmin(); + return schedule; + } + + function pendingDelay_() external view returns (uint48) { + (uint48 newDelay, ) = pendingDefaultAdminDelay(); + return newDelay; + } + + function pendingDelaySchedule_() external view returns (uint48) { + (, uint48 schedule) = pendingDefaultAdminDelay(); + return schedule; + } + + function delayChangeWait_(uint48 newDelay) external view returns (uint48) { + return _delayChangeWait(newDelay); + } + + // Overrides + function defaultAdminDelayIncreaseWait() public view override returns (uint48) { + return _delayIncreaseWait; + } +} diff --git a/certora/specs.json b/certora/specs.json index 6f8f57bdf..3e5acb568 100644 --- a/certora/specs.json +++ b/certora/specs.json @@ -9,6 +9,11 @@ "contract": "AccessControlHarness", "files": ["certora/harnesses/AccessControlHarness.sol"] }, + { + "spec": "AccessControlDefaultAdminRules", + "contract": "AccessControlDefaultAdminRulesHarness", + "files": ["certora/harnesses/AccessControlDefaultAdminRulesHarness.sol"] + }, { "spec": "DoubleEndedQueue", "contract": "DoubleEndedQueueHarness", diff --git a/certora/specs/AccessControl.spec b/certora/specs/AccessControl.spec index 81b6d3604..cd5af2a99 100644 --- a/certora/specs/AccessControl.spec +++ b/certora/specs/AccessControl.spec @@ -1,13 +1,20 @@ import "helpers/helpers.spec" import "methods/IAccessControl.spec" +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Definitions │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +definition DEFAULT_ADMIN_ROLE() returns bytes32 = 0x0000000000000000000000000000000000000000000000000000000000000000; + /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Identify entrypoints: only grantRole, revokeRole and renounceRole can alter permissions │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule onlyGrantCanGrant(env e, bytes32 role, address account) { - method f; calldataarg args; +rule onlyGrantCanGrant(env e, method f, bytes32 role, address account) { + calldataarg args; bool hasRoleBefore = hasRole(role, account); f(e, args); @@ -34,10 +41,9 @@ rule onlyGrantCanGrant(env e, bytes32 role, address account) { │ Function correctness: grantRole only affects the specified user/role combo │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule grantRoleEffect(env e) { +rule grantRoleEffect(env e, bytes32 role) { require nonpayable(e); - bytes32 role; bytes32 otherRole; address account; address otherAccount; @@ -65,10 +71,9 @@ rule grantRoleEffect(env e) { │ Function correctness: revokeRole only affects the specified user/role combo │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule revokeRoleEffect(env e) { +rule revokeRoleEffect(env e, bytes32 role) { require nonpayable(e); - bytes32 role; bytes32 otherRole; address account; address otherAccount; @@ -96,10 +101,9 @@ rule revokeRoleEffect(env e) { │ Function correctness: renounceRole only affects the specified user/role combo │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ -rule renounceRoleEffect(env e) { +rule renounceRoleEffect(env e, bytes32 role) { require nonpayable(e); - bytes32 role; bytes32 otherRole; address account; address otherAccount; diff --git a/certora/specs/AccessControlDefaultAdminRules.spec b/certora/specs/AccessControlDefaultAdminRules.spec new file mode 100644 index 000000000..a4baa1871 --- /dev/null +++ b/certora/specs/AccessControlDefaultAdminRules.spec @@ -0,0 +1,500 @@ +import "helpers/helpers.spec" +import "methods/IAccessControlDefaultAdminRules.spec" +import "methods/IAccessControl.spec" +import "AccessControl.spec" + +use rule onlyGrantCanGrant filtered { + f -> f.selector != acceptDefaultAdminTransfer().selector +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Helpers │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ + +function max_uint48() returns mathint { + return (1 << 48) - 1; +} + +function nonZeroAccount(address account) returns bool { + return account != 0; +} + +function timeSanity(env e) returns bool { + return + e.block.timestamp > 0 && // Avoids 0 schedules + e.block.timestamp + defaultAdminDelay(e) < max_uint48(); +} + +function delayChangeWaitSanity(env e, uint48 newDelay) returns bool { + return e.block.timestamp + delayChangeWait_(e, newDelay) < max_uint48(); +} + +function isSet(uint48 schedule) returns bool { + return schedule != 0; +} + +function hasPassed(env e, uint48 schedule) returns bool { + return schedule < e.block.timestamp; +} + +function min(uint48 a, uint48 b) returns mathint { + return a < b ? a : b; +} + +function increasingDelaySchedule(env e, uint48 newDelay) returns mathint { + return e.block.timestamp + min(newDelay, defaultAdminDelayIncreaseWait()); +} + +function decreasingDelaySchedule(env e, uint48 newDelay) returns mathint { + return e.block.timestamp + defaultAdminDelay(e) - newDelay; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: defaultAdmin holds the DEFAULT_ADMIN_ROLE │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant defaultAdminConsistency(address account) + defaultAdmin() == account <=> hasRole(DEFAULT_ADMIN_ROLE(), account) + { + preserved { + // defaultAdmin() returns the zero address when there's no default admin + require nonZeroAccount(account); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: Only one account holds the DEFAULT_ADMIN_ROLE │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant singleDefaultAdmin(address account, address another) + hasRole(DEFAULT_ADMIN_ROLE(), account) && hasRole(DEFAULT_ADMIN_ROLE(), another) => another == account + // We filter here because we couldn't find a way to force Certora to have an initial state with + // only one DEFAULT_ADMIN_ROLE enforced, so a counter example is a different default admin since inception + // triggering the transfer, which is known to be impossible by definition. + filtered { f -> f.selector != acceptDefaultAdminTransfer().selector } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: DEFAULT_ADMIN_ROLE's admin is always DEFAULT_ADMIN_ROLE │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant defaultAdminRoleAdminConsistency() + getRoleAdmin(DEFAULT_ADMIN_ROLE()) == DEFAULT_ADMIN_ROLE() + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: owner is the defaultAdmin │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant ownerConsistency() + defaultAdmin() == owner() + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: revokeRole only affects the specified user/role combo │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule revokeRoleEffect(env e, bytes32 role) { + require nonpayable(e); + + bytes32 otherRole; + address account; + address otherAccount; + + bool isCallerAdmin = hasRole(getRoleAdmin(role), e.msg.sender); + bool hasOtherRoleBefore = hasRole(otherRole, otherAccount); + + revokeRole@withrevert(e, role, account); + bool success = !lastReverted; + + bool hasOtherRoleAfter = hasRole(otherRole, otherAccount); + + // liveness + assert success <=> isCallerAdmin && role != DEFAULT_ADMIN_ROLE(), + "roles can only be revoked by their owner except for the default admin role"; + + // effect + assert success => !hasRole(role, account), "role is revoked"; + + // no side effect + assert hasOtherRoleBefore != hasOtherRoleAfter => (role == otherRole && account == otherAccount), + "no other role is affected"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: renounceRole only affects the specified user/role combo │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule renounceRoleEffect(env e, bytes32 role) { + require nonpayable(e); + + bytes32 otherRole; + address account; + address otherAccount; + + bool hasOtherRoleBefore = hasRole(otherRole, otherAccount); + uint48 scheduleBefore = pendingDefaultAdminSchedule_(); + address pendingAdminBefore = pendingDefaultAdmin_(); + + renounceRole@withrevert(e, role, account); + bool success = !lastReverted; + + bool hasOtherRoleAfter = hasRole(otherRole, otherAccount); + + // liveness + assert success <=> ( + account == e.msg.sender && + ( + ( + role != DEFAULT_ADMIN_ROLE() + ) || ( + role == DEFAULT_ADMIN_ROLE() && + pendingAdminBefore == 0 && + isSet(scheduleBefore) && + hasPassed(e, scheduleBefore) + ) + ) + ), "an account only can renounce by itself with a delay for the default admin role"; + + // effect + assert success => !hasRole(role, account), "role is renounced"; + + // no side effect + assert hasOtherRoleBefore != hasOtherRoleAfter => (role == otherRole && account == otherAccount), + "no other role is affected"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: defaultAdmin is only affected by accepting an admin transfer or renoucing │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noDefaultAdminChange(env e, method f, calldataarg args) { + require nonZeroAccount(e.msg.sender); + requireInvariant defaultAdminConsistency(defaultAdmin()); + requireInvariant singleDefaultAdmin(e.msg.sender, defaultAdmin()); + + address adminBefore = defaultAdmin(); + f(e, args); + address adminAfter = defaultAdmin(); + + assert adminBefore != adminAfter => ( + f.selector == acceptDefaultAdminTransfer().selector || + f.selector == renounceRole(bytes32,address).selector + ), "default admin is only affected by accepting an admin transfer or renoucing"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: pendingDefaultAdmin is only affected by beginning, accepting or canceling an admin transfer │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noPendingDefaultAdminChange(env e, method f, calldataarg args) { + requireInvariant defaultAdminConsistency(defaultAdmin()); + requireInvariant singleDefaultAdmin(e.msg.sender, defaultAdmin()); + + address pendingAdminBefore = pendingDefaultAdmin_(); + address scheduleBefore = pendingDefaultAdminSchedule_(); + f(e, args); + address pendingAdminAfter = pendingDefaultAdmin_(); + address scheduleAfter = pendingDefaultAdminSchedule_(); + + assert ( + pendingAdminBefore != pendingAdminAfter || + scheduleBefore != scheduleAfter + ) => ( + f.selector == beginDefaultAdminTransfer(address).selector || + f.selector == acceptDefaultAdminTransfer().selector || + f.selector == cancelDefaultAdminTransfer().selector + ), "pending admin and its schedule is only affected by beginning, accepting or cancelling an admin transfer"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: defaultAdminDelay can't be changed atomically by any function │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noDefaultAdminDelayChange(env e, method f, calldataarg args) { + uint48 delayBefore = defaultAdminDelay(e); + f(e, args); + uint48 delayAfter = defaultAdminDelay(e); + + assert delayBefore == delayAfter, "delay can't be changed atomically by any function"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: pendingDefaultAdminDelay is only affected by changeDefaultAdminDelay or rollbackDefaultAdminDelay │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noPendingDefaultAdminDelayChange(env e, method f, calldataarg args) { + uint48 pendingDelayBefore = pendingDelay_(e); + f(e, args); + uint48 pendingDelayAfter = pendingDelay_(e); + + assert pendingDelayBefore != pendingDelayAfter => ( + f.selector == changeDefaultAdminDelay(uint48).selector || + f.selector == rollbackDefaultAdminDelay().selector + ), "pending delay is only affected by changeDefaultAdminDelay or rollbackDefaultAdminDelay"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: defaultAdminDelayIncreaseWait can't be changed atomically by any function │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noDefaultAdminDelayIncreaseWaitChange(env e, method f, calldataarg args) { + uint48 delayIncreaseWaitBefore = defaultAdminDelayIncreaseWait(); + f(e, args); + uint48 delayIncreaseWaitAfter = defaultAdminDelayIncreaseWait(); + + assert delayIncreaseWaitBefore == delayIncreaseWaitAfter, + "delay increase wait can't be changed atomically by any function"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: beginDefaultAdminTransfer sets a pending default admin and its schedule │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule beginDefaultAdminTransfer(env e, address newAdmin) { + require nonpayable(e); + require timeSanity(e); + requireInvariant defaultAdminConsistency(defaultAdmin()); + requireInvariant singleDefaultAdmin(e.msg.sender, defaultAdmin()); + + beginDefaultAdminTransfer@withrevert(e, newAdmin); + bool success = !lastReverted; + + // liveness + assert success <=> e.msg.sender == defaultAdmin(), + "only the current default admin can begin a transfer"; + + // effect + assert success => pendingDefaultAdmin_() == newAdmin, + "pending default admin is set"; + assert success => pendingDefaultAdminSchedule_() == e.block.timestamp + defaultAdminDelay(e), + "pending default admin delay is set"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: A default admin can't change in less than the applied schedule │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pendingDefaultAdminDelayEnforced(env e1, env e2, method f, calldataarg args, address newAdmin) { + require e1.block.timestamp < e2.block.timestamp; + + uint48 delayBefore = defaultAdminDelay(e1); + address adminBefore = defaultAdmin(); + // There might be a better way to generalize this without requiring `beginDefaultAdminTransfer`, but currently + // it's the only way in which we can attest that only `delayBefore` has passed before a change. + beginDefaultAdminTransfer(e1, newAdmin); + f(e2, args); + address adminAfter = defaultAdmin(); + + assert adminAfter == newAdmin => ((e2.block.timestamp >= e1.block.timestamp + delayBefore) || adminBefore == newAdmin), + "A delay can't change in less than applied schedule"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: acceptDefaultAdminTransfer updates defaultAdmin resetting the pending admin and its schedule │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule acceptDefaultAdminTransfer(env e) { + require nonpayable(e); + requireInvariant defaultAdminConsistency(defaultAdmin()); + requireInvariant singleDefaultAdmin(e.msg.sender, defaultAdmin()); + + address pendingAdminBefore = pendingDefaultAdmin_(); + uint48 scheduleAfter = pendingDefaultAdminSchedule_(); + + acceptDefaultAdminTransfer@withrevert(e); + bool success = !lastReverted; + + // liveness + assert success <=> e.msg.sender == pendingAdminBefore && isSet(scheduleAfter) && hasPassed(e, scheduleAfter), + "only the pending default admin can accept the role after the schedule has been set and passed"; + + // effect + assert success => defaultAdmin() == pendingAdminBefore, + "Default admin is set to the previous pending default admin"; + assert success => pendingDefaultAdmin_() == 0, + "Pending default admin is reset"; + assert success => pendingDefaultAdminSchedule_() == 0, + "Pending default admin delay is reset"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: cancelDefaultAdminTransfer resets pending default admin and its schedule │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule cancelDefaultAdminTransfer(env e) { + require nonpayable(e); + requireInvariant defaultAdminConsistency(defaultAdmin()); + requireInvariant singleDefaultAdmin(e.msg.sender, defaultAdmin()); + + cancelDefaultAdminTransfer@withrevert(e); + bool success = !lastReverted; + + // liveness + assert success <=> e.msg.sender == defaultAdmin(), + "only the current default admin can cancel a transfer"; + + // effect + assert success => pendingDefaultAdmin_() == 0, + "Pending default admin is reset"; + assert success => pendingDefaultAdminSchedule_() == 0, + "Pending default admin delay is reset"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: changeDefaultAdminDelay sets a pending default admin delay and its schedule │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule changeDefaultAdminDelay(env e, uint48 newDelay) { + require nonpayable(e); + require timeSanity(e); + require delayChangeWaitSanity(e, newDelay); + requireInvariant defaultAdminConsistency(defaultAdmin()); + requireInvariant singleDefaultAdmin(e.msg.sender, defaultAdmin()); + + uint48 delayBefore = defaultAdminDelay(e); + + changeDefaultAdminDelay@withrevert(e, newDelay); + bool success = !lastReverted; + + // liveness + assert success <=> e.msg.sender == defaultAdmin(), + "only the current default admin can begin a delay change"; + + // effect + assert success => pendingDelay_(e) == newDelay, "pending delay is set"; + assert success => ( + pendingDelaySchedule_(e) > e.block.timestamp || + delayBefore == newDelay || // Interpreted as decreasing, x - x = 0 + defaultAdminDelayIncreaseWait() == 0 + ), + "pending delay schedule is set in the future unless accepted edge cases"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: A delay can't change in less than the applied schedule │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pendingDelayWaitEnforced(env e1, env e2, method f, calldataarg args, uint48 newDelay) { + require e1.block.timestamp < e2.block.timestamp; + + uint48 delayBefore = defaultAdminDelay(e1); + changeDefaultAdminDelay(e1, newDelay); + f(e2, args); + uint48 delayAfter = defaultAdminDelay(e2); + + mathint delayWait = newDelay > delayBefore ? increasingDelaySchedule(e1, newDelay) : decreasingDelaySchedule(e1, newDelay); + + assert delayAfter == newDelay => (e2.block.timestamp >= delayWait || delayBefore == newDelay), + "A delay can't change in less than applied schedule"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: pending delay wait is set depending on increasing or decreasing the delay │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pendingDelayWait(env e, uint48 newDelay) { + uint48 oldDelay = defaultAdminDelay(e); + changeDefaultAdminDelay(e, newDelay); + + assert newDelay > oldDelay => pendingDelaySchedule_(e) == increasingDelaySchedule(e, newDelay), + "Delay wait is the minimum between the new delay and a threshold when the delay is increased"; + assert newDelay <= oldDelay => pendingDelaySchedule_(e) == decreasingDelaySchedule(e, newDelay), + "Delay wait is the difference between the current and the new delay when the delay is decreased"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: rollbackDefaultAdminDelay resets the delay and its schedule │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule rollbackDefaultAdminDelay(env e) { + require nonpayable(e); + requireInvariant defaultAdminConsistency(defaultAdmin()); + requireInvariant singleDefaultAdmin(e.msg.sender, defaultAdmin()); + + rollbackDefaultAdminDelay@withrevert(e); + bool success = !lastReverted; + + // liveness + assert success <=> e.msg.sender == defaultAdmin(), + "only the current default admin can rollback a delay change"; + + // effect + assert success => pendingDelay_(e) == 0, + "Pending default admin is reset"; + assert success => pendingDelaySchedule_(e) == 0, + "Pending default admin delay is reset"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: pending default admin and the delay can only change along with their corresponding schedules │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pendingValueAndScheduleCoupling(env e, address newAdmin, uint48 newDelay) { + requireInvariant defaultAdminConsistency(defaultAdmin()); + requireInvariant singleDefaultAdmin(e.msg.sender, defaultAdmin()); + + // Pending admin + address pendingAdminBefore = pendingDefaultAdmin_(); + uint48 pendingAdminScheduleBefore = pendingDefaultAdminSchedule_(); + + beginDefaultAdminTransfer(e, newAdmin); + + address pendingAdminAfter = pendingDefaultAdmin_(); + uint48 pendingAdminScheduleAfter = pendingDefaultAdminSchedule_(); + + assert ( + pendingAdminScheduleBefore != pendingDefaultAdminSchedule_() && + pendingAdminBefore == pendingAdminAfter + ) => newAdmin == pendingAdminBefore, "pending admin stays the same if the new admin set is the same"; + + assert ( + pendingAdminBefore != pendingAdminAfter && + pendingAdminScheduleBefore == pendingDefaultAdminSchedule_() + ) => ( + // Schedule doesn't change if: + // - The defaultAdminDelay was reduced to a value such that added to the block.timestamp is equal to previous schedule + e.block.timestamp + defaultAdminDelay(e) == pendingAdminScheduleBefore + ), "pending admin stays the same if a default admin transfer is begun on accepted edge cases"; + + // Pending delay + address pendingDelayBefore = pendingDelay_(e); + uint48 pendingDelayScheduleBefore = pendingDelaySchedule_(e); + + changeDefaultAdminDelay(e, newDelay); + + address pendingDelayAfter = pendingDelay_(e); + uint48 pendingDelayScheduleAfter = pendingDelaySchedule_(e); + + assert ( + pendingDelayScheduleBefore != pendingDelayScheduleAfter && + pendingDelayBefore == pendingDelayAfter + ) => newDelay == pendingDelayBefore || pendingDelayBefore == 0, "pending delay stays the same if the new delay set is the same"; + + assert ( + pendingDelayBefore != pendingDelayAfter && + pendingDelayScheduleBefore == pendingDelayScheduleAfter + ) => ( + increasingDelaySchedule(e, newDelay) == pendingDelayScheduleBefore || + decreasingDelaySchedule(e, newDelay) == pendingDelayScheduleBefore + ), "pending delay stays the same if a default admin transfer is begun on accepted edge cases"; +} diff --git a/certora/specs/methods/IAccessControlDefaultAdminRules.spec b/certora/specs/methods/IAccessControlDefaultAdminRules.spec new file mode 100644 index 000000000..a9dd08b7f --- /dev/null +++ b/certora/specs/methods/IAccessControlDefaultAdminRules.spec @@ -0,0 +1,36 @@ +import "./IERC5313.spec" + +methods { + // === View == + + // Default Admin + defaultAdmin() returns(address) envfree + pendingDefaultAdmin() returns(address, uint48) envfree + + // Default Admin Delay + defaultAdminDelay() returns(uint48) + pendingDefaultAdminDelay() returns(uint48, uint48) + defaultAdminDelayIncreaseWait() returns(uint48) envfree + + // === Mutations == + + // Default Admin + beginDefaultAdminTransfer(address) + cancelDefaultAdminTransfer() + acceptDefaultAdminTransfer() + + // Default Admin Delay + changeDefaultAdminDelay(uint48) + rollbackDefaultAdminDelay() + + // == FV == + + // Default Admin + pendingDefaultAdmin_() returns (address) envfree + pendingDefaultAdminSchedule_() returns (uint48) envfree + + // Default Admin Delay + pendingDelay_() returns (uint48) + pendingDelaySchedule_() returns (uint48) + delayChangeWait_(uint48) returns (uint48) +} diff --git a/certora/specs/methods/IERC5313.spec b/certora/specs/methods/IERC5313.spec new file mode 100644 index 000000000..d4c5a0412 --- /dev/null +++ b/certora/specs/methods/IERC5313.spec @@ -0,0 +1,3 @@ +methods { + owner() returns (address) envfree +} From 72ed4ca67a99bba17852fb6a7dea558980260757 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 5 May 2023 21:31:23 +0200 Subject: [PATCH 118/197] Ensure AccessControlDefaultAdminRules's initialDefaultAdmin is non-zero (#4220) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- contracts/access/AccessControlDefaultAdminRules.sol | 1 + test/access/AccessControlDefaultAdminRules.test.js | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/contracts/access/AccessControlDefaultAdminRules.sol b/contracts/access/AccessControlDefaultAdminRules.sol index 0c640fb99..6cdda81a1 100644 --- a/contracts/access/AccessControlDefaultAdminRules.sol +++ b/contracts/access/AccessControlDefaultAdminRules.sol @@ -53,6 +53,7 @@ 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"); _currentDelay = initialDelay; _grantRole(DEFAULT_ADMIN_ROLE, initialDefaultAdmin); } diff --git a/test/access/AccessControlDefaultAdminRules.test.js b/test/access/AccessControlDefaultAdminRules.test.js index 4e3167a46..be112481e 100644 --- a/test/access/AccessControlDefaultAdminRules.test.js +++ b/test/access/AccessControlDefaultAdminRules.test.js @@ -1,4 +1,4 @@ -const { time } = require('@openzeppelin/test-helpers'); +const { time, constants, expectRevert } = require('@openzeppelin/test-helpers'); const { shouldBehaveLikeAccessControl, shouldBehaveLikeAccessControlDefaultAdminRules, @@ -13,6 +13,13 @@ contract('AccessControlDefaultAdminRules', function (accounts) { this.accessControl = await AccessControlDefaultAdminRules.new(delay, accounts[0], { from: accounts[0] }); }); + it('initial admin not zero', async function () { + await expectRevert( + AccessControlDefaultAdminRules.new(delay, constants.ZERO_ADDRESS), + 'AccessControl: 0 default admin', + ); + }); + shouldBehaveLikeAccessControl('AccessControl', ...accounts); shouldBehaveLikeAccessControlDefaultAdminRules('AccessControl', delay, ...accounts); }); From 692d8c85a494f78b83aa4de44dcce4e3e63997d1 Mon Sep 17 00:00:00 2001 From: ToonVanHove Date: Fri, 5 May 2023 22:25:23 +0200 Subject: [PATCH 119/197] Fix lookup documentation in ERC20Votes and Checkpoints (#4218) Co-authored-by: Hadrien Croubois --- .../token/ERC20/extensions/ERC20Votes.sol | 2 +- contracts/utils/Checkpoints.sol | 24 +++++++++---------- scripts/generate/templates/Checkpoints.js | 10 ++++---- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index d72d34f81..f44b89604 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -114,7 +114,7 @@ abstract contract ERC20Votes is ERC20Permit, IERC5805 { * @dev Lookup a value in a list of (sorted) checkpoints. */ function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 timepoint) private view returns (uint256) { - // We run a binary search to look for the earliest checkpoint taken after `timepoint`. + // We run a binary search to look for the last (most recent) checkpoint taken before (or at) `timepoint`. // // Initially we check if the block is recent to narrow the search range. // During the loop, the index of the wanted checkpoint remains in the range [low-1, high). diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index 7d7905fd1..27da4b517 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -151,7 +151,7 @@ library Checkpoints { } /** - * @dev Return the index of the oldest checkpoint whose key is greater than the search key, or `high` if there is none. + * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or `high` if there is none. * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`. * * WARNING: `high` should not be greater than the array's length. @@ -174,7 +174,7 @@ library Checkpoints { } /** - * @dev Return the index of the oldest checkpoint whose key is greater or equal than the search key, or `high` if there is none. + * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or `high` if there is none. * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`. * * WARNING: `high` should not be greater than the array's length. @@ -225,7 +225,7 @@ library Checkpoints { } /** - * @dev Returns the value in the oldest checkpoint with key greater or equal than the search key, or zero if there is none. + * @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if there is none. */ function lowerLookup(Trace224 storage self, uint32 key) internal view returns (uint224) { uint256 len = self._checkpoints.length; @@ -234,7 +234,7 @@ library Checkpoints { } /** - * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key. + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key. */ function upperLookup(Trace224 storage self, uint32 key) internal view returns (uint224) { uint256 len = self._checkpoints.length; @@ -243,7 +243,7 @@ library Checkpoints { } /** - * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key. + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key. * * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys). */ @@ -324,7 +324,7 @@ library Checkpoints { } /** - * @dev Return the index of the oldest checkpoint whose key is greater than the search key, or `high` if there is none. + * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or `high` if there is none. * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`. * * WARNING: `high` should not be greater than the array's length. @@ -347,7 +347,7 @@ library Checkpoints { } /** - * @dev Return the index of the oldest checkpoint whose key is greater or equal than the search key, or `high` if there is none. + * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or `high` if there is none. * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`. * * WARNING: `high` should not be greater than the array's length. @@ -401,7 +401,7 @@ library Checkpoints { } /** - * @dev Returns the value in the oldest checkpoint with key greater or equal than the search key, or zero if there is none. + * @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if there is none. */ function lowerLookup(Trace160 storage self, uint96 key) internal view returns (uint160) { uint256 len = self._checkpoints.length; @@ -410,7 +410,7 @@ library Checkpoints { } /** - * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key. + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key. */ function upperLookup(Trace160 storage self, uint96 key) internal view returns (uint160) { uint256 len = self._checkpoints.length; @@ -419,7 +419,7 @@ library Checkpoints { } /** - * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key. + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key. * * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys). */ @@ -500,7 +500,7 @@ library Checkpoints { } /** - * @dev Return the index of the oldest checkpoint whose key is greater than the search key, or `high` if there is none. + * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or `high` if there is none. * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`. * * WARNING: `high` should not be greater than the array's length. @@ -523,7 +523,7 @@ library Checkpoints { } /** - * @dev Return the index of the oldest checkpoint whose key is greater or equal than the search key, or `high` if there is none. + * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or `high` if there is none. * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`. * * WARNING: `high` should not be greater than the array's length. diff --git a/scripts/generate/templates/Checkpoints.js b/scripts/generate/templates/Checkpoints.js index 18999682d..1d79031da 100644 --- a/scripts/generate/templates/Checkpoints.js +++ b/scripts/generate/templates/Checkpoints.js @@ -46,7 +46,7 @@ function push( } /** - * @dev Returns the value in the oldest checkpoint with key greater or equal than the search key, or zero if there is none. + * @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if there is none. */ function lowerLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) { uint256 len = self.${opts.checkpointFieldName}.length; @@ -55,7 +55,7 @@ function lowerLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} k } /** - * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key. + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key. */ function upperLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) { uint256 len = self.${opts.checkpointFieldName}.length; @@ -64,7 +64,7 @@ function upperLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} k } /** - * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key. + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key. * * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys). */ @@ -227,7 +227,7 @@ function _insert( } /** - * @dev Return the index of the oldest checkpoint whose key is greater than the search key, or \`high\` if there is none. + * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or \`high\` if there is none. * \`low\` and \`high\` define a section where to do the search, with inclusive \`low\` and exclusive \`high\`. * * WARNING: \`high\` should not be greater than the array's length. @@ -250,7 +250,7 @@ function _upperBinaryLookup( } /** - * @dev Return the index of the oldest checkpoint whose key is greater or equal than the search key, or \`high\` if there is none. + * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or \`high\` if there is none. * \`low\` and \`high\` define a section where to do the search, with inclusive \`low\` and exclusive \`high\`. * * WARNING: \`high\` should not be greater than the array's length. From 908f78d07b7b28c79af04bf0fc90501a81246b0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Mon, 8 May 2023 23:00:49 +0200 Subject: [PATCH 120/197] Enable more Slither detectors (#4219) --- package.json | 2 +- slither.config.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 56798f788..a3b98fe37 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "test:inheritance": "scripts/checks/inheritance-ordering.js artifacts/build-info/*", "test:generation": "scripts/checks/generation.sh", "gas-report": "env ENABLE_GAS_REPORT=true npm run test", - "slither": "npm run clean && slither . --detect reentrancy-eth,reentrancy-no-eth,reentrancy-unlimited-gas" + "slither": "npm run clean && slither ." }, "repository": { "type": "git", diff --git a/slither.config.json b/slither.config.json index 2b618794a..069da1f3a 100644 --- a/slither.config.json +++ b/slither.config.json @@ -1,5 +1,5 @@ { - "detectors_to_run": "reentrancy-eth,reentrancy-no-eth,reentrancy-unlimited-gas", - "filter_paths": "contracts/mocks", + "detectors_to_run": "arbitrary-send-erc20,array-by-reference,incorrect-shift,name-reused,rtlo,suicidal,uninitialized-state,uninitialized-storage,arbitrary-send-erc20-permit,controlled-array-length,controlled-delegatecall,delegatecall-loop,msg-value-loop,reentrancy-eth,unchecked-transfer,weak-prng,domain-separator-collision,erc20-interface,erc721-interface,locked-ether,mapping-deletion,shadowing-abstract,tautology,write-after-write,boolean-cst,reentrancy-no-eth,reused-constructor,tx-origin,unchecked-lowlevel,unchecked-send,variable-scope,void-cst,events-access,events-maths,incorrect-unary,boolean-equal,cyclomatic-complexity,deprecated-standards,erc20-indexed,function-init-state,pragma,unused-state,reentrancy-unlimited-gas,constable-states,immutable-states,var-read-using-this", + "filter_paths": "contracts/mocks,contracts-exposed", "compile_force_framework": "hardhat" -} \ No newline at end of file +} From 832c352c7d68c75a89974bd1e4623bd1a1618ab0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 May 2023 00:04:41 -0300 Subject: [PATCH 121/197] Update lockfile (#4203) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 3346 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 2547 insertions(+), 799 deletions(-) diff --git a/package-lock.json b/package-lock.json index f44a1b939..2219b08f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,9 +53,9 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", "dev": true, "dependencies": { "@babel/highlight": "^7.18.6" @@ -88,9 +88,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", "dev": true, "dependencies": { "regenerator-runtime": "^0.13.11" @@ -99,6 +99,32 @@ "node": ">=6.9.0" } }, + "node_modules/@chainsafe/as-sha256": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz", + "integrity": "sha512-hldFFYuf49ed7DAakWVXSJODuq3pzJEguD8tQ7h+sGkM18vja+OFoJI9krnGmgzyuZC2ETX0NOIcCTy31v2Mtg==", + "dev": true + }, + "node_modules/@chainsafe/persistent-merkle-tree": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.4.2.tgz", + "integrity": "sha512-lLO3ihKPngXLTus/L7WHKaw9PnNJWizlOF1H9NNzHP6Xvh82vzg9F2bzkXhYIFshMZ2gTCEz8tq6STe7r5NDfQ==", + "dev": true, + "dependencies": { + "@chainsafe/as-sha256": "^0.3.1" + } + }, + "node_modules/@chainsafe/ssz": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@chainsafe/ssz/-/ssz-0.9.4.tgz", + "integrity": "sha512-77Qtg2N1ayqs4Bg/wvnWfg5Bta7iy7IRh8XqXh7oNMeP2HBbBwx8m6yTpA8p0EHItWPEBkgZd5S5/LSlp3GXuQ==", + "dev": true, + "dependencies": { + "@chainsafe/as-sha256": "^0.3.1", + "@chainsafe/persistent-merkle-tree": "^0.4.2", + "case": "^1.6.3" + } + }, "node_modules/@changesets/apply-release-plan": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-6.1.3.tgz", @@ -173,9 +199,9 @@ } }, "node_modules/@changesets/cli": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.26.0.tgz", - "integrity": "sha512-0cbTiDms+ICTVtEwAFLNW0jBNex9f5+fFv3I771nBvdnV/mOjd1QJ4+f8KtVSOrwD9SJkk9xbDkWFb0oXd8d1Q==", + "version": "2.26.1", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.26.1.tgz", + "integrity": "sha512-XnTa+b51vt057fyAudvDKGB0Sh72xutQZNAdXkCqPBKO2zvs2yYZx5hFZj1u9cbtpwM6Sxtcr02/FQJfZOzemQ==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", @@ -500,9 +526,9 @@ "dev": true }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz", - "integrity": "sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" @@ -515,23 +541,23 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.0.tgz", - "integrity": "sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.1.tgz", - "integrity": "sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", + "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.0", + "espree": "^9.5.1", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -565,9 +591,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.36.0.tgz", - "integrity": "sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", + "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1536,34 +1562,84 @@ } }, "node_modules/@nomicfoundation/ethereumjs-block": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-4.0.0.tgz", - "integrity": "sha512-bk8uP8VuexLgyIZAHExH1QEovqx0Lzhc9Ntm63nCRKLHXIZkobaFaeCVwTESV7YkPKUk7NiK11s8ryed4CS9yA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-5.0.1.tgz", + "integrity": "sha512-u1Yioemi6Ckj3xspygu/SfFvm8vZEO8/Yx5a1QLzi6nVU0jz3Pg2OmHKJ5w+D9Ogk1vhwRiqEBAqcb0GVhCyHw==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-tx": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", - "ethereum-cryptography": "0.1.3" + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-trie": "6.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", + "ethereum-cryptography": "0.1.3", + "ethers": "^5.7.1" }, "engines": { "node": ">=14" } }, + "node_modules/@nomicfoundation/ethereumjs-block/node_modules/ethers": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", + "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abi": "5.7.0", + "@ethersproject/abstract-provider": "5.7.0", + "@ethersproject/abstract-signer": "5.7.0", + "@ethersproject/address": "5.7.0", + "@ethersproject/base64": "5.7.0", + "@ethersproject/basex": "5.7.0", + "@ethersproject/bignumber": "5.7.0", + "@ethersproject/bytes": "5.7.0", + "@ethersproject/constants": "5.7.0", + "@ethersproject/contracts": "5.7.0", + "@ethersproject/hash": "5.7.0", + "@ethersproject/hdnode": "5.7.0", + "@ethersproject/json-wallets": "5.7.0", + "@ethersproject/keccak256": "5.7.0", + "@ethersproject/logger": "5.7.0", + "@ethersproject/networks": "5.7.1", + "@ethersproject/pbkdf2": "5.7.0", + "@ethersproject/properties": "5.7.0", + "@ethersproject/providers": "5.7.2", + "@ethersproject/random": "5.7.0", + "@ethersproject/rlp": "5.7.0", + "@ethersproject/sha2": "5.7.0", + "@ethersproject/signing-key": "5.7.0", + "@ethersproject/solidity": "5.7.0", + "@ethersproject/strings": "5.7.0", + "@ethersproject/transactions": "5.7.0", + "@ethersproject/units": "5.7.0", + "@ethersproject/wallet": "5.7.0", + "@ethersproject/web": "5.7.1", + "@ethersproject/wordlists": "5.7.0" + } + }, "node_modules/@nomicfoundation/ethereumjs-blockchain": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-6.0.0.tgz", - "integrity": "sha512-pLFEoea6MWd81QQYSReLlLfH7N9v7lH66JC/NMPN848ySPPQA5renWnE7wPByfQFzNrPBuDDRFFULMDmj1C0xw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-7.0.1.tgz", + "integrity": "sha512-NhzndlGg829XXbqJEYrF1VeZhAwSPgsK/OB7TVrdzft3y918hW5KNd7gIZ85sn6peDZOdjBsAXIpXZ38oBYE5A==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-block": "^4.0.0", - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-ethash": "^2.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-block": "5.0.1", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-ethash": "3.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-trie": "6.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "abstract-level": "^1.0.3", "debug": "^4.3.3", "ethereum-cryptography": "0.1.3", @@ -1576,24 +1652,24 @@ } }, "node_modules/@nomicfoundation/ethereumjs-common": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-3.0.0.tgz", - "integrity": "sha512-WS7qSshQfxoZOpHG/XqlHEGRG1zmyjYrvmATvc4c62+gZXgre1ymYP8ZNgx/3FyZY0TWe9OjFlKOfLqmgOeYwA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-4.0.1.tgz", + "integrity": "sha512-OBErlkfp54GpeiE06brBW/TTbtbuBJV5YI5Nz/aB2evTDo+KawyEzPjBlSr84z/8MFfj8wS2wxzQX1o32cev5g==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-util": "9.0.1", "crc-32": "^1.2.0" } }, "node_modules/@nomicfoundation/ethereumjs-ethash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-2.0.0.tgz", - "integrity": "sha512-WpDvnRncfDUuXdsAXlI4lXbqUDOA+adYRQaEezIkxqDkc+LDyYDbd/xairmY98GnQzo1zIqsIL6GB5MoMSJDew==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-3.0.1.tgz", + "integrity": "sha512-KDjGIB5igzWOp8Ik5I6QiRH5DH+XgILlplsHR7TEuWANZA759G6krQ6o8bvj+tRUz08YygMQu/sGd9mJ1DYT8w==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-block": "^4.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-block": "5.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "abstract-level": "^1.0.3", "bigint-crypto-utils": "^3.0.23", "ethereum-cryptography": "0.1.3" @@ -1603,15 +1679,15 @@ } }, "node_modules/@nomicfoundation/ethereumjs-evm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-1.0.0.tgz", - "integrity": "sha512-hVS6qRo3V1PLKCO210UfcEQHvlG7GqR8iFzp0yyjTg2TmJQizcChKgWo8KFsdMw6AyoLgLhHGHw4HdlP8a4i+Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-2.0.1.tgz", + "integrity": "sha512-oL8vJcnk0Bx/onl+TgQOQ1t/534GKFaEG17fZmwtPFeH8S5soiBYPCLUrvANOl4sCp9elYxIMzIiTtMtNNN8EQ==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", - "@types/async-eventemitter": "^0.2.1", - "async-eventemitter": "^0.2.4", + "@ethersproject/providers": "^5.7.1", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "debug": "^4.3.3", "ethereum-cryptography": "0.1.3", "mcl-wasm": "^0.7.1", @@ -1622,9 +1698,9 @@ } }, "node_modules/@nomicfoundation/ethereumjs-rlp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-4.0.0.tgz", - "integrity": "sha512-GaSOGk5QbUk4eBP5qFbpXoZoZUj/NrW7MRa0tKY4Ew4c2HAS0GXArEMAamtFrkazp0BO4K5p2ZCG3b2FmbShmw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-5.0.1.tgz", + "integrity": "sha512-xtxrMGa8kP4zF5ApBQBtjlSbN5E2HI8m8FYgVSYAnO6ssUoY5pVPGy2H8+xdf/bmMa22Ce8nWMH3aEW8CcqMeQ==", "dev": true, "bin": { "rlp": "bin/rlp" @@ -1634,28 +1710,76 @@ } }, "node_modules/@nomicfoundation/ethereumjs-statemanager": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-1.0.0.tgz", - "integrity": "sha512-jCtqFjcd2QejtuAMjQzbil/4NHf5aAWxUc+CvS0JclQpl+7M0bxMofR2AJdtz+P3u0ke2euhYREDiE7iSO31vQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-2.0.1.tgz", + "integrity": "sha512-B5ApMOnlruVOR7gisBaYwFX+L/AP7i/2oAahatssjPIBVDF6wTX1K7Qpa39E/nzsH8iYuL3krkYeUFIdO3EMUQ==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", "debug": "^4.3.3", "ethereum-cryptography": "0.1.3", - "functional-red-black-tree": "^1.0.1" + "ethers": "^5.7.1", + "js-sdsl": "^4.1.4" + } + }, + "node_modules/@nomicfoundation/ethereumjs-statemanager/node_modules/ethers": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", + "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abi": "5.7.0", + "@ethersproject/abstract-provider": "5.7.0", + "@ethersproject/abstract-signer": "5.7.0", + "@ethersproject/address": "5.7.0", + "@ethersproject/base64": "5.7.0", + "@ethersproject/basex": "5.7.0", + "@ethersproject/bignumber": "5.7.0", + "@ethersproject/bytes": "5.7.0", + "@ethersproject/constants": "5.7.0", + "@ethersproject/contracts": "5.7.0", + "@ethersproject/hash": "5.7.0", + "@ethersproject/hdnode": "5.7.0", + "@ethersproject/json-wallets": "5.7.0", + "@ethersproject/keccak256": "5.7.0", + "@ethersproject/logger": "5.7.0", + "@ethersproject/networks": "5.7.1", + "@ethersproject/pbkdf2": "5.7.0", + "@ethersproject/properties": "5.7.0", + "@ethersproject/providers": "5.7.2", + "@ethersproject/random": "5.7.0", + "@ethersproject/rlp": "5.7.0", + "@ethersproject/sha2": "5.7.0", + "@ethersproject/signing-key": "5.7.0", + "@ethersproject/solidity": "5.7.0", + "@ethersproject/strings": "5.7.0", + "@ethersproject/transactions": "5.7.0", + "@ethersproject/units": "5.7.0", + "@ethersproject/wallet": "5.7.0", + "@ethersproject/web": "5.7.1", + "@ethersproject/wordlists": "5.7.0" } }, "node_modules/@nomicfoundation/ethereumjs-trie": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-5.0.0.tgz", - "integrity": "sha512-LIj5XdE+s+t6WSuq/ttegJzZ1vliwg6wlb+Y9f4RlBpuK35B9K02bO7xU+E6Rgg9RGptkWd6TVLdedTI4eNc2A==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-6.0.1.tgz", + "integrity": "sha512-A64It/IMpDVODzCgxDgAAla8jNjNtsoQZIzZUfIV5AY6Coi4nvn7+VReBn5itlxMiL2yaTlQr9TRWp3CSI6VoA==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", + "@types/readable-stream": "^2.3.13", "ethereum-cryptography": "0.1.3", "readable-stream": "^3.6.0" }, @@ -1664,14 +1788,16 @@ } }, "node_modules/@nomicfoundation/ethereumjs-tx": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-4.0.0.tgz", - "integrity": "sha512-Gg3Lir2lNUck43Kp/3x6TfBNwcWC9Z1wYue9Nz3v4xjdcv6oDW9QSMJxqsKw9QEGoBBZ+gqwpW7+F05/rs/g1w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-5.0.1.tgz", + "integrity": "sha512-0HwxUF2u2hrsIM1fsasjXvlbDOq1ZHFV2dd1yGq8CA+MEYhaxZr8OTScpVkkxqMwBcc5y83FyPl0J9MZn3kY0w==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@chainsafe/ssz": "^0.9.2", + "@ethersproject/providers": "^5.7.2", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "ethereum-cryptography": "0.1.3" }, "engines": { @@ -1679,38 +1805,55 @@ } }, "node_modules/@nomicfoundation/ethereumjs-util": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-8.0.0.tgz", - "integrity": "sha512-2emi0NJ/HmTG+CGY58fa+DQuAoroFeSH9gKu9O6JnwTtlzJtgfTixuoOqLEgyyzZVvwfIpRueuePb8TonL1y+A==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-9.0.1.tgz", + "integrity": "sha512-TwbhOWQ8QoSCFhV/DDfSmyfFIHjPjFBj957219+V3jTZYZ2rf9PmDtNOeZWAE3p3vlp8xb02XGpd0v6nTUPbsA==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-rlp": "^4.0.0-beta.2", + "@chainsafe/ssz": "^0.10.0", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", "ethereum-cryptography": "0.1.3" }, "engines": { "node": ">=14" } }, - "node_modules/@nomicfoundation/ethereumjs-vm": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-6.0.0.tgz", - "integrity": "sha512-JMPxvPQ3fzD063Sg3Tp+UdwUkVxMoo1uML6KSzFhMH3hoQi/LMuXBoEHAoW83/vyNS9BxEe6jm6LmT5xdeEJ6w==", + "node_modules/@nomicfoundation/ethereumjs-util/node_modules/@chainsafe/persistent-merkle-tree": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.5.0.tgz", + "integrity": "sha512-l0V1b5clxA3iwQLXP40zYjyZYospQLZXzBVIhhr9kDg/1qHZfzzHw0jj4VPBijfYCArZDlPkRi1wZaV2POKeuw==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-block": "^4.0.0", - "@nomicfoundation/ethereumjs-blockchain": "^6.0.0", - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-evm": "^1.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-statemanager": "^1.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-tx": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", - "@types/async-eventemitter": "^0.2.1", - "async-eventemitter": "^0.2.4", + "@chainsafe/as-sha256": "^0.3.1" + } + }, + "node_modules/@nomicfoundation/ethereumjs-util/node_modules/@chainsafe/ssz": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@chainsafe/ssz/-/ssz-0.10.2.tgz", + "integrity": "sha512-/NL3Lh8K+0q7A3LsiFq09YXS9fPE+ead2rr7vM2QK8PLzrNsw3uqrif9bpRX5UxgeRjM+vYi+boCM3+GM4ovXg==", + "dev": true, + "dependencies": { + "@chainsafe/as-sha256": "^0.3.1", + "@chainsafe/persistent-merkle-tree": "^0.5.0" + } + }, + "node_modules/@nomicfoundation/ethereumjs-vm": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-7.0.1.tgz", + "integrity": "sha512-rArhyn0jPsS/D+ApFsz3yVJMQ29+pVzNZ0VJgkzAZ+7FqXSRtThl1C1prhmlVr3YNUlfpZ69Ak+RUT4g7VoOuQ==", + "dev": true, + "dependencies": { + "@nomicfoundation/ethereumjs-block": "5.0.1", + "@nomicfoundation/ethereumjs-blockchain": "7.0.1", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-evm": "2.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-statemanager": "2.0.1", + "@nomicfoundation/ethereumjs-trie": "6.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "debug": "^4.3.3", "ethereum-cryptography": "0.1.3", - "functional-red-black-tree": "^1.0.1", "mcl-wasm": "^0.7.1", "rustbn.js": "~0.2.0" }, @@ -2115,9 +2258,9 @@ } }, "node_modules/@openzeppelin/upgrades-core": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.24.1.tgz", - "integrity": "sha512-QhdIQDUykJ3vQauB6CheV7vk4zgn0e1iY+IDg7r1KqpA1m2bqIGjQCpzidW33K4bZc9zdJSPx2/Z6Um5KxCB7A==", + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.25.0.tgz", + "integrity": "sha512-vSxOSm1k+P156nNm15ydhOmSPGC37mnl092FMVOH+eGaoqLjr2Za6ULVjDMFzvMnG+sGE1UoDOqBNPfTm0ch8w==", "dev": true, "dependencies": { "cbor": "^8.0.0", @@ -2390,16 +2533,40 @@ "web3-utils": "1.8.2" } }, + "node_modules/@truffle/abi-utils/node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true + }, + "node_modules/@truffle/abi-utils/node_modules/web3-utils": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", + "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", + "dev": true, + "dependencies": { + "bn.js": "^5.2.1", + "ethereum-bloom-filters": "^1.0.6", + "ethereumjs-util": "^7.1.0", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randombytes": "^2.1.0", + "utf8": "3.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@truffle/blockchain-utils": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@truffle/blockchain-utils/-/blockchain-utils-0.1.6.tgz", - "integrity": "sha512-SldoNRIFSm3+HMBnSc2jFsu5TWDkCN4X6vL3wrd0t6DIeF7nD6EoPPjxwbFSoqCnkkRxMuZeL6sUx7UMJS/wSA==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@truffle/blockchain-utils/-/blockchain-utils-0.1.7.tgz", + "integrity": "sha512-1nibqGjEHC7KAyDThEFvbm2+EO8zAHee/VjCtxkYBE3ySwP50joh0QCEBjy7K/9z+icpMoDucfxmgaKToBFUgQ==", "dev": true }, "node_modules/@truffle/codec": { - "version": "0.14.16", - "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.14.16.tgz", - "integrity": "sha512-a9UY3n/FnkKN3Q4zOuMFOOcLWb80mdknj+voim4vvXYtJm1aAZQZE5sG9aLnMBTl4TiGLzUtfNDVYY7WgWgDag==", + "version": "0.14.17", + "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.14.17.tgz", + "integrity": "sha512-kD4dD86huLeaBEq5R8D1zleJEu6NsXbyYLdXl1V1TKdiO8odw5CBC6Y/+wdu5d3t1dyEYrTbhn1dqknZa52pmw==", "dev": true, "dependencies": { "@truffle/abi-utils": "^0.3.9", @@ -2478,6 +2645,24 @@ "node": ">=10" } }, + "node_modules/@truffle/codec/node_modules/web3-utils": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", + "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", + "dev": true, + "dependencies": { + "bn.js": "^5.2.1", + "ethereum-bloom-filters": "^1.0.6", + "ethereumjs-util": "^7.1.0", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randombytes": "^2.1.0", + "utf8": "3.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@truffle/codec/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -2501,17 +2686,17 @@ "dev": true }, "node_modules/@truffle/contract": { - "version": "4.6.17", - "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.17.tgz", - "integrity": "sha512-sIMam5Wqr9AEiqHfOcWGJGqTv8Qy+BT765PaNHUUT6JBAY+tpHM3FlQd2nM6zLJ8paR3SLDGIthkhCBH/KNgDA==", + "version": "4.6.20", + "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.20.tgz", + "integrity": "sha512-s7Mbc37L/CF5Apy/cjPnalkgACmG9tTAmcIW28cIZLRLOUAze18pqhtdHryxAQhEOtKGaDAho6TriqL7/74uHw==", "dev": true, "dependencies": { "@ensdomains/ensjs": "^2.1.0", - "@truffle/blockchain-utils": "^0.1.6", + "@truffle/blockchain-utils": "^0.1.7", "@truffle/contract-schema": "^3.4.13", - "@truffle/debug-utils": "^6.0.47", + "@truffle/debug-utils": "^6.0.48", "@truffle/error": "^0.2.0", - "@truffle/interface-adapter": "^0.5.30", + "@truffle/interface-adapter": "^0.5.32", "bignumber.js": "^7.2.1", "debug": "^4.3.1", "ethers": "^4.0.32", @@ -2538,13 +2723,391 @@ "integrity": "sha512-Fe0/z4WWb7IP2gBnv3l6zqP87Y0kSMs7oiSLakKJq17q3GUunrHSdioKuNspdggxkXIBhEQLhi8C+LJdwmHKWQ==", "dev": true }, - "node_modules/@truffle/debug-utils": { - "version": "6.0.47", - "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.47.tgz", - "integrity": "sha512-bUjdzLPdEKtoUCDzaXkrOoi+PbyAJlMBzGequBK8tirT7xL9bCP2Pd/WxvnmRd7AnfroxGNvXwVXWTItW5SMWQ==", + "node_modules/@truffle/contract/node_modules/eth-lib": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.2.8.tgz", + "integrity": "sha512-ArJ7x1WcWOlSpzdoTBX8vkwlkSQ85CjjifSZtV4co64vWxSV8geWfPI9x4SVYu3DSxnX4yWFVTtGL+j9DUFLNw==", "dev": true, "dependencies": { - "@truffle/codec": "^0.14.16", + "bn.js": "^4.11.6", + "elliptic": "^6.4.0", + "xhr-request-promise": "^0.1.2" + } + }, + "node_modules/@truffle/contract/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@truffle/contract/node_modules/web3": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.8.2.tgz", + "integrity": "sha512-92h0GdEHW9wqDICQQKyG4foZBYi0OQkyg4CRml2F7XBl/NG+fu9o6J19kzfFXzSBoA4DnJXbyRgj/RHZv5LRiw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "web3-bzz": "1.8.2", + "web3-core": "1.8.2", + "web3-eth": "1.8.2", + "web3-eth-personal": "1.8.2", + "web3-net": "1.8.2", + "web3-shh": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-bzz": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.8.2.tgz", + "integrity": "sha512-1EEnxjPnFnvNWw3XeeKuTR8PBxYd0+XWzvaLK7OJC/Go9O8llLGxrxICbKV+8cgIE0sDRBxiYx02X+6OhoAQ9w==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@types/node": "^12.12.6", + "got": "12.1.0", + "swarm-js": "^0.1.40" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-core": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.8.2.tgz", + "integrity": "sha512-DJTVEAYcNqxkqruJE+Rxp3CIv0y5AZMwPHQmOkz/cz+MM75SIzMTc0AUdXzGyTS8xMF8h3YWMQGgGEy8SBf1PQ==", + "dev": true, + "dependencies": { + "@types/bn.js": "^5.1.0", + "@types/node": "^12.12.6", + "bignumber.js": "^9.0.0", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-requestmanager": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-core-helpers": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.8.2.tgz", + "integrity": "sha512-6B1eLlq9JFrfealZBomd1fmlq1o4A09vrCVQSa51ANoib/jllT3atZrRDr0zt1rfI7TSZTZBXdN/aTdeN99DWw==", + "dev": true, + "dependencies": { + "web3-eth-iban": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-core-method": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.8.2.tgz", + "integrity": "sha512-1qnr5mw5wVyULzLOrk4B+ryO3gfGjGd/fx8NR+J2xCGLf1e6OSjxT9vbfuQ3fErk/NjSTWWreieYWLMhaogcRA==", + "dev": true, + "dependencies": { + "@ethersproject/transactions": "^5.6.2", + "web3-core-helpers": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-core-promievent": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.8.2.tgz", + "integrity": "sha512-nvkJWDVgoOSsolJldN33tKW6bKKRJX3MCPDYMwP5SUFOA/mCzDEoI88N0JFofDTXkh1k7gOqp1pvwi9heuaxGg==", + "dev": true, + "dependencies": { + "eventemitter3": "4.0.4" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-core-requestmanager": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.8.2.tgz", + "integrity": "sha512-p1d090RYs5Mu7DK1yyc3GCBVZB/03rBtFhYFoS2EruGzOWs/5Q0grgtpwS/DScdRAm8wB8mYEBhY/RKJWF6B2g==", + "dev": true, + "dependencies": { + "util": "^0.12.5", + "web3-core-helpers": "1.8.2", + "web3-providers-http": "1.8.2", + "web3-providers-ipc": "1.8.2", + "web3-providers-ws": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-core-subscriptions": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.8.2.tgz", + "integrity": "sha512-vXQogHDmAIQcKpXvGiMddBUeP9lnKgYF64+yQJhPNE5PnWr1sAibXuIPV7mIPihpFr/n/DORRj6Wh1pUv9zaTw==", + "dev": true, + "dependencies": { + "eventemitter3": "4.0.4", + "web3-core-helpers": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-core/node_modules/bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/@truffle/contract/node_modules/web3-eth": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.8.2.tgz", + "integrity": "sha512-JoTiWWc4F4TInpbvDUGb0WgDYJsFhuIjJlinc5ByjWD88Gvh+GKLsRjjFdbqe5YtwIGT4NymwoC5LQd1K6u/QQ==", + "dev": true, + "dependencies": { + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-eth-accounts": "1.8.2", + "web3-eth-contract": "1.8.2", + "web3-eth-ens": "1.8.2", + "web3-eth-iban": "1.8.2", + "web3-eth-personal": "1.8.2", + "web3-net": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-eth-abi": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.8.2.tgz", + "integrity": "sha512-Om9g3kaRNjqiNPAgKwGT16y+ZwtBzRe4ZJFGjLiSs6v5I7TPNF+rRMWuKnR6jq0azQZDj6rblvKFMA49/k48Og==", + "dev": true, + "dependencies": { + "@ethersproject/abi": "^5.6.3", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-eth-accounts": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.8.2.tgz", + "integrity": "sha512-c367Ij63VCz9YdyjiHHWLFtN85l6QghgwMQH2B1eM/p9Y5lTlTX7t/Eg/8+f1yoIStXbk2w/PYM2lk+IkbqdLA==", + "dev": true, + "dependencies": { + "@ethereumjs/common": "2.5.0", + "@ethereumjs/tx": "3.3.2", + "eth-lib": "0.2.8", + "ethereumjs-util": "^7.1.5", + "scrypt-js": "^3.0.1", + "uuid": "^9.0.0", + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-eth-contract": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.8.2.tgz", + "integrity": "sha512-ID5A25tHTSBNwOPjiXSVzxruz006ULRIDbzWTYIFTp7NJ7vXu/kynKK2ag/ObuTqBpMbobP8nXcA9b5EDkIdQA==", + "dev": true, + "dependencies": { + "@types/bn.js": "^5.1.0", + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-eth-ens": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.8.2.tgz", + "integrity": "sha512-PWph7C/CnqdWuu1+SH4U4zdrK4t2HNt0I4XzPYFdv9ugE8EuojselioPQXsVGvjql+Nt3jDLvQvggPqlMbvwRw==", + "dev": true, + "dependencies": { + "content-hash": "^2.5.2", + "eth-ens-namehash": "2.0.8", + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-eth-contract": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-eth-iban": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.8.2.tgz", + "integrity": "sha512-h3vNblDWkWMuYx93Q27TAJz6lhzpP93EiC3+45D6xoz983p6si773vntoQ+H+5aZhwglBtoiBzdh7PSSOnP/xQ==", + "dev": true, + "dependencies": { + "bn.js": "^5.2.1", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-eth-iban/node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true + }, + "node_modules/@truffle/contract/node_modules/web3-eth-personal": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.8.2.tgz", + "integrity": "sha512-Vg4HfwCr7doiUF/RC+Jz0wT4+cYaXcOWMAW2AHIjHX6Z7Xwa8nrURIeQgeEE62qcEHAzajyAdB1u6bJyTfuCXw==", + "dev": true, + "dependencies": { + "@types/node": "^12.12.6", + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-net": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-net": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.8.2.tgz", + "integrity": "sha512-1itkDMGmbgb83Dg9nporFes9/fxsU7smJ3oRXlFkg4ZHn8YJyP1MSQFPJWWwSc+GrcCFt4O5IrUTvEkHqE3xag==", + "dev": true, + "dependencies": { + "web3-core": "1.8.2", + "web3-core-method": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-providers-http": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.8.2.tgz", + "integrity": "sha512-2xY94IIEQd16+b+vIBF4IC1p7GVaz9q4EUFscvMUjtEq4ru4Atdzjs9GP+jmcoo49p70II0UV3bqQcz0TQfVyQ==", + "dev": true, + "dependencies": { + "abortcontroller-polyfill": "^1.7.3", + "cross-fetch": "^3.1.4", + "es6-promise": "^4.2.8", + "web3-core-helpers": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-providers-ipc": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.8.2.tgz", + "integrity": "sha512-p6fqKVGFg+WiXGHWnB1hu43PbvPkDHTz4RgoEzbXugv5rtv5zfYLqm8Ba6lrJOS5ks9kGKR21a0y3NzE3u7V4w==", + "dev": true, + "dependencies": { + "oboe": "2.1.5", + "web3-core-helpers": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-providers-ws": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.8.2.tgz", + "integrity": "sha512-3s/4K+wHgbiN+Zrp9YjMq2eqAF6QGABw7wFftPdx+m5hWImV27/MoIx57c6HffNRqZXmCHnfWWFCNHHsi7wXnA==", + "dev": true, + "dependencies": { + "eventemitter3": "4.0.4", + "web3-core-helpers": "1.8.2", + "websocket": "^1.0.32" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-shh": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.8.2.tgz", + "integrity": "sha512-uZ+3MAoNcaJsXXNCDnizKJ5viBNeHOFYsCbFhV755Uu52FswzTOw6DtE7yK9nYXMtIhiSgi7nwl1RYzP8pystw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "web3-core": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-net": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-utils": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", + "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", + "dev": true, + "dependencies": { + "bn.js": "^5.2.1", + "ethereum-bloom-filters": "^1.0.6", + "ethereumjs-util": "^7.1.0", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randombytes": "^2.1.0", + "utf8": "3.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/contract/node_modules/web3-utils/node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true + }, + "node_modules/@truffle/debug-utils": { + "version": "6.0.48", + "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.48.tgz", + "integrity": "sha512-HdK/7eH5EFrcTPeZVEgKaKkkzuZ4xsrH8yw+EoLEsScLsOEuQeKynY61NctjuU93voATWrYmV99Sfb/MRq2i2g==", + "dev": true, + "dependencies": { + "@truffle/codec": "^0.14.17", "@trufflesuite/chromafi": "^3.0.0", "bn.js": "^5.1.3", "chalk": "^2.4.2", @@ -2565,9 +3128,9 @@ "dev": true }, "node_modules/@truffle/interface-adapter": { - "version": "0.5.30", - "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.30.tgz", - "integrity": "sha512-wyCcESeZJBkAfuSGU8GHCusIWDUDyQjJZMcyShv59ZTSAwQR7xx0+a0Q1LlS532G/pGFLYe2VzKSY1pRHRwgug==", + "version": "0.5.32", + "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.32.tgz", + "integrity": "sha512-7Hgmdb4nJUcWtq4vvgWY7Mr2RLOTOp5FZaWyMiFmjkcEEhDlezm2QstALWAXgT0W6q7tDmDBpw3vTIFenRhHBA==", "dev": true, "dependencies": { "bn.js": "^5.1.3", @@ -2575,12 +3138,384 @@ "web3": "1.8.2" } }, + "node_modules/@truffle/interface-adapter/node_modules/bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/@truffle/interface-adapter/node_modules/bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", "dev": true }, + "node_modules/@truffle/interface-adapter/node_modules/eth-lib": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.2.8.tgz", + "integrity": "sha512-ArJ7x1WcWOlSpzdoTBX8vkwlkSQ85CjjifSZtV4co64vWxSV8geWfPI9x4SVYu3DSxnX4yWFVTtGL+j9DUFLNw==", + "dev": true, + "dependencies": { + "bn.js": "^4.11.6", + "elliptic": "^6.4.0", + "xhr-request-promise": "^0.1.2" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/eth-lib/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/@truffle/interface-adapter/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.8.2.tgz", + "integrity": "sha512-92h0GdEHW9wqDICQQKyG4foZBYi0OQkyg4CRml2F7XBl/NG+fu9o6J19kzfFXzSBoA4DnJXbyRgj/RHZv5LRiw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "web3-bzz": "1.8.2", + "web3-core": "1.8.2", + "web3-eth": "1.8.2", + "web3-eth-personal": "1.8.2", + "web3-net": "1.8.2", + "web3-shh": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-bzz": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.8.2.tgz", + "integrity": "sha512-1EEnxjPnFnvNWw3XeeKuTR8PBxYd0+XWzvaLK7OJC/Go9O8llLGxrxICbKV+8cgIE0sDRBxiYx02X+6OhoAQ9w==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@types/node": "^12.12.6", + "got": "12.1.0", + "swarm-js": "^0.1.40" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-core": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.8.2.tgz", + "integrity": "sha512-DJTVEAYcNqxkqruJE+Rxp3CIv0y5AZMwPHQmOkz/cz+MM75SIzMTc0AUdXzGyTS8xMF8h3YWMQGgGEy8SBf1PQ==", + "dev": true, + "dependencies": { + "@types/bn.js": "^5.1.0", + "@types/node": "^12.12.6", + "bignumber.js": "^9.0.0", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-requestmanager": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-core-helpers": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.8.2.tgz", + "integrity": "sha512-6B1eLlq9JFrfealZBomd1fmlq1o4A09vrCVQSa51ANoib/jllT3atZrRDr0zt1rfI7TSZTZBXdN/aTdeN99DWw==", + "dev": true, + "dependencies": { + "web3-eth-iban": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-core-method": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.8.2.tgz", + "integrity": "sha512-1qnr5mw5wVyULzLOrk4B+ryO3gfGjGd/fx8NR+J2xCGLf1e6OSjxT9vbfuQ3fErk/NjSTWWreieYWLMhaogcRA==", + "dev": true, + "dependencies": { + "@ethersproject/transactions": "^5.6.2", + "web3-core-helpers": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-core-promievent": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.8.2.tgz", + "integrity": "sha512-nvkJWDVgoOSsolJldN33tKW6bKKRJX3MCPDYMwP5SUFOA/mCzDEoI88N0JFofDTXkh1k7gOqp1pvwi9heuaxGg==", + "dev": true, + "dependencies": { + "eventemitter3": "4.0.4" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-core-requestmanager": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.8.2.tgz", + "integrity": "sha512-p1d090RYs5Mu7DK1yyc3GCBVZB/03rBtFhYFoS2EruGzOWs/5Q0grgtpwS/DScdRAm8wB8mYEBhY/RKJWF6B2g==", + "dev": true, + "dependencies": { + "util": "^0.12.5", + "web3-core-helpers": "1.8.2", + "web3-providers-http": "1.8.2", + "web3-providers-ipc": "1.8.2", + "web3-providers-ws": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-core-subscriptions": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.8.2.tgz", + "integrity": "sha512-vXQogHDmAIQcKpXvGiMddBUeP9lnKgYF64+yQJhPNE5PnWr1sAibXuIPV7mIPihpFr/n/DORRj6Wh1pUv9zaTw==", + "dev": true, + "dependencies": { + "eventemitter3": "4.0.4", + "web3-core-helpers": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-eth": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.8.2.tgz", + "integrity": "sha512-JoTiWWc4F4TInpbvDUGb0WgDYJsFhuIjJlinc5ByjWD88Gvh+GKLsRjjFdbqe5YtwIGT4NymwoC5LQd1K6u/QQ==", + "dev": true, + "dependencies": { + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-eth-accounts": "1.8.2", + "web3-eth-contract": "1.8.2", + "web3-eth-ens": "1.8.2", + "web3-eth-iban": "1.8.2", + "web3-eth-personal": "1.8.2", + "web3-net": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-eth-abi": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.8.2.tgz", + "integrity": "sha512-Om9g3kaRNjqiNPAgKwGT16y+ZwtBzRe4ZJFGjLiSs6v5I7TPNF+rRMWuKnR6jq0azQZDj6rblvKFMA49/k48Og==", + "dev": true, + "dependencies": { + "@ethersproject/abi": "^5.6.3", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-eth-accounts": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.8.2.tgz", + "integrity": "sha512-c367Ij63VCz9YdyjiHHWLFtN85l6QghgwMQH2B1eM/p9Y5lTlTX7t/Eg/8+f1yoIStXbk2w/PYM2lk+IkbqdLA==", + "dev": true, + "dependencies": { + "@ethereumjs/common": "2.5.0", + "@ethereumjs/tx": "3.3.2", + "eth-lib": "0.2.8", + "ethereumjs-util": "^7.1.5", + "scrypt-js": "^3.0.1", + "uuid": "^9.0.0", + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-eth-contract": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.8.2.tgz", + "integrity": "sha512-ID5A25tHTSBNwOPjiXSVzxruz006ULRIDbzWTYIFTp7NJ7vXu/kynKK2ag/ObuTqBpMbobP8nXcA9b5EDkIdQA==", + "dev": true, + "dependencies": { + "@types/bn.js": "^5.1.0", + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-eth-ens": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.8.2.tgz", + "integrity": "sha512-PWph7C/CnqdWuu1+SH4U4zdrK4t2HNt0I4XzPYFdv9ugE8EuojselioPQXsVGvjql+Nt3jDLvQvggPqlMbvwRw==", + "dev": true, + "dependencies": { + "content-hash": "^2.5.2", + "eth-ens-namehash": "2.0.8", + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-eth-contract": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-eth-iban": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.8.2.tgz", + "integrity": "sha512-h3vNblDWkWMuYx93Q27TAJz6lhzpP93EiC3+45D6xoz983p6si773vntoQ+H+5aZhwglBtoiBzdh7PSSOnP/xQ==", + "dev": true, + "dependencies": { + "bn.js": "^5.2.1", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-eth-personal": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.8.2.tgz", + "integrity": "sha512-Vg4HfwCr7doiUF/RC+Jz0wT4+cYaXcOWMAW2AHIjHX6Z7Xwa8nrURIeQgeEE62qcEHAzajyAdB1u6bJyTfuCXw==", + "dev": true, + "dependencies": { + "@types/node": "^12.12.6", + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-net": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-net": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.8.2.tgz", + "integrity": "sha512-1itkDMGmbgb83Dg9nporFes9/fxsU7smJ3oRXlFkg4ZHn8YJyP1MSQFPJWWwSc+GrcCFt4O5IrUTvEkHqE3xag==", + "dev": true, + "dependencies": { + "web3-core": "1.8.2", + "web3-core-method": "1.8.2", + "web3-utils": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-providers-http": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.8.2.tgz", + "integrity": "sha512-2xY94IIEQd16+b+vIBF4IC1p7GVaz9q4EUFscvMUjtEq4ru4Atdzjs9GP+jmcoo49p70II0UV3bqQcz0TQfVyQ==", + "dev": true, + "dependencies": { + "abortcontroller-polyfill": "^1.7.3", + "cross-fetch": "^3.1.4", + "es6-promise": "^4.2.8", + "web3-core-helpers": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-providers-ipc": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.8.2.tgz", + "integrity": "sha512-p6fqKVGFg+WiXGHWnB1hu43PbvPkDHTz4RgoEzbXugv5rtv5zfYLqm8Ba6lrJOS5ks9kGKR21a0y3NzE3u7V4w==", + "dev": true, + "dependencies": { + "oboe": "2.1.5", + "web3-core-helpers": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-providers-ws": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.8.2.tgz", + "integrity": "sha512-3s/4K+wHgbiN+Zrp9YjMq2eqAF6QGABw7wFftPdx+m5hWImV27/MoIx57c6HffNRqZXmCHnfWWFCNHHsi7wXnA==", + "dev": true, + "dependencies": { + "eventemitter3": "4.0.4", + "web3-core-helpers": "1.8.2", + "websocket": "^1.0.32" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-shh": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.8.2.tgz", + "integrity": "sha512-uZ+3MAoNcaJsXXNCDnizKJ5viBNeHOFYsCbFhV755Uu52FswzTOw6DtE7yK9nYXMtIhiSgi7nwl1RYzP8pystw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "web3-core": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-net": "1.8.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@truffle/interface-adapter/node_modules/web3-utils": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", + "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", + "dev": true, + "dependencies": { + "bn.js": "^5.2.1", + "ethereum-bloom-filters": "^1.0.6", + "ethereumjs-util": "^7.1.0", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randombytes": "^2.1.0", + "utf8": "3.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@trufflesuite/chromafi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@trufflesuite/chromafi/-/chromafi-3.0.0.tgz", @@ -2606,12 +3541,6 @@ "node": ">=4" } }, - "node_modules/@types/async-eventemitter": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@types/async-eventemitter/-/async-eventemitter-0.2.1.tgz", - "integrity": "sha512-M2P4Ng26QbAeITiH7w1d7OxtldgfAe0wobpyJzVK/XOb0cUGKU2R4pfAhqcJBXAe2ife5ZOhSv4wk7p+ffURtg==", - "dev": true - }, "node_modules/@types/bignumber.js": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/bignumber.js/-/bignumber.js-5.0.0.tgz", @@ -2644,9 +3573,9 @@ } }, "node_modules/@types/chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", "dev": true }, "node_modules/@types/concat-stream": { @@ -2746,6 +3675,22 @@ "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", "dev": true }, + "node_modules/@types/readable-stream": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.15.tgz", + "integrity": "sha512-oM5JSKQCcICF1wvGgmecmHldZ48OZamtMxcGGVICOJA8o8cahXC1zEVAif8iwoc5j8etxFaRFnf095+CDsuoFQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, + "node_modules/@types/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -3171,22 +4116,10 @@ } }, "node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "dependencies": { - "lodash": "^4.17.14" - } - }, - "node_modules/async-eventemitter": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/async-eventemitter/-/async-eventemitter-0.2.4.tgz", - "integrity": "sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw==", - "dev": true, - "dependencies": { - "async": "^2.4.0" - } + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "dev": true }, "node_modules/async-limiter": { "version": "1.0.1", @@ -3318,24 +4251,12 @@ } }, "node_modules/bigint-crypto-utils": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/bigint-crypto-utils/-/bigint-crypto-utils-3.1.8.tgz", - "integrity": "sha512-+VMV9Laq8pXLBKKKK49nOoq9bfR3j7NNQAtbA617a4nw9bVLo8rsqkKMBgM2AJWlNX9fEIyYaYX+d0laqYV4tw==", - "dev": true, - "dependencies": { - "bigint-mod-arith": "^3.1.0" - }, - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/bigint-mod-arith": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bigint-mod-arith/-/bigint-mod-arith-3.1.2.tgz", - "integrity": "sha512-nx8J8bBeiRR+NlsROFH9jHswW5HO8mgfOSqW0AmjicMMvaONDa8AO+5ViKDUUNytBPWiwfvZP4/Bj4Y3lUfvgQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/bigint-crypto-utils/-/bigint-crypto-utils-3.2.2.tgz", + "integrity": "sha512-U1RbE3aX9ayCUVcIPHuPDPKcK3SFOXf93J1UK/iHlJuQB7bhagPIX06/CLpLEsDThJ7KA4Dhrnzynl+d2weTiw==", "dev": true, "engines": { - "node": ">=10.4.0" + "node": ">=14.0.0" } }, "node_modules/bignumber.js": { @@ -3723,6 +4644,15 @@ "node": ">=6" } }, + "node_modules/case": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/case/-/case-1.6.3.tgz", + "integrity": "sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -3974,16 +4904,16 @@ "dev": true }, "node_modules/classic-level": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.2.0.tgz", - "integrity": "sha512-qw5B31ANxSluWz9xBzklRWTUAJ1SXIdaVKTVS7HcTGKOAmExx65Wo5BUICW+YGORe2FOUaDghoI9ZDxj82QcFg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.3.0.tgz", + "integrity": "sha512-iwFAJQYtqRTRM0F6L8h4JCt00ZSGdOyqh7yVrhhjrOpFhmBjNlRUey64MCiyo6UmQHMJ+No3c81nujPv+n9yrg==", "dev": true, "hasInstallScript": true, "dependencies": { "abstract-level": "^1.0.2", "catering": "^2.1.0", "module-error": "^1.0.1", - "napi-macros": "~2.0.0", + "napi-macros": "^2.2.2", "node-gyp-build": "^4.3.0" }, "engines": { @@ -4146,9 +5076,9 @@ "dev": true }, "node_modules/commander": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz", - "integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, "engines": { "node": ">=14" @@ -4288,9 +5218,9 @@ } }, "node_modules/cosmiconfig": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.0.tgz", - "integrity": "sha512-0tLZ9URlPGU7JsKq0DQOQ3FoRsYX8xDZ7xMiATQfaiGMz7EHowNkbU9u1coAOmnh9p/1ySpm0RB3JNWRXM5GCg==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", + "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", "dev": true, "dependencies": { "import-fresh": "^3.2.1", @@ -4816,14 +5746,14 @@ } }, "node_modules/domutils": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", - "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dev": true, "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", - "domhandler": "^5.0.1" + "domhandler": "^5.0.3" }, "funding": { "url": "https://github.com/fb55/domutils?sponsor=1" @@ -4915,9 +5845,9 @@ } }, "node_modules/entities": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "engines": { "node": ">=0.12" @@ -5200,15 +6130,15 @@ } }, "node_modules/eslint": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.36.0.tgz", - "integrity": "sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", + "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.1", - "@eslint/js": "8.36.0", + "@eslint/eslintrc": "^2.0.2", + "@eslint/js": "8.39.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -5218,9 +6148,9 @@ "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.5.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.0", + "espree": "^9.5.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -5257,9 +6187,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.7.0.tgz", - "integrity": "sha512-HHVXLSlVUhMSmyW4ZzEuvjpwqamgmlfkutD53cYXLikh4pt/modINRcCIApJ84czDxM4GZInwUrromsDdTImTA==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" @@ -5269,9 +6199,9 @@ } }, "node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -5279,15 +6209,21 @@ }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", + "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/ansi-regex": { @@ -5470,14 +6406,14 @@ } }, "node_modules/espree": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.0.tgz", - "integrity": "sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", + "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", "dev": true, "dependencies": { "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -7165,9 +8101,9 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, "node_modules/grapheme-splitter": { @@ -7257,23 +8193,23 @@ } }, "node_modules/hardhat": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.13.0.tgz", - "integrity": "sha512-ZlzBOLML1QGlm6JWyVAG8lVTEAoOaVm1in/RU2zoGAnYEoD1Rp4T+ZMvrLNhHaaeS9hfjJ1gJUBfiDr4cx+htQ==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.14.0.tgz", + "integrity": "sha512-73jsInY4zZahMSVFurSK+5TNCJTXMv+vemvGia0Ac34Mm19fYp6vEPVGF3sucbumszsYxiTT2TbS8Ii2dsDSoQ==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.1.2", "@metamask/eth-sig-util": "^4.0.0", - "@nomicfoundation/ethereumjs-block": "^4.0.0", - "@nomicfoundation/ethereumjs-blockchain": "^6.0.0", - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-evm": "^1.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-statemanager": "^1.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-tx": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", - "@nomicfoundation/ethereumjs-vm": "^6.0.0", + "@nomicfoundation/ethereumjs-block": "5.0.1", + "@nomicfoundation/ethereumjs-blockchain": "7.0.1", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-evm": "2.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-statemanager": "2.0.1", + "@nomicfoundation/ethereumjs-trie": "6.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", + "@nomicfoundation/ethereumjs-vm": "7.0.1", "@nomicfoundation/solidity-analyzer": "^0.1.0", "@sentry/node": "^5.18.1", "@types/bn.js": "^5.1.0", @@ -7749,9 +8685,9 @@ "dev": true }, "node_modules/htmlparser2": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", - "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -7762,9 +8698,9 @@ ], "dependencies": { "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", + "domhandler": "^5.0.3", "domutils": "^3.0.1", - "entities": "^4.3.0" + "entities": "^4.4.0" } }, "node_modules/http-basic": { @@ -8491,9 +9427,9 @@ "dev": true }, "node_modules/js-sdsl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", - "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", + "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", "dev": true, "funding": { "type": "opencollective", @@ -9337,9 +10273,9 @@ } }, "node_modules/mixme": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.5.tgz", - "integrity": "sha512-/6IupbRx32s7jjEwHcycXikJwFD5UujbVNuJFkeKLYje+92OvtuPniF6JhnFm5JCTDUhS+kYK3W/4BWYQYXz7w==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.9.tgz", + "integrity": "sha512-VC5fg6ySUscaWUpI4gxCBTQMH2RdUpNrk+MsbpCYtIvf9SBJdiUey4qE7BXviJsJR4nDQxCZ+3yaYNW3guz/Pw==", "dev": true, "engines": { "node": ">= 8.0.0" @@ -9758,9 +10694,9 @@ } }, "node_modules/napi-macros": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", - "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.2.2.tgz", + "integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==", "dev": true }, "node_modules/natural-compare": { @@ -10035,15 +10971,16 @@ } }, "node_modules/object.getownpropertydescriptors": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz", - "integrity": "sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz", + "integrity": "sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ==", "dev": true, "dependencies": { "array.prototype.reduce": "^1.0.5", "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.21.2", + "safe-array-concat": "^1.0.0" }, "engines": { "node": ">= 0.8" @@ -10551,9 +11488,9 @@ } }, "node_modules/prettier": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", - "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, "bin": { "prettier": "bin-prettier.js" @@ -10924,14 +11861,14 @@ "dev": true }, "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -11272,6 +12209,30 @@ "integrity": "sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==", "dev": true }, + "node_modules/safe-array-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", + "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -11337,12 +12298,6 @@ "istanbul": "lib/cli.js" } }, - "node_modules/sc-istanbul/node_modules/async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", - "dev": true - }, "node_modules/sc-istanbul/node_modules/esprima": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", @@ -11433,9 +12388,9 @@ } }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -13963,18 +14918,18 @@ "dev": true }, "node_modules/tty-table": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.1.6.tgz", - "integrity": "sha512-kRj5CBzOrakV4VRRY5kUWbNYvo/FpOsz65DzI5op9P+cHov3+IqPbo1JE1ZnQGkHdZgNFDsrEjrfqqy/Ply9fw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.2.1.tgz", + "integrity": "sha512-xz0uKo+KakCQ+Dxj1D/tKn2FSyreSYWzdkL/BYhgN6oMW808g8QRMuh1atAV9fjTPbWBjfbkKQpI/5rEcnAc7g==", "dev": true, "dependencies": { "chalk": "^4.1.2", - "csv": "^5.5.0", - "kleur": "^4.1.4", + "csv": "^5.5.3", + "kleur": "^4.1.5", "smartwrap": "^2.0.2", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wcwidth": "^1.0.1", - "yargs": "^17.1.1" + "yargs": "^17.7.1" }, "bin": { "tty-table": "adapters/terminal-adapter.js" @@ -14220,15 +15175,15 @@ "dev": true }, "node_modules/undici": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.21.0.tgz", - "integrity": "sha512-HOjK8l6a57b2ZGXOcUsI5NLfoTrfmbOl90ixJDl0AEFG4wgHNDQxtZy15/ZQp7HhjkpaGlp/eneMgtsu1dIlUA==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.0.tgz", + "integrity": "sha512-fR9RXCc+6Dxav4P9VV/sp5w3eFiSdOjJYsbtWfd4s5L5C4ogyuVpdKIVHeW0vV1MloM65/f7W45nR9ZxwVdyiA==", "dev": true, "dependencies": { "busboy": "^1.6.0" }, "engines": { - "node": ">=12.18" + "node": ">=14.0" } }, "node_modules/universalify": { @@ -14384,28 +15339,28 @@ } }, "node_modules/web3": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.8.2.tgz", - "integrity": "sha512-92h0GdEHW9wqDICQQKyG4foZBYi0OQkyg4CRml2F7XBl/NG+fu9o6J19kzfFXzSBoA4DnJXbyRgj/RHZv5LRiw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.9.0.tgz", + "integrity": "sha512-E9IvVy/d2ozfQQsCiV+zh/LmlZGv9fQxI0UedDVjm87yOKf4AYbBNEn1iWtHveiGzAk2CEMZMUzAZzaQNSSYog==", "dev": true, "hasInstallScript": true, "dependencies": { - "web3-bzz": "1.8.2", - "web3-core": "1.8.2", - "web3-eth": "1.8.2", - "web3-eth-personal": "1.8.2", - "web3-net": "1.8.2", - "web3-shh": "1.8.2", - "web3-utils": "1.8.2" + "web3-bzz": "1.9.0", + "web3-core": "1.9.0", + "web3-eth": "1.9.0", + "web3-eth-personal": "1.9.0", + "web3-net": "1.9.0", + "web3-shh": "1.9.0", + "web3-utils": "1.9.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-bzz": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.8.2.tgz", - "integrity": "sha512-1EEnxjPnFnvNWw3XeeKuTR8PBxYd0+XWzvaLK7OJC/Go9O8llLGxrxICbKV+8cgIE0sDRBxiYx02X+6OhoAQ9w==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.9.0.tgz", + "integrity": "sha512-9Zli9dikX8GdHwBb5/WPzpSVuy3EWMKY3P4EokCQra31fD7DLizqAAaTUsFwnK7xYkw5ogpHgelw9uKHHzNajg==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -14418,56 +15373,56 @@ } }, "node_modules/web3-core": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.8.2.tgz", - "integrity": "sha512-DJTVEAYcNqxkqruJE+Rxp3CIv0y5AZMwPHQmOkz/cz+MM75SIzMTc0AUdXzGyTS8xMF8h3YWMQGgGEy8SBf1PQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.9.0.tgz", + "integrity": "sha512-DZ+TPmq/ZLlx4LSVzFgrHCP/QFpKDbGWO4HoquZSdu24cjk5SZ+FEU1SZB2OaK3/bgBh+25mRbmv8y56ysUu1w==", "dev": true, "dependencies": { - "@types/bn.js": "^5.1.0", + "@types/bn.js": "^5.1.1", "@types/node": "^12.12.6", "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-requestmanager": "1.8.2", - "web3-utils": "1.8.2" + "web3-core-helpers": "1.9.0", + "web3-core-method": "1.9.0", + "web3-core-requestmanager": "1.9.0", + "web3-utils": "1.9.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-helpers": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.8.2.tgz", - "integrity": "sha512-6B1eLlq9JFrfealZBomd1fmlq1o4A09vrCVQSa51ANoib/jllT3atZrRDr0zt1rfI7TSZTZBXdN/aTdeN99DWw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.9.0.tgz", + "integrity": "sha512-NeJzylAp9Yj9xAt2uTT+kyug3X0DLnfBdnAcGZuY6HhoNPDIfQRA9CkJjLngVRlGTLZGjNp9x9eR+RyZQgUlXg==", "dev": true, "dependencies": { - "web3-eth-iban": "1.8.2", - "web3-utils": "1.8.2" + "web3-eth-iban": "1.9.0", + "web3-utils": "1.9.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-method": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.8.2.tgz", - "integrity": "sha512-1qnr5mw5wVyULzLOrk4B+ryO3gfGjGd/fx8NR+J2xCGLf1e6OSjxT9vbfuQ3fErk/NjSTWWreieYWLMhaogcRA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.9.0.tgz", + "integrity": "sha512-sswbNsY2xRBBhGeaLt9c/eDc+0yDDhi6keUBAkgIRa9ueSx/VKzUY9HMqiV6bXDcGT2fJyejq74FfEB4lc/+/w==", "dev": true, "dependencies": { "@ethersproject/transactions": "^5.6.2", - "web3-core-helpers": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-utils": "1.8.2" + "web3-core-helpers": "1.9.0", + "web3-core-promievent": "1.9.0", + "web3-core-subscriptions": "1.9.0", + "web3-utils": "1.9.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-promievent": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.8.2.tgz", - "integrity": "sha512-nvkJWDVgoOSsolJldN33tKW6bKKRJX3MCPDYMwP5SUFOA/mCzDEoI88N0JFofDTXkh1k7gOqp1pvwi9heuaxGg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.9.0.tgz", + "integrity": "sha512-PHG1Mn23IGwMZhnPDN8dETKypqsFbHfiyRqP+XsVMPmTHkVfzDQTCBU/c2r6hUktBDoGKut5xZQpGfhFk71KbQ==", "dev": true, "dependencies": { "eventemitter3": "4.0.4" @@ -14477,29 +15432,29 @@ } }, "node_modules/web3-core-requestmanager": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.8.2.tgz", - "integrity": "sha512-p1d090RYs5Mu7DK1yyc3GCBVZB/03rBtFhYFoS2EruGzOWs/5Q0grgtpwS/DScdRAm8wB8mYEBhY/RKJWF6B2g==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.9.0.tgz", + "integrity": "sha512-hcJ5PCtTIJpj+8qWxoseqlCovDo94JJjTX7dZOLXgwp8ah7E3WRYozhGyZocerx+KebKyg1mCQIhkDpMwjfo9Q==", "dev": true, "dependencies": { "util": "^0.12.5", - "web3-core-helpers": "1.8.2", - "web3-providers-http": "1.8.2", - "web3-providers-ipc": "1.8.2", - "web3-providers-ws": "1.8.2" + "web3-core-helpers": "1.9.0", + "web3-providers-http": "1.9.0", + "web3-providers-ipc": "1.9.0", + "web3-providers-ws": "1.9.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-subscriptions": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.8.2.tgz", - "integrity": "sha512-vXQogHDmAIQcKpXvGiMddBUeP9lnKgYF64+yQJhPNE5PnWr1sAibXuIPV7mIPihpFr/n/DORRj6Wh1pUv9zaTw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.9.0.tgz", + "integrity": "sha512-MaIo29yz7hTV8X8bioclPDbHFOVuHmnbMv+D3PDH12ceJFJAXGyW8GL5KU1DYyWIj4TD1HM4WknyVA/YWBiiLA==", "dev": true, "dependencies": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.2" + "web3-core-helpers": "1.9.0" }, "engines": { "node": ">=8.0.0" @@ -14515,45 +15470,45 @@ } }, "node_modules/web3-eth": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.8.2.tgz", - "integrity": "sha512-JoTiWWc4F4TInpbvDUGb0WgDYJsFhuIjJlinc5ByjWD88Gvh+GKLsRjjFdbqe5YtwIGT4NymwoC5LQd1K6u/QQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.9.0.tgz", + "integrity": "sha512-c5gSWk9bLNr6VPATHmZ1n7LTIefIZQnJMzfnvkoBcIFGKJbGmsuRhv6lEXsKdAO/FlqYnSbaw3fOq1fVFiIOFQ==", "dev": true, "dependencies": { - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-eth-accounts": "1.8.2", - "web3-eth-contract": "1.8.2", - "web3-eth-ens": "1.8.2", - "web3-eth-iban": "1.8.2", - "web3-eth-personal": "1.8.2", - "web3-net": "1.8.2", - "web3-utils": "1.8.2" + "web3-core": "1.9.0", + "web3-core-helpers": "1.9.0", + "web3-core-method": "1.9.0", + "web3-core-subscriptions": "1.9.0", + "web3-eth-abi": "1.9.0", + "web3-eth-accounts": "1.9.0", + "web3-eth-contract": "1.9.0", + "web3-eth-ens": "1.9.0", + "web3-eth-iban": "1.9.0", + "web3-eth-personal": "1.9.0", + "web3-net": "1.9.0", + "web3-utils": "1.9.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-abi": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.8.2.tgz", - "integrity": "sha512-Om9g3kaRNjqiNPAgKwGT16y+ZwtBzRe4ZJFGjLiSs6v5I7TPNF+rRMWuKnR6jq0azQZDj6rblvKFMA49/k48Og==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.9.0.tgz", + "integrity": "sha512-0BLQ3FKMrzJkA930jOX3fMaybAyubk06HChclLpiR0NWmgWXm1tmBrJdkyRy2ZTZpmfuZc9xTFRfl0yZID1voA==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.6.3", - "web3-utils": "1.8.2" + "web3-utils": "1.9.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-accounts": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.8.2.tgz", - "integrity": "sha512-c367Ij63VCz9YdyjiHHWLFtN85l6QghgwMQH2B1eM/p9Y5lTlTX7t/Eg/8+f1yoIStXbk2w/PYM2lk+IkbqdLA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.9.0.tgz", + "integrity": "sha512-VeIZVevmnSll0AC1k5F/y398ZE89d1SRuYk8IewLUhL/tVAsFEsjl2SGgm0+aDcHmgPrkW+qsCJ+C7rWg/N4ZA==", "dev": true, "dependencies": { "@ethereumjs/common": "2.5.0", @@ -14562,10 +15517,10 @@ "ethereumjs-util": "^7.1.5", "scrypt-js": "^3.0.1", "uuid": "^9.0.0", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-utils": "1.8.2" + "web3-core": "1.9.0", + "web3-core-helpers": "1.9.0", + "web3-core-method": "1.9.0", + "web3-utils": "1.9.0" }, "engines": { "node": ">=8.0.0" @@ -14592,51 +15547,51 @@ } }, "node_modules/web3-eth-contract": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.8.2.tgz", - "integrity": "sha512-ID5A25tHTSBNwOPjiXSVzxruz006ULRIDbzWTYIFTp7NJ7vXu/kynKK2ag/ObuTqBpMbobP8nXcA9b5EDkIdQA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.9.0.tgz", + "integrity": "sha512-+j26hpSaEtAdUed0TN5rnc+YZOcjPxMjFX4ZBKatvFkImdbVv/tzTvcHlltubSpgb2ZLyZ89lSL6phKYwd2zNQ==", "dev": true, "dependencies": { - "@types/bn.js": "^5.1.0", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-utils": "1.8.2" + "@types/bn.js": "^5.1.1", + "web3-core": "1.9.0", + "web3-core-helpers": "1.9.0", + "web3-core-method": "1.9.0", + "web3-core-promievent": "1.9.0", + "web3-core-subscriptions": "1.9.0", + "web3-eth-abi": "1.9.0", + "web3-utils": "1.9.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-ens": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.8.2.tgz", - "integrity": "sha512-PWph7C/CnqdWuu1+SH4U4zdrK4t2HNt0I4XzPYFdv9ugE8EuojselioPQXsVGvjql+Nt3jDLvQvggPqlMbvwRw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.9.0.tgz", + "integrity": "sha512-LOJZeN+AGe9arhuExnrPPFYQr4WSxXEkpvYIlst/joOEUNLDwfndHnJIK6PI5mXaYSROBtTx6erv+HupzGo7vA==", "dev": true, "dependencies": { "content-hash": "^2.5.2", "eth-ens-namehash": "2.0.8", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-eth-contract": "1.8.2", - "web3-utils": "1.8.2" + "web3-core": "1.9.0", + "web3-core-helpers": "1.9.0", + "web3-core-promievent": "1.9.0", + "web3-eth-abi": "1.9.0", + "web3-eth-contract": "1.9.0", + "web3-utils": "1.9.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-iban": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.8.2.tgz", - "integrity": "sha512-h3vNblDWkWMuYx93Q27TAJz6lhzpP93EiC3+45D6xoz983p6si773vntoQ+H+5aZhwglBtoiBzdh7PSSOnP/xQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.9.0.tgz", + "integrity": "sha512-jPAm77PuEs1kE/UrrBFJdPD2PN42pwfXA0gFuuw35bZezhskYML9W4QCxcqnUtceyEA4FUn7K2qTMuCk+23fog==", "dev": true, "dependencies": { "bn.js": "^5.2.1", - "web3-utils": "1.8.2" + "web3-utils": "1.9.0" }, "engines": { "node": ">=8.0.0" @@ -14649,72 +15604,72 @@ "dev": true }, "node_modules/web3-eth-personal": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.8.2.tgz", - "integrity": "sha512-Vg4HfwCr7doiUF/RC+Jz0wT4+cYaXcOWMAW2AHIjHX6Z7Xwa8nrURIeQgeEE62qcEHAzajyAdB1u6bJyTfuCXw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.9.0.tgz", + "integrity": "sha512-r9Ldo/luBqJlv1vCUEQnUS+C3a3ZdbYxVHyfDkj6RWMyCqqo8JE41HWE+pfa0RmB1xnGL2g8TbYcHcqItck/qg==", "dev": true, "dependencies": { "@types/node": "^12.12.6", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-net": "1.8.2", - "web3-utils": "1.8.2" + "web3-core": "1.9.0", + "web3-core-helpers": "1.9.0", + "web3-core-method": "1.9.0", + "web3-net": "1.9.0", + "web3-utils": "1.9.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-net": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.8.2.tgz", - "integrity": "sha512-1itkDMGmbgb83Dg9nporFes9/fxsU7smJ3oRXlFkg4ZHn8YJyP1MSQFPJWWwSc+GrcCFt4O5IrUTvEkHqE3xag==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.9.0.tgz", + "integrity": "sha512-L+fDZFgrLM5Y15aonl2q6L+RvfaImAngmC0Jv45hV2FJ5IfRT0/2ob9etxZmvEBWvOpbqSvghfOhJIT3XZ37Pg==", "dev": true, "dependencies": { - "web3-core": "1.8.2", - "web3-core-method": "1.8.2", - "web3-utils": "1.8.2" + "web3-core": "1.9.0", + "web3-core-method": "1.9.0", + "web3-utils": "1.9.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-providers-http": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.8.2.tgz", - "integrity": "sha512-2xY94IIEQd16+b+vIBF4IC1p7GVaz9q4EUFscvMUjtEq4ru4Atdzjs9GP+jmcoo49p70II0UV3bqQcz0TQfVyQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.9.0.tgz", + "integrity": "sha512-5+dMNDAE0rRFz6SJpfnBqlVi2J5bB/Ivr2SanMt2YUrkxW5t8betZbzVwRkTbwtUvkqgj3xeUQzqpOttiv+IqQ==", "dev": true, "dependencies": { "abortcontroller-polyfill": "^1.7.3", "cross-fetch": "^3.1.4", "es6-promise": "^4.2.8", - "web3-core-helpers": "1.8.2" + "web3-core-helpers": "1.9.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-providers-ipc": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.8.2.tgz", - "integrity": "sha512-p6fqKVGFg+WiXGHWnB1hu43PbvPkDHTz4RgoEzbXugv5rtv5zfYLqm8Ba6lrJOS5ks9kGKR21a0y3NzE3u7V4w==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.9.0.tgz", + "integrity": "sha512-cPXU93Du40HCylvjaa5x62DbnGqH+86HpK/+kMcFIzF6sDUBhKpag2tSbYhGbj7GMpfkmDTUiiMLdWnFV6+uBA==", "dev": true, "dependencies": { "oboe": "2.1.5", - "web3-core-helpers": "1.8.2" + "web3-core-helpers": "1.9.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-providers-ws": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.8.2.tgz", - "integrity": "sha512-3s/4K+wHgbiN+Zrp9YjMq2eqAF6QGABw7wFftPdx+m5hWImV27/MoIx57c6HffNRqZXmCHnfWWFCNHHsi7wXnA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.9.0.tgz", + "integrity": "sha512-JRVsnQZ7j2k1a2yzBNHe39xqk1ijOv01dfIBFw52VeEkSRzvrOcsPIM/ttSyBuJqt70ntMxXY0ekCrqfleKH/w==", "dev": true, "dependencies": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.2", + "web3-core-helpers": "1.9.0", "websocket": "^1.0.32" }, "engines": { @@ -14722,25 +15677,25 @@ } }, "node_modules/web3-shh": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.8.2.tgz", - "integrity": "sha512-uZ+3MAoNcaJsXXNCDnizKJ5viBNeHOFYsCbFhV755Uu52FswzTOw6DtE7yK9nYXMtIhiSgi7nwl1RYzP8pystw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.9.0.tgz", + "integrity": "sha512-bIBZlralgz4ICCrwkefB2nPPJWfx28NuHIpjB7d9ADKynElubQuqudYhKtSEkKXACuME/BJm0pIFJcJs/gDnMg==", "dev": true, "hasInstallScript": true, "dependencies": { - "web3-core": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-net": "1.8.2" + "web3-core": "1.9.0", + "web3-core-method": "1.9.0", + "web3-core-subscriptions": "1.9.0", + "web3-net": "1.9.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-utils": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", - "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.9.0.tgz", + "integrity": "sha512-p++69rCNNfu2jM9n5+VD/g26l+qkEOQ1m6cfRQCbH8ZRrtquTmrirJMgTmyOoax5a5XRYOuws14aypCOs51pdQ==", "dev": true, "dependencies": { "bn.js": "^5.2.1", @@ -14841,9 +15796,9 @@ } }, "node_modules/which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "dev": true }, "node_modules/which-pm": { @@ -15121,9 +16076,9 @@ "dev": true }, "node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "dependencies": { "cliui": "^8.0.1", @@ -15276,9 +16231,9 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", "dev": true, "requires": { "@babel/highlight": "^7.18.6" @@ -15302,14 +16257,40 @@ } }, "@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", "dev": true, "requires": { "regenerator-runtime": "^0.13.11" } }, + "@chainsafe/as-sha256": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz", + "integrity": "sha512-hldFFYuf49ed7DAakWVXSJODuq3pzJEguD8tQ7h+sGkM18vja+OFoJI9krnGmgzyuZC2ETX0NOIcCTy31v2Mtg==", + "dev": true + }, + "@chainsafe/persistent-merkle-tree": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.4.2.tgz", + "integrity": "sha512-lLO3ihKPngXLTus/L7WHKaw9PnNJWizlOF1H9NNzHP6Xvh82vzg9F2bzkXhYIFshMZ2gTCEz8tq6STe7r5NDfQ==", + "dev": true, + "requires": { + "@chainsafe/as-sha256": "^0.3.1" + } + }, + "@chainsafe/ssz": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@chainsafe/ssz/-/ssz-0.9.4.tgz", + "integrity": "sha512-77Qtg2N1ayqs4Bg/wvnWfg5Bta7iy7IRh8XqXh7oNMeP2HBbBwx8m6yTpA8p0EHItWPEBkgZd5S5/LSlp3GXuQ==", + "dev": true, + "requires": { + "@chainsafe/as-sha256": "^0.3.1", + "@chainsafe/persistent-merkle-tree": "^0.4.2", + "case": "^1.6.3" + } + }, "@changesets/apply-release-plan": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-6.1.3.tgz", @@ -15382,9 +16363,9 @@ } }, "@changesets/cli": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.26.0.tgz", - "integrity": "sha512-0cbTiDms+ICTVtEwAFLNW0jBNex9f5+fFv3I771nBvdnV/mOjd1QJ4+f8KtVSOrwD9SJkk9xbDkWFb0oXd8d1Q==", + "version": "2.26.1", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.26.1.tgz", + "integrity": "sha512-XnTa+b51vt057fyAudvDKGB0Sh72xutQZNAdXkCqPBKO2zvs2yYZx5hFZj1u9cbtpwM6Sxtcr02/FQJfZOzemQ==", "dev": true, "requires": { "@babel/runtime": "^7.20.1", @@ -15688,29 +16669,29 @@ "dev": true }, "@eslint-community/eslint-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz", - "integrity": "sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, "requires": { "eslint-visitor-keys": "^3.3.0" } }, "@eslint-community/regexpp": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.0.tgz", - "integrity": "sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", "dev": true }, "@eslint/eslintrc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.1.tgz", - "integrity": "sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", + "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.0", + "espree": "^9.5.1", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -15737,9 +16718,9 @@ } }, "@eslint/js": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.36.0.tgz", - "integrity": "sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", + "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", "dev": true }, "@ethereumjs/common": { @@ -16361,31 +17342,73 @@ } }, "@nomicfoundation/ethereumjs-block": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-4.0.0.tgz", - "integrity": "sha512-bk8uP8VuexLgyIZAHExH1QEovqx0Lzhc9Ntm63nCRKLHXIZkobaFaeCVwTESV7YkPKUk7NiK11s8ryed4CS9yA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-5.0.1.tgz", + "integrity": "sha512-u1Yioemi6Ckj3xspygu/SfFvm8vZEO8/Yx5a1QLzi6nVU0jz3Pg2OmHKJ5w+D9Ogk1vhwRiqEBAqcb0GVhCyHw==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-tx": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", - "ethereum-cryptography": "0.1.3" + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-trie": "6.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", + "ethereum-cryptography": "0.1.3", + "ethers": "^5.7.1" + }, + "dependencies": { + "ethers": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", + "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", + "dev": true, + "requires": { + "@ethersproject/abi": "5.7.0", + "@ethersproject/abstract-provider": "5.7.0", + "@ethersproject/abstract-signer": "5.7.0", + "@ethersproject/address": "5.7.0", + "@ethersproject/base64": "5.7.0", + "@ethersproject/basex": "5.7.0", + "@ethersproject/bignumber": "5.7.0", + "@ethersproject/bytes": "5.7.0", + "@ethersproject/constants": "5.7.0", + "@ethersproject/contracts": "5.7.0", + "@ethersproject/hash": "5.7.0", + "@ethersproject/hdnode": "5.7.0", + "@ethersproject/json-wallets": "5.7.0", + "@ethersproject/keccak256": "5.7.0", + "@ethersproject/logger": "5.7.0", + "@ethersproject/networks": "5.7.1", + "@ethersproject/pbkdf2": "5.7.0", + "@ethersproject/properties": "5.7.0", + "@ethersproject/providers": "5.7.2", + "@ethersproject/random": "5.7.0", + "@ethersproject/rlp": "5.7.0", + "@ethersproject/sha2": "5.7.0", + "@ethersproject/signing-key": "5.7.0", + "@ethersproject/solidity": "5.7.0", + "@ethersproject/strings": "5.7.0", + "@ethersproject/transactions": "5.7.0", + "@ethersproject/units": "5.7.0", + "@ethersproject/wallet": "5.7.0", + "@ethersproject/web": "5.7.1", + "@ethersproject/wordlists": "5.7.0" + } + } } }, "@nomicfoundation/ethereumjs-blockchain": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-6.0.0.tgz", - "integrity": "sha512-pLFEoea6MWd81QQYSReLlLfH7N9v7lH66JC/NMPN848ySPPQA5renWnE7wPByfQFzNrPBuDDRFFULMDmj1C0xw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-7.0.1.tgz", + "integrity": "sha512-NhzndlGg829XXbqJEYrF1VeZhAwSPgsK/OB7TVrdzft3y918hW5KNd7gIZ85sn6peDZOdjBsAXIpXZ38oBYE5A==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-block": "^4.0.0", - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-ethash": "^2.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-block": "5.0.1", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-ethash": "3.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-trie": "6.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "abstract-level": "^1.0.3", "debug": "^4.3.3", "ethereum-cryptography": "0.1.3", @@ -16395,39 +17418,39 @@ } }, "@nomicfoundation/ethereumjs-common": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-3.0.0.tgz", - "integrity": "sha512-WS7qSshQfxoZOpHG/XqlHEGRG1zmyjYrvmATvc4c62+gZXgre1ymYP8ZNgx/3FyZY0TWe9OjFlKOfLqmgOeYwA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-4.0.1.tgz", + "integrity": "sha512-OBErlkfp54GpeiE06brBW/TTbtbuBJV5YI5Nz/aB2evTDo+KawyEzPjBlSr84z/8MFfj8wS2wxzQX1o32cev5g==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-util": "9.0.1", "crc-32": "^1.2.0" } }, "@nomicfoundation/ethereumjs-ethash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-2.0.0.tgz", - "integrity": "sha512-WpDvnRncfDUuXdsAXlI4lXbqUDOA+adYRQaEezIkxqDkc+LDyYDbd/xairmY98GnQzo1zIqsIL6GB5MoMSJDew==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-3.0.1.tgz", + "integrity": "sha512-KDjGIB5igzWOp8Ik5I6QiRH5DH+XgILlplsHR7TEuWANZA759G6krQ6o8bvj+tRUz08YygMQu/sGd9mJ1DYT8w==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-block": "^4.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-block": "5.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "abstract-level": "^1.0.3", "bigint-crypto-utils": "^3.0.23", "ethereum-cryptography": "0.1.3" } }, "@nomicfoundation/ethereumjs-evm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-1.0.0.tgz", - "integrity": "sha512-hVS6qRo3V1PLKCO210UfcEQHvlG7GqR8iFzp0yyjTg2TmJQizcChKgWo8KFsdMw6AyoLgLhHGHw4HdlP8a4i+Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-2.0.1.tgz", + "integrity": "sha512-oL8vJcnk0Bx/onl+TgQOQ1t/534GKFaEG17fZmwtPFeH8S5soiBYPCLUrvANOl4sCp9elYxIMzIiTtMtNNN8EQ==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", - "@types/async-eventemitter": "^0.2.1", - "async-eventemitter": "^0.2.4", + "@ethersproject/providers": "^5.7.1", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "debug": "^4.3.3", "ethereum-cryptography": "0.1.3", "mcl-wasm": "^0.7.1", @@ -16435,80 +17458,141 @@ } }, "@nomicfoundation/ethereumjs-rlp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-4.0.0.tgz", - "integrity": "sha512-GaSOGk5QbUk4eBP5qFbpXoZoZUj/NrW7MRa0tKY4Ew4c2HAS0GXArEMAamtFrkazp0BO4K5p2ZCG3b2FmbShmw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-5.0.1.tgz", + "integrity": "sha512-xtxrMGa8kP4zF5ApBQBtjlSbN5E2HI8m8FYgVSYAnO6ssUoY5pVPGy2H8+xdf/bmMa22Ce8nWMH3aEW8CcqMeQ==", "dev": true }, "@nomicfoundation/ethereumjs-statemanager": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-1.0.0.tgz", - "integrity": "sha512-jCtqFjcd2QejtuAMjQzbil/4NHf5aAWxUc+CvS0JclQpl+7M0bxMofR2AJdtz+P3u0ke2euhYREDiE7iSO31vQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-2.0.1.tgz", + "integrity": "sha512-B5ApMOnlruVOR7gisBaYwFX+L/AP7i/2oAahatssjPIBVDF6wTX1K7Qpa39E/nzsH8iYuL3krkYeUFIdO3EMUQ==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", "debug": "^4.3.3", "ethereum-cryptography": "0.1.3", - "functional-red-black-tree": "^1.0.1" + "ethers": "^5.7.1", + "js-sdsl": "^4.1.4" + }, + "dependencies": { + "ethers": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", + "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", + "dev": true, + "requires": { + "@ethersproject/abi": "5.7.0", + "@ethersproject/abstract-provider": "5.7.0", + "@ethersproject/abstract-signer": "5.7.0", + "@ethersproject/address": "5.7.0", + "@ethersproject/base64": "5.7.0", + "@ethersproject/basex": "5.7.0", + "@ethersproject/bignumber": "5.7.0", + "@ethersproject/bytes": "5.7.0", + "@ethersproject/constants": "5.7.0", + "@ethersproject/contracts": "5.7.0", + "@ethersproject/hash": "5.7.0", + "@ethersproject/hdnode": "5.7.0", + "@ethersproject/json-wallets": "5.7.0", + "@ethersproject/keccak256": "5.7.0", + "@ethersproject/logger": "5.7.0", + "@ethersproject/networks": "5.7.1", + "@ethersproject/pbkdf2": "5.7.0", + "@ethersproject/properties": "5.7.0", + "@ethersproject/providers": "5.7.2", + "@ethersproject/random": "5.7.0", + "@ethersproject/rlp": "5.7.0", + "@ethersproject/sha2": "5.7.0", + "@ethersproject/signing-key": "5.7.0", + "@ethersproject/solidity": "5.7.0", + "@ethersproject/strings": "5.7.0", + "@ethersproject/transactions": "5.7.0", + "@ethersproject/units": "5.7.0", + "@ethersproject/wallet": "5.7.0", + "@ethersproject/web": "5.7.1", + "@ethersproject/wordlists": "5.7.0" + } + } } }, "@nomicfoundation/ethereumjs-trie": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-5.0.0.tgz", - "integrity": "sha512-LIj5XdE+s+t6WSuq/ttegJzZ1vliwg6wlb+Y9f4RlBpuK35B9K02bO7xU+E6Rgg9RGptkWd6TVLdedTI4eNc2A==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-6.0.1.tgz", + "integrity": "sha512-A64It/IMpDVODzCgxDgAAla8jNjNtsoQZIzZUfIV5AY6Coi4nvn7+VReBn5itlxMiL2yaTlQr9TRWp3CSI6VoA==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", + "@types/readable-stream": "^2.3.13", "ethereum-cryptography": "0.1.3", "readable-stream": "^3.6.0" } }, "@nomicfoundation/ethereumjs-tx": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-4.0.0.tgz", - "integrity": "sha512-Gg3Lir2lNUck43Kp/3x6TfBNwcWC9Z1wYue9Nz3v4xjdcv6oDW9QSMJxqsKw9QEGoBBZ+gqwpW7+F05/rs/g1w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-5.0.1.tgz", + "integrity": "sha512-0HwxUF2u2hrsIM1fsasjXvlbDOq1ZHFV2dd1yGq8CA+MEYhaxZr8OTScpVkkxqMwBcc5y83FyPl0J9MZn3kY0w==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@chainsafe/ssz": "^0.9.2", + "@ethersproject/providers": "^5.7.2", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "ethereum-cryptography": "0.1.3" } }, "@nomicfoundation/ethereumjs-util": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-8.0.0.tgz", - "integrity": "sha512-2emi0NJ/HmTG+CGY58fa+DQuAoroFeSH9gKu9O6JnwTtlzJtgfTixuoOqLEgyyzZVvwfIpRueuePb8TonL1y+A==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-9.0.1.tgz", + "integrity": "sha512-TwbhOWQ8QoSCFhV/DDfSmyfFIHjPjFBj957219+V3jTZYZ2rf9PmDtNOeZWAE3p3vlp8xb02XGpd0v6nTUPbsA==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-rlp": "^4.0.0-beta.2", + "@chainsafe/ssz": "^0.10.0", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", "ethereum-cryptography": "0.1.3" + }, + "dependencies": { + "@chainsafe/persistent-merkle-tree": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.5.0.tgz", + "integrity": "sha512-l0V1b5clxA3iwQLXP40zYjyZYospQLZXzBVIhhr9kDg/1qHZfzzHw0jj4VPBijfYCArZDlPkRi1wZaV2POKeuw==", + "dev": true, + "requires": { + "@chainsafe/as-sha256": "^0.3.1" + } + }, + "@chainsafe/ssz": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@chainsafe/ssz/-/ssz-0.10.2.tgz", + "integrity": "sha512-/NL3Lh8K+0q7A3LsiFq09YXS9fPE+ead2rr7vM2QK8PLzrNsw3uqrif9bpRX5UxgeRjM+vYi+boCM3+GM4ovXg==", + "dev": true, + "requires": { + "@chainsafe/as-sha256": "^0.3.1", + "@chainsafe/persistent-merkle-tree": "^0.5.0" + } + } } }, "@nomicfoundation/ethereumjs-vm": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-6.0.0.tgz", - "integrity": "sha512-JMPxvPQ3fzD063Sg3Tp+UdwUkVxMoo1uML6KSzFhMH3hoQi/LMuXBoEHAoW83/vyNS9BxEe6jm6LmT5xdeEJ6w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-7.0.1.tgz", + "integrity": "sha512-rArhyn0jPsS/D+ApFsz3yVJMQ29+pVzNZ0VJgkzAZ+7FqXSRtThl1C1prhmlVr3YNUlfpZ69Ak+RUT4g7VoOuQ==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-block": "^4.0.0", - "@nomicfoundation/ethereumjs-blockchain": "^6.0.0", - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-evm": "^1.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-statemanager": "^1.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-tx": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", - "@types/async-eventemitter": "^0.2.1", - "async-eventemitter": "^0.2.4", + "@nomicfoundation/ethereumjs-block": "5.0.1", + "@nomicfoundation/ethereumjs-blockchain": "7.0.1", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-evm": "2.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-statemanager": "2.0.1", + "@nomicfoundation/ethereumjs-trie": "6.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "debug": "^4.3.3", "ethereum-cryptography": "0.1.3", - "functional-red-black-tree": "^1.0.1", "mcl-wasm": "^0.7.1", "rustbn.js": "~0.2.0" } @@ -16774,9 +17858,9 @@ } }, "@openzeppelin/upgrades-core": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.24.1.tgz", - "integrity": "sha512-QhdIQDUykJ3vQauB6CheV7vk4zgn0e1iY+IDg7r1KqpA1m2bqIGjQCpzidW33K4bZc9zdJSPx2/Z6Um5KxCB7A==", + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.25.0.tgz", + "integrity": "sha512-vSxOSm1k+P156nNm15ydhOmSPGC37mnl092FMVOH+eGaoqLjr2Za6ULVjDMFzvMnG+sGE1UoDOqBNPfTm0ch8w==", "dev": true, "requires": { "cbor": "^8.0.0", @@ -16980,18 +18064,41 @@ "change-case": "3.0.2", "fast-check": "3.1.1", "web3-utils": "1.8.2" + }, + "dependencies": { + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true + }, + "web3-utils": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", + "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", + "dev": true, + "requires": { + "bn.js": "^5.2.1", + "ethereum-bloom-filters": "^1.0.6", + "ethereumjs-util": "^7.1.0", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randombytes": "^2.1.0", + "utf8": "3.0.0" + } + } } }, "@truffle/blockchain-utils": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@truffle/blockchain-utils/-/blockchain-utils-0.1.6.tgz", - "integrity": "sha512-SldoNRIFSm3+HMBnSc2jFsu5TWDkCN4X6vL3wrd0t6DIeF7nD6EoPPjxwbFSoqCnkkRxMuZeL6sUx7UMJS/wSA==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@truffle/blockchain-utils/-/blockchain-utils-0.1.7.tgz", + "integrity": "sha512-1nibqGjEHC7KAyDThEFvbm2+EO8zAHee/VjCtxkYBE3ySwP50joh0QCEBjy7K/9z+icpMoDucfxmgaKToBFUgQ==", "dev": true }, "@truffle/codec": { - "version": "0.14.16", - "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.14.16.tgz", - "integrity": "sha512-a9UY3n/FnkKN3Q4zOuMFOOcLWb80mdknj+voim4vvXYtJm1aAZQZE5sG9aLnMBTl4TiGLzUtfNDVYY7WgWgDag==", + "version": "0.14.17", + "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.14.17.tgz", + "integrity": "sha512-kD4dD86huLeaBEq5R8D1zleJEu6NsXbyYLdXl1V1TKdiO8odw5CBC6Y/+wdu5d3t1dyEYrTbhn1dqknZa52pmw==", "dev": true, "requires": { "@truffle/abi-utils": "^0.3.9", @@ -17052,6 +18159,21 @@ "lru-cache": "^6.0.0" } }, + "web3-utils": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", + "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", + "dev": true, + "requires": { + "bn.js": "^5.2.1", + "ethereum-bloom-filters": "^1.0.6", + "ethereumjs-util": "^7.1.0", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randombytes": "^2.1.0", + "utf8": "3.0.0" + } + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -17079,17 +18201,17 @@ } }, "@truffle/contract": { - "version": "4.6.17", - "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.17.tgz", - "integrity": "sha512-sIMam5Wqr9AEiqHfOcWGJGqTv8Qy+BT765PaNHUUT6JBAY+tpHM3FlQd2nM6zLJ8paR3SLDGIthkhCBH/KNgDA==", + "version": "4.6.20", + "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.20.tgz", + "integrity": "sha512-s7Mbc37L/CF5Apy/cjPnalkgACmG9tTAmcIW28cIZLRLOUAze18pqhtdHryxAQhEOtKGaDAho6TriqL7/74uHw==", "dev": true, "requires": { "@ensdomains/ensjs": "^2.1.0", - "@truffle/blockchain-utils": "^0.1.6", + "@truffle/blockchain-utils": "^0.1.7", "@truffle/contract-schema": "^3.4.13", - "@truffle/debug-utils": "^6.0.47", + "@truffle/debug-utils": "^6.0.48", "@truffle/error": "^0.2.0", - "@truffle/interface-adapter": "^0.5.30", + "@truffle/interface-adapter": "^0.5.32", "bignumber.js": "^7.2.1", "debug": "^4.3.1", "ethers": "^4.0.32", @@ -17105,6 +18227,318 @@ "resolved": "https://registry.npmjs.org/@truffle/error/-/error-0.2.0.tgz", "integrity": "sha512-Fe0/z4WWb7IP2gBnv3l6zqP87Y0kSMs7oiSLakKJq17q3GUunrHSdioKuNspdggxkXIBhEQLhi8C+LJdwmHKWQ==", "dev": true + }, + "eth-lib": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.2.8.tgz", + "integrity": "sha512-ArJ7x1WcWOlSpzdoTBX8vkwlkSQ85CjjifSZtV4co64vWxSV8geWfPI9x4SVYu3DSxnX4yWFVTtGL+j9DUFLNw==", + "dev": true, + "requires": { + "bn.js": "^4.11.6", + "elliptic": "^6.4.0", + "xhr-request-promise": "^0.1.2" + } + }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true + }, + "web3": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.8.2.tgz", + "integrity": "sha512-92h0GdEHW9wqDICQQKyG4foZBYi0OQkyg4CRml2F7XBl/NG+fu9o6J19kzfFXzSBoA4DnJXbyRgj/RHZv5LRiw==", + "dev": true, + "requires": { + "web3-bzz": "1.8.2", + "web3-core": "1.8.2", + "web3-eth": "1.8.2", + "web3-eth-personal": "1.8.2", + "web3-net": "1.8.2", + "web3-shh": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-bzz": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.8.2.tgz", + "integrity": "sha512-1EEnxjPnFnvNWw3XeeKuTR8PBxYd0+XWzvaLK7OJC/Go9O8llLGxrxICbKV+8cgIE0sDRBxiYx02X+6OhoAQ9w==", + "dev": true, + "requires": { + "@types/node": "^12.12.6", + "got": "12.1.0", + "swarm-js": "^0.1.40" + } + }, + "web3-core": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.8.2.tgz", + "integrity": "sha512-DJTVEAYcNqxkqruJE+Rxp3CIv0y5AZMwPHQmOkz/cz+MM75SIzMTc0AUdXzGyTS8xMF8h3YWMQGgGEy8SBf1PQ==", + "dev": true, + "requires": { + "@types/bn.js": "^5.1.0", + "@types/node": "^12.12.6", + "bignumber.js": "^9.0.0", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-requestmanager": "1.8.2", + "web3-utils": "1.8.2" + }, + "dependencies": { + "bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "dev": true + } + } + }, + "web3-core-helpers": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.8.2.tgz", + "integrity": "sha512-6B1eLlq9JFrfealZBomd1fmlq1o4A09vrCVQSa51ANoib/jllT3atZrRDr0zt1rfI7TSZTZBXdN/aTdeN99DWw==", + "dev": true, + "requires": { + "web3-eth-iban": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-core-method": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.8.2.tgz", + "integrity": "sha512-1qnr5mw5wVyULzLOrk4B+ryO3gfGjGd/fx8NR+J2xCGLf1e6OSjxT9vbfuQ3fErk/NjSTWWreieYWLMhaogcRA==", + "dev": true, + "requires": { + "@ethersproject/transactions": "^5.6.2", + "web3-core-helpers": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-core-promievent": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.8.2.tgz", + "integrity": "sha512-nvkJWDVgoOSsolJldN33tKW6bKKRJX3MCPDYMwP5SUFOA/mCzDEoI88N0JFofDTXkh1k7gOqp1pvwi9heuaxGg==", + "dev": true, + "requires": { + "eventemitter3": "4.0.4" + } + }, + "web3-core-requestmanager": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.8.2.tgz", + "integrity": "sha512-p1d090RYs5Mu7DK1yyc3GCBVZB/03rBtFhYFoS2EruGzOWs/5Q0grgtpwS/DScdRAm8wB8mYEBhY/RKJWF6B2g==", + "dev": true, + "requires": { + "util": "^0.12.5", + "web3-core-helpers": "1.8.2", + "web3-providers-http": "1.8.2", + "web3-providers-ipc": "1.8.2", + "web3-providers-ws": "1.8.2" + } + }, + "web3-core-subscriptions": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.8.2.tgz", + "integrity": "sha512-vXQogHDmAIQcKpXvGiMddBUeP9lnKgYF64+yQJhPNE5PnWr1sAibXuIPV7mIPihpFr/n/DORRj6Wh1pUv9zaTw==", + "dev": true, + "requires": { + "eventemitter3": "4.0.4", + "web3-core-helpers": "1.8.2" + } + }, + "web3-eth": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.8.2.tgz", + "integrity": "sha512-JoTiWWc4F4TInpbvDUGb0WgDYJsFhuIjJlinc5ByjWD88Gvh+GKLsRjjFdbqe5YtwIGT4NymwoC5LQd1K6u/QQ==", + "dev": true, + "requires": { + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-eth-accounts": "1.8.2", + "web3-eth-contract": "1.8.2", + "web3-eth-ens": "1.8.2", + "web3-eth-iban": "1.8.2", + "web3-eth-personal": "1.8.2", + "web3-net": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-eth-abi": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.8.2.tgz", + "integrity": "sha512-Om9g3kaRNjqiNPAgKwGT16y+ZwtBzRe4ZJFGjLiSs6v5I7TPNF+rRMWuKnR6jq0azQZDj6rblvKFMA49/k48Og==", + "dev": true, + "requires": { + "@ethersproject/abi": "^5.6.3", + "web3-utils": "1.8.2" + } + }, + "web3-eth-accounts": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.8.2.tgz", + "integrity": "sha512-c367Ij63VCz9YdyjiHHWLFtN85l6QghgwMQH2B1eM/p9Y5lTlTX7t/Eg/8+f1yoIStXbk2w/PYM2lk+IkbqdLA==", + "dev": true, + "requires": { + "@ethereumjs/common": "2.5.0", + "@ethereumjs/tx": "3.3.2", + "eth-lib": "0.2.8", + "ethereumjs-util": "^7.1.5", + "scrypt-js": "^3.0.1", + "uuid": "^9.0.0", + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-eth-contract": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.8.2.tgz", + "integrity": "sha512-ID5A25tHTSBNwOPjiXSVzxruz006ULRIDbzWTYIFTp7NJ7vXu/kynKK2ag/ObuTqBpMbobP8nXcA9b5EDkIdQA==", + "dev": true, + "requires": { + "@types/bn.js": "^5.1.0", + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-eth-ens": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.8.2.tgz", + "integrity": "sha512-PWph7C/CnqdWuu1+SH4U4zdrK4t2HNt0I4XzPYFdv9ugE8EuojselioPQXsVGvjql+Nt3jDLvQvggPqlMbvwRw==", + "dev": true, + "requires": { + "content-hash": "^2.5.2", + "eth-ens-namehash": "2.0.8", + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-eth-contract": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-eth-iban": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.8.2.tgz", + "integrity": "sha512-h3vNblDWkWMuYx93Q27TAJz6lhzpP93EiC3+45D6xoz983p6si773vntoQ+H+5aZhwglBtoiBzdh7PSSOnP/xQ==", + "dev": true, + "requires": { + "bn.js": "^5.2.1", + "web3-utils": "1.8.2" + }, + "dependencies": { + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true + } + } + }, + "web3-eth-personal": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.8.2.tgz", + "integrity": "sha512-Vg4HfwCr7doiUF/RC+Jz0wT4+cYaXcOWMAW2AHIjHX6Z7Xwa8nrURIeQgeEE62qcEHAzajyAdB1u6bJyTfuCXw==", + "dev": true, + "requires": { + "@types/node": "^12.12.6", + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-net": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-net": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.8.2.tgz", + "integrity": "sha512-1itkDMGmbgb83Dg9nporFes9/fxsU7smJ3oRXlFkg4ZHn8YJyP1MSQFPJWWwSc+GrcCFt4O5IrUTvEkHqE3xag==", + "dev": true, + "requires": { + "web3-core": "1.8.2", + "web3-core-method": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-providers-http": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.8.2.tgz", + "integrity": "sha512-2xY94IIEQd16+b+vIBF4IC1p7GVaz9q4EUFscvMUjtEq4ru4Atdzjs9GP+jmcoo49p70II0UV3bqQcz0TQfVyQ==", + "dev": true, + "requires": { + "abortcontroller-polyfill": "^1.7.3", + "cross-fetch": "^3.1.4", + "es6-promise": "^4.2.8", + "web3-core-helpers": "1.8.2" + } + }, + "web3-providers-ipc": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.8.2.tgz", + "integrity": "sha512-p6fqKVGFg+WiXGHWnB1hu43PbvPkDHTz4RgoEzbXugv5rtv5zfYLqm8Ba6lrJOS5ks9kGKR21a0y3NzE3u7V4w==", + "dev": true, + "requires": { + "oboe": "2.1.5", + "web3-core-helpers": "1.8.2" + } + }, + "web3-providers-ws": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.8.2.tgz", + "integrity": "sha512-3s/4K+wHgbiN+Zrp9YjMq2eqAF6QGABw7wFftPdx+m5hWImV27/MoIx57c6HffNRqZXmCHnfWWFCNHHsi7wXnA==", + "dev": true, + "requires": { + "eventemitter3": "4.0.4", + "web3-core-helpers": "1.8.2", + "websocket": "^1.0.32" + } + }, + "web3-shh": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.8.2.tgz", + "integrity": "sha512-uZ+3MAoNcaJsXXNCDnizKJ5viBNeHOFYsCbFhV755Uu52FswzTOw6DtE7yK9nYXMtIhiSgi7nwl1RYzP8pystw==", + "dev": true, + "requires": { + "web3-core": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-net": "1.8.2" + } + }, + "web3-utils": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", + "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", + "dev": true, + "requires": { + "bn.js": "^5.2.1", + "ethereum-bloom-filters": "^1.0.6", + "ethereumjs-util": "^7.1.0", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randombytes": "^2.1.0", + "utf8": "3.0.0" + }, + "dependencies": { + "bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true + } + } } } }, @@ -17119,12 +18553,12 @@ } }, "@truffle/debug-utils": { - "version": "6.0.47", - "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.47.tgz", - "integrity": "sha512-bUjdzLPdEKtoUCDzaXkrOoi+PbyAJlMBzGequBK8tirT7xL9bCP2Pd/WxvnmRd7AnfroxGNvXwVXWTItW5SMWQ==", + "version": "6.0.48", + "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.48.tgz", + "integrity": "sha512-HdK/7eH5EFrcTPeZVEgKaKkkzuZ4xsrH8yw+EoLEsScLsOEuQeKynY61NctjuU93voATWrYmV99Sfb/MRq2i2g==", "dev": true, "requires": { - "@truffle/codec": "^0.14.16", + "@truffle/codec": "^0.14.17", "@trufflesuite/chromafi": "^3.0.0", "bn.js": "^5.1.3", "chalk": "^2.4.2", @@ -17147,9 +18581,9 @@ "dev": true }, "@truffle/interface-adapter": { - "version": "0.5.30", - "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.30.tgz", - "integrity": "sha512-wyCcESeZJBkAfuSGU8GHCusIWDUDyQjJZMcyShv59ZTSAwQR7xx0+a0Q1LlS532G/pGFLYe2VzKSY1pRHRwgug==", + "version": "0.5.32", + "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.32.tgz", + "integrity": "sha512-7Hgmdb4nJUcWtq4vvgWY7Mr2RLOTOp5FZaWyMiFmjkcEEhDlezm2QstALWAXgT0W6q7tDmDBpw3vTIFenRhHBA==", "dev": true, "requires": { "bn.js": "^5.1.3", @@ -17157,11 +18591,313 @@ "web3": "1.8.2" }, "dependencies": { + "bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "dev": true + }, "bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", "dev": true + }, + "eth-lib": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.2.8.tgz", + "integrity": "sha512-ArJ7x1WcWOlSpzdoTBX8vkwlkSQ85CjjifSZtV4co64vWxSV8geWfPI9x4SVYu3DSxnX4yWFVTtGL+j9DUFLNw==", + "dev": true, + "requires": { + "bn.js": "^4.11.6", + "elliptic": "^6.4.0", + "xhr-request-promise": "^0.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + } + } + }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true + }, + "web3": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.8.2.tgz", + "integrity": "sha512-92h0GdEHW9wqDICQQKyG4foZBYi0OQkyg4CRml2F7XBl/NG+fu9o6J19kzfFXzSBoA4DnJXbyRgj/RHZv5LRiw==", + "dev": true, + "requires": { + "web3-bzz": "1.8.2", + "web3-core": "1.8.2", + "web3-eth": "1.8.2", + "web3-eth-personal": "1.8.2", + "web3-net": "1.8.2", + "web3-shh": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-bzz": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.8.2.tgz", + "integrity": "sha512-1EEnxjPnFnvNWw3XeeKuTR8PBxYd0+XWzvaLK7OJC/Go9O8llLGxrxICbKV+8cgIE0sDRBxiYx02X+6OhoAQ9w==", + "dev": true, + "requires": { + "@types/node": "^12.12.6", + "got": "12.1.0", + "swarm-js": "^0.1.40" + } + }, + "web3-core": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.8.2.tgz", + "integrity": "sha512-DJTVEAYcNqxkqruJE+Rxp3CIv0y5AZMwPHQmOkz/cz+MM75SIzMTc0AUdXzGyTS8xMF8h3YWMQGgGEy8SBf1PQ==", + "dev": true, + "requires": { + "@types/bn.js": "^5.1.0", + "@types/node": "^12.12.6", + "bignumber.js": "^9.0.0", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-requestmanager": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-core-helpers": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.8.2.tgz", + "integrity": "sha512-6B1eLlq9JFrfealZBomd1fmlq1o4A09vrCVQSa51ANoib/jllT3atZrRDr0zt1rfI7TSZTZBXdN/aTdeN99DWw==", + "dev": true, + "requires": { + "web3-eth-iban": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-core-method": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.8.2.tgz", + "integrity": "sha512-1qnr5mw5wVyULzLOrk4B+ryO3gfGjGd/fx8NR+J2xCGLf1e6OSjxT9vbfuQ3fErk/NjSTWWreieYWLMhaogcRA==", + "dev": true, + "requires": { + "@ethersproject/transactions": "^5.6.2", + "web3-core-helpers": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-core-promievent": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.8.2.tgz", + "integrity": "sha512-nvkJWDVgoOSsolJldN33tKW6bKKRJX3MCPDYMwP5SUFOA/mCzDEoI88N0JFofDTXkh1k7gOqp1pvwi9heuaxGg==", + "dev": true, + "requires": { + "eventemitter3": "4.0.4" + } + }, + "web3-core-requestmanager": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.8.2.tgz", + "integrity": "sha512-p1d090RYs5Mu7DK1yyc3GCBVZB/03rBtFhYFoS2EruGzOWs/5Q0grgtpwS/DScdRAm8wB8mYEBhY/RKJWF6B2g==", + "dev": true, + "requires": { + "util": "^0.12.5", + "web3-core-helpers": "1.8.2", + "web3-providers-http": "1.8.2", + "web3-providers-ipc": "1.8.2", + "web3-providers-ws": "1.8.2" + } + }, + "web3-core-subscriptions": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.8.2.tgz", + "integrity": "sha512-vXQogHDmAIQcKpXvGiMddBUeP9lnKgYF64+yQJhPNE5PnWr1sAibXuIPV7mIPihpFr/n/DORRj6Wh1pUv9zaTw==", + "dev": true, + "requires": { + "eventemitter3": "4.0.4", + "web3-core-helpers": "1.8.2" + } + }, + "web3-eth": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.8.2.tgz", + "integrity": "sha512-JoTiWWc4F4TInpbvDUGb0WgDYJsFhuIjJlinc5ByjWD88Gvh+GKLsRjjFdbqe5YtwIGT4NymwoC5LQd1K6u/QQ==", + "dev": true, + "requires": { + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-eth-accounts": "1.8.2", + "web3-eth-contract": "1.8.2", + "web3-eth-ens": "1.8.2", + "web3-eth-iban": "1.8.2", + "web3-eth-personal": "1.8.2", + "web3-net": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-eth-abi": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.8.2.tgz", + "integrity": "sha512-Om9g3kaRNjqiNPAgKwGT16y+ZwtBzRe4ZJFGjLiSs6v5I7TPNF+rRMWuKnR6jq0azQZDj6rblvKFMA49/k48Og==", + "dev": true, + "requires": { + "@ethersproject/abi": "^5.6.3", + "web3-utils": "1.8.2" + } + }, + "web3-eth-accounts": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.8.2.tgz", + "integrity": "sha512-c367Ij63VCz9YdyjiHHWLFtN85l6QghgwMQH2B1eM/p9Y5lTlTX7t/Eg/8+f1yoIStXbk2w/PYM2lk+IkbqdLA==", + "dev": true, + "requires": { + "@ethereumjs/common": "2.5.0", + "@ethereumjs/tx": "3.3.2", + "eth-lib": "0.2.8", + "ethereumjs-util": "^7.1.5", + "scrypt-js": "^3.0.1", + "uuid": "^9.0.0", + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-eth-contract": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.8.2.tgz", + "integrity": "sha512-ID5A25tHTSBNwOPjiXSVzxruz006ULRIDbzWTYIFTp7NJ7vXu/kynKK2ag/ObuTqBpMbobP8nXcA9b5EDkIdQA==", + "dev": true, + "requires": { + "@types/bn.js": "^5.1.0", + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-eth-ens": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.8.2.tgz", + "integrity": "sha512-PWph7C/CnqdWuu1+SH4U4zdrK4t2HNt0I4XzPYFdv9ugE8EuojselioPQXsVGvjql+Nt3jDLvQvggPqlMbvwRw==", + "dev": true, + "requires": { + "content-hash": "^2.5.2", + "eth-ens-namehash": "2.0.8", + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-promievent": "1.8.2", + "web3-eth-abi": "1.8.2", + "web3-eth-contract": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-eth-iban": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.8.2.tgz", + "integrity": "sha512-h3vNblDWkWMuYx93Q27TAJz6lhzpP93EiC3+45D6xoz983p6si773vntoQ+H+5aZhwglBtoiBzdh7PSSOnP/xQ==", + "dev": true, + "requires": { + "bn.js": "^5.2.1", + "web3-utils": "1.8.2" + } + }, + "web3-eth-personal": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.8.2.tgz", + "integrity": "sha512-Vg4HfwCr7doiUF/RC+Jz0wT4+cYaXcOWMAW2AHIjHX6Z7Xwa8nrURIeQgeEE62qcEHAzajyAdB1u6bJyTfuCXw==", + "dev": true, + "requires": { + "@types/node": "^12.12.6", + "web3-core": "1.8.2", + "web3-core-helpers": "1.8.2", + "web3-core-method": "1.8.2", + "web3-net": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-net": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.8.2.tgz", + "integrity": "sha512-1itkDMGmbgb83Dg9nporFes9/fxsU7smJ3oRXlFkg4ZHn8YJyP1MSQFPJWWwSc+GrcCFt4O5IrUTvEkHqE3xag==", + "dev": true, + "requires": { + "web3-core": "1.8.2", + "web3-core-method": "1.8.2", + "web3-utils": "1.8.2" + } + }, + "web3-providers-http": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.8.2.tgz", + "integrity": "sha512-2xY94IIEQd16+b+vIBF4IC1p7GVaz9q4EUFscvMUjtEq4ru4Atdzjs9GP+jmcoo49p70II0UV3bqQcz0TQfVyQ==", + "dev": true, + "requires": { + "abortcontroller-polyfill": "^1.7.3", + "cross-fetch": "^3.1.4", + "es6-promise": "^4.2.8", + "web3-core-helpers": "1.8.2" + } + }, + "web3-providers-ipc": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.8.2.tgz", + "integrity": "sha512-p6fqKVGFg+WiXGHWnB1hu43PbvPkDHTz4RgoEzbXugv5rtv5zfYLqm8Ba6lrJOS5ks9kGKR21a0y3NzE3u7V4w==", + "dev": true, + "requires": { + "oboe": "2.1.5", + "web3-core-helpers": "1.8.2" + } + }, + "web3-providers-ws": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.8.2.tgz", + "integrity": "sha512-3s/4K+wHgbiN+Zrp9YjMq2eqAF6QGABw7wFftPdx+m5hWImV27/MoIx57c6HffNRqZXmCHnfWWFCNHHsi7wXnA==", + "dev": true, + "requires": { + "eventemitter3": "4.0.4", + "web3-core-helpers": "1.8.2", + "websocket": "^1.0.32" + } + }, + "web3-shh": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.8.2.tgz", + "integrity": "sha512-uZ+3MAoNcaJsXXNCDnizKJ5viBNeHOFYsCbFhV755Uu52FswzTOw6DtE7yK9nYXMtIhiSgi7nwl1RYzP8pystw==", + "dev": true, + "requires": { + "web3-core": "1.8.2", + "web3-core-method": "1.8.2", + "web3-core-subscriptions": "1.8.2", + "web3-net": "1.8.2" + } + }, + "web3-utils": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", + "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", + "dev": true, + "requires": { + "bn.js": "^5.2.1", + "ethereum-bloom-filters": "^1.0.6", + "ethereumjs-util": "^7.1.0", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randombytes": "^2.1.0", + "utf8": "3.0.0" + } } } }, @@ -17189,12 +18925,6 @@ } } }, - "@types/async-eventemitter": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@types/async-eventemitter/-/async-eventemitter-0.2.1.tgz", - "integrity": "sha512-M2P4Ng26QbAeITiH7w1d7OxtldgfAe0wobpyJzVK/XOb0cUGKU2R4pfAhqcJBXAe2ife5ZOhSv4wk7p+ffURtg==", - "dev": true - }, "@types/bignumber.js": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/bignumber.js/-/bignumber.js-5.0.0.tgz", @@ -17226,9 +18956,9 @@ } }, "@types/chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", "dev": true }, "@types/concat-stream": { @@ -17328,6 +19058,24 @@ "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", "dev": true }, + "@types/readable-stream": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.15.tgz", + "integrity": "sha512-oM5JSKQCcICF1wvGgmecmHldZ48OZamtMxcGGVICOJA8o8cahXC1zEVAif8iwoc5j8etxFaRFnf095+CDsuoFQ==", + "dev": true, + "requires": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, "@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -17647,22 +19395,10 @@ "dev": true }, "async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - }, - "async-eventemitter": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/async-eventemitter/-/async-eventemitter-0.2.4.tgz", - "integrity": "sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw==", - "dev": true, - "requires": { - "async": "^2.4.0" - } + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "dev": true }, "async-limiter": { "version": "1.0.1", @@ -17760,18 +19496,9 @@ "dev": true }, "bigint-crypto-utils": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/bigint-crypto-utils/-/bigint-crypto-utils-3.1.8.tgz", - "integrity": "sha512-+VMV9Laq8pXLBKKKK49nOoq9bfR3j7NNQAtbA617a4nw9bVLo8rsqkKMBgM2AJWlNX9fEIyYaYX+d0laqYV4tw==", - "dev": true, - "requires": { - "bigint-mod-arith": "^3.1.0" - } - }, - "bigint-mod-arith": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bigint-mod-arith/-/bigint-mod-arith-3.1.2.tgz", - "integrity": "sha512-nx8J8bBeiRR+NlsROFH9jHswW5HO8mgfOSqW0AmjicMMvaONDa8AO+5ViKDUUNytBPWiwfvZP4/Bj4Y3lUfvgQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/bigint-crypto-utils/-/bigint-crypto-utils-3.2.2.tgz", + "integrity": "sha512-U1RbE3aX9ayCUVcIPHuPDPKcK3SFOXf93J1UK/iHlJuQB7bhagPIX06/CLpLEsDThJ7KA4Dhrnzynl+d2weTiw==", "dev": true }, "bignumber.js": { @@ -18089,6 +19816,12 @@ } } }, + "case": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/case/-/case-1.6.3.tgz", + "integrity": "sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==", + "dev": true + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -18286,15 +20019,15 @@ "dev": true }, "classic-level": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.2.0.tgz", - "integrity": "sha512-qw5B31ANxSluWz9xBzklRWTUAJ1SXIdaVKTVS7HcTGKOAmExx65Wo5BUICW+YGORe2FOUaDghoI9ZDxj82QcFg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.3.0.tgz", + "integrity": "sha512-iwFAJQYtqRTRM0F6L8h4JCt00ZSGdOyqh7yVrhhjrOpFhmBjNlRUey64MCiyo6UmQHMJ+No3c81nujPv+n9yrg==", "dev": true, "requires": { "abstract-level": "^1.0.2", "catering": "^2.1.0", "module-error": "^1.0.1", - "napi-macros": "~2.0.0", + "napi-macros": "^2.2.2", "node-gyp-build": "^4.3.0" } }, @@ -18418,9 +20151,9 @@ "dev": true }, "commander": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz", - "integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true }, "compare-versions": { @@ -18544,9 +20277,9 @@ } }, "cosmiconfig": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.0.tgz", - "integrity": "sha512-0tLZ9URlPGU7JsKq0DQOQ3FoRsYX8xDZ7xMiATQfaiGMz7EHowNkbU9u1coAOmnh9p/1ySpm0RB3JNWRXM5GCg==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", + "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", "dev": true, "requires": { "import-fresh": "^3.2.1", @@ -18939,14 +20672,14 @@ } }, "domutils": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", - "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dev": true, "requires": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", - "domhandler": "^5.0.1" + "domhandler": "^5.0.3" } }, "dot-case": { @@ -19026,9 +20759,9 @@ } }, "entities": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true }, "env-paths": { @@ -19248,15 +20981,15 @@ } }, "eslint": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.36.0.tgz", - "integrity": "sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", + "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.1", - "@eslint/js": "8.36.0", + "@eslint/eslintrc": "^2.0.2", + "@eslint/js": "8.39.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -19266,9 +20999,9 @@ "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.5.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.0", + "espree": "^9.5.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -19420,16 +21153,16 @@ } }, "eslint-config-prettier": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.7.0.tgz", - "integrity": "sha512-HHVXLSlVUhMSmyW4ZzEuvjpwqamgmlfkutD53cYXLikh4pt/modINRcCIApJ84czDxM4GZInwUrromsDdTImTA==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", "dev": true, "requires": {} }, "eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -19437,20 +21170,20 @@ } }, "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", + "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", "dev": true }, "espree": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.0.tgz", - "integrity": "sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", + "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", "dev": true, "requires": { "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.0" } }, "esprima": { @@ -20839,9 +22572,9 @@ } }, "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, "grapheme-splitter": { @@ -20909,23 +22642,23 @@ "dev": true }, "hardhat": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.13.0.tgz", - "integrity": "sha512-ZlzBOLML1QGlm6JWyVAG8lVTEAoOaVm1in/RU2zoGAnYEoD1Rp4T+ZMvrLNhHaaeS9hfjJ1gJUBfiDr4cx+htQ==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.14.0.tgz", + "integrity": "sha512-73jsInY4zZahMSVFurSK+5TNCJTXMv+vemvGia0Ac34Mm19fYp6vEPVGF3sucbumszsYxiTT2TbS8Ii2dsDSoQ==", "dev": true, "requires": { "@ethersproject/abi": "^5.1.2", "@metamask/eth-sig-util": "^4.0.0", - "@nomicfoundation/ethereumjs-block": "^4.0.0", - "@nomicfoundation/ethereumjs-blockchain": "^6.0.0", - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-evm": "^1.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-statemanager": "^1.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-tx": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", - "@nomicfoundation/ethereumjs-vm": "^6.0.0", + "@nomicfoundation/ethereumjs-block": "5.0.1", + "@nomicfoundation/ethereumjs-blockchain": "7.0.1", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-evm": "2.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-statemanager": "2.0.1", + "@nomicfoundation/ethereumjs-trie": "6.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", + "@nomicfoundation/ethereumjs-vm": "7.0.1", "@nomicfoundation/solidity-analyzer": "^0.1.0", "@sentry/node": "^5.18.1", "@types/bn.js": "^5.1.0", @@ -21299,15 +23032,15 @@ "dev": true }, "htmlparser2": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", - "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", "dev": true, "requires": { "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", + "domhandler": "^5.0.3", "domutils": "^3.0.1", - "entities": "^4.3.0" + "entities": "^4.4.0" } }, "http-basic": { @@ -21830,9 +23563,9 @@ "dev": true }, "js-sdsl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", - "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", + "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", "dev": true }, "js-sha3": { @@ -22495,9 +24228,9 @@ } }, "mixme": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.5.tgz", - "integrity": "sha512-/6IupbRx32s7jjEwHcycXikJwFD5UujbVNuJFkeKLYje+92OvtuPniF6JhnFm5JCTDUhS+kYK3W/4BWYQYXz7w==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.9.tgz", + "integrity": "sha512-VC5fg6ySUscaWUpI4gxCBTQMH2RdUpNrk+MsbpCYtIvf9SBJdiUey4qE7BXviJsJR4nDQxCZ+3yaYNW3guz/Pw==", "dev": true }, "mkdirp": { @@ -22822,9 +24555,9 @@ "dev": true }, "napi-macros": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", - "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.2.2.tgz", + "integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==", "dev": true }, "natural-compare": { @@ -23034,15 +24767,16 @@ } }, "object.getownpropertydescriptors": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz", - "integrity": "sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz", + "integrity": "sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ==", "dev": true, "requires": { "array.prototype.reduce": "^1.0.5", "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.21.2", + "safe-array-concat": "^1.0.0" } }, "obliterator": { @@ -23424,9 +25158,9 @@ "dev": true }, "prettier": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", - "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true }, "prettier-plugin-solidity": { @@ -23703,14 +25437,14 @@ "dev": true }, "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" } }, "req-cwd": { @@ -23951,6 +25685,26 @@ "integrity": "sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==", "dev": true }, + "safe-array-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", + "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -23996,12 +25750,6 @@ "wordwrap": "^1.0.0" }, "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", - "dev": true - }, "esprima": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", @@ -24071,9 +25819,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -26041,18 +27789,18 @@ "dev": true }, "tty-table": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.1.6.tgz", - "integrity": "sha512-kRj5CBzOrakV4VRRY5kUWbNYvo/FpOsz65DzI5op9P+cHov3+IqPbo1JE1ZnQGkHdZgNFDsrEjrfqqy/Ply9fw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.2.1.tgz", + "integrity": "sha512-xz0uKo+KakCQ+Dxj1D/tKn2FSyreSYWzdkL/BYhgN6oMW808g8QRMuh1atAV9fjTPbWBjfbkKQpI/5rEcnAc7g==", "dev": true, "requires": { "chalk": "^4.1.2", - "csv": "^5.5.0", - "kleur": "^4.1.4", + "csv": "^5.5.3", + "kleur": "^4.1.5", "smartwrap": "^2.0.2", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wcwidth": "^1.0.1", - "yargs": "^17.1.1" + "yargs": "^17.7.1" }, "dependencies": { "ansi-regex": { @@ -26237,9 +27985,9 @@ "dev": true }, "undici": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.21.0.tgz", - "integrity": "sha512-HOjK8l6a57b2ZGXOcUsI5NLfoTrfmbOl90ixJDl0AEFG4wgHNDQxtZy15/ZQp7HhjkpaGlp/eneMgtsu1dIlUA==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.0.tgz", + "integrity": "sha512-fR9RXCc+6Dxav4P9VV/sp5w3eFiSdOjJYsbtWfd4s5L5C4ogyuVpdKIVHeW0vV1MloM65/f7W45nR9ZxwVdyiA==", "dev": true, "requires": { "busboy": "^1.6.0" @@ -26376,24 +28124,24 @@ } }, "web3": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.8.2.tgz", - "integrity": "sha512-92h0GdEHW9wqDICQQKyG4foZBYi0OQkyg4CRml2F7XBl/NG+fu9o6J19kzfFXzSBoA4DnJXbyRgj/RHZv5LRiw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.9.0.tgz", + "integrity": "sha512-E9IvVy/d2ozfQQsCiV+zh/LmlZGv9fQxI0UedDVjm87yOKf4AYbBNEn1iWtHveiGzAk2CEMZMUzAZzaQNSSYog==", "dev": true, "requires": { - "web3-bzz": "1.8.2", - "web3-core": "1.8.2", - "web3-eth": "1.8.2", - "web3-eth-personal": "1.8.2", - "web3-net": "1.8.2", - "web3-shh": "1.8.2", - "web3-utils": "1.8.2" + "web3-bzz": "1.9.0", + "web3-core": "1.9.0", + "web3-eth": "1.9.0", + "web3-eth-personal": "1.9.0", + "web3-net": "1.9.0", + "web3-shh": "1.9.0", + "web3-utils": "1.9.0" } }, "web3-bzz": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.8.2.tgz", - "integrity": "sha512-1EEnxjPnFnvNWw3XeeKuTR8PBxYd0+XWzvaLK7OJC/Go9O8llLGxrxICbKV+8cgIE0sDRBxiYx02X+6OhoAQ9w==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.9.0.tgz", + "integrity": "sha512-9Zli9dikX8GdHwBb5/WPzpSVuy3EWMKY3P4EokCQra31fD7DLizqAAaTUsFwnK7xYkw5ogpHgelw9uKHHzNajg==", "dev": true, "requires": { "@types/node": "^12.12.6", @@ -26402,18 +28150,18 @@ } }, "web3-core": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.8.2.tgz", - "integrity": "sha512-DJTVEAYcNqxkqruJE+Rxp3CIv0y5AZMwPHQmOkz/cz+MM75SIzMTc0AUdXzGyTS8xMF8h3YWMQGgGEy8SBf1PQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.9.0.tgz", + "integrity": "sha512-DZ+TPmq/ZLlx4LSVzFgrHCP/QFpKDbGWO4HoquZSdu24cjk5SZ+FEU1SZB2OaK3/bgBh+25mRbmv8y56ysUu1w==", "dev": true, "requires": { - "@types/bn.js": "^5.1.0", + "@types/bn.js": "^5.1.1", "@types/node": "^12.12.6", "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-requestmanager": "1.8.2", - "web3-utils": "1.8.2" + "web3-core-helpers": "1.9.0", + "web3-core-method": "1.9.0", + "web3-core-requestmanager": "1.9.0", + "web3-utils": "1.9.0" }, "dependencies": { "bignumber.js": { @@ -26425,94 +28173,94 @@ } }, "web3-core-helpers": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.8.2.tgz", - "integrity": "sha512-6B1eLlq9JFrfealZBomd1fmlq1o4A09vrCVQSa51ANoib/jllT3atZrRDr0zt1rfI7TSZTZBXdN/aTdeN99DWw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.9.0.tgz", + "integrity": "sha512-NeJzylAp9Yj9xAt2uTT+kyug3X0DLnfBdnAcGZuY6HhoNPDIfQRA9CkJjLngVRlGTLZGjNp9x9eR+RyZQgUlXg==", "dev": true, "requires": { - "web3-eth-iban": "1.8.2", - "web3-utils": "1.8.2" + "web3-eth-iban": "1.9.0", + "web3-utils": "1.9.0" } }, "web3-core-method": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.8.2.tgz", - "integrity": "sha512-1qnr5mw5wVyULzLOrk4B+ryO3gfGjGd/fx8NR+J2xCGLf1e6OSjxT9vbfuQ3fErk/NjSTWWreieYWLMhaogcRA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.9.0.tgz", + "integrity": "sha512-sswbNsY2xRBBhGeaLt9c/eDc+0yDDhi6keUBAkgIRa9ueSx/VKzUY9HMqiV6bXDcGT2fJyejq74FfEB4lc/+/w==", "dev": true, "requires": { "@ethersproject/transactions": "^5.6.2", - "web3-core-helpers": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-utils": "1.8.2" + "web3-core-helpers": "1.9.0", + "web3-core-promievent": "1.9.0", + "web3-core-subscriptions": "1.9.0", + "web3-utils": "1.9.0" } }, "web3-core-promievent": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.8.2.tgz", - "integrity": "sha512-nvkJWDVgoOSsolJldN33tKW6bKKRJX3MCPDYMwP5SUFOA/mCzDEoI88N0JFofDTXkh1k7gOqp1pvwi9heuaxGg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.9.0.tgz", + "integrity": "sha512-PHG1Mn23IGwMZhnPDN8dETKypqsFbHfiyRqP+XsVMPmTHkVfzDQTCBU/c2r6hUktBDoGKut5xZQpGfhFk71KbQ==", "dev": true, "requires": { "eventemitter3": "4.0.4" } }, "web3-core-requestmanager": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.8.2.tgz", - "integrity": "sha512-p1d090RYs5Mu7DK1yyc3GCBVZB/03rBtFhYFoS2EruGzOWs/5Q0grgtpwS/DScdRAm8wB8mYEBhY/RKJWF6B2g==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.9.0.tgz", + "integrity": "sha512-hcJ5PCtTIJpj+8qWxoseqlCovDo94JJjTX7dZOLXgwp8ah7E3WRYozhGyZocerx+KebKyg1mCQIhkDpMwjfo9Q==", "dev": true, "requires": { "util": "^0.12.5", - "web3-core-helpers": "1.8.2", - "web3-providers-http": "1.8.2", - "web3-providers-ipc": "1.8.2", - "web3-providers-ws": "1.8.2" + "web3-core-helpers": "1.9.0", + "web3-providers-http": "1.9.0", + "web3-providers-ipc": "1.9.0", + "web3-providers-ws": "1.9.0" } }, "web3-core-subscriptions": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.8.2.tgz", - "integrity": "sha512-vXQogHDmAIQcKpXvGiMddBUeP9lnKgYF64+yQJhPNE5PnWr1sAibXuIPV7mIPihpFr/n/DORRj6Wh1pUv9zaTw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.9.0.tgz", + "integrity": "sha512-MaIo29yz7hTV8X8bioclPDbHFOVuHmnbMv+D3PDH12ceJFJAXGyW8GL5KU1DYyWIj4TD1HM4WknyVA/YWBiiLA==", "dev": true, "requires": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.2" + "web3-core-helpers": "1.9.0" } }, "web3-eth": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.8.2.tgz", - "integrity": "sha512-JoTiWWc4F4TInpbvDUGb0WgDYJsFhuIjJlinc5ByjWD88Gvh+GKLsRjjFdbqe5YtwIGT4NymwoC5LQd1K6u/QQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.9.0.tgz", + "integrity": "sha512-c5gSWk9bLNr6VPATHmZ1n7LTIefIZQnJMzfnvkoBcIFGKJbGmsuRhv6lEXsKdAO/FlqYnSbaw3fOq1fVFiIOFQ==", "dev": true, "requires": { - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-eth-accounts": "1.8.2", - "web3-eth-contract": "1.8.2", - "web3-eth-ens": "1.8.2", - "web3-eth-iban": "1.8.2", - "web3-eth-personal": "1.8.2", - "web3-net": "1.8.2", - "web3-utils": "1.8.2" + "web3-core": "1.9.0", + "web3-core-helpers": "1.9.0", + "web3-core-method": "1.9.0", + "web3-core-subscriptions": "1.9.0", + "web3-eth-abi": "1.9.0", + "web3-eth-accounts": "1.9.0", + "web3-eth-contract": "1.9.0", + "web3-eth-ens": "1.9.0", + "web3-eth-iban": "1.9.0", + "web3-eth-personal": "1.9.0", + "web3-net": "1.9.0", + "web3-utils": "1.9.0" } }, "web3-eth-abi": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.8.2.tgz", - "integrity": "sha512-Om9g3kaRNjqiNPAgKwGT16y+ZwtBzRe4ZJFGjLiSs6v5I7TPNF+rRMWuKnR6jq0azQZDj6rblvKFMA49/k48Og==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.9.0.tgz", + "integrity": "sha512-0BLQ3FKMrzJkA930jOX3fMaybAyubk06HChclLpiR0NWmgWXm1tmBrJdkyRy2ZTZpmfuZc9xTFRfl0yZID1voA==", "dev": true, "requires": { "@ethersproject/abi": "^5.6.3", - "web3-utils": "1.8.2" + "web3-utils": "1.9.0" } }, "web3-eth-accounts": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.8.2.tgz", - "integrity": "sha512-c367Ij63VCz9YdyjiHHWLFtN85l6QghgwMQH2B1eM/p9Y5lTlTX7t/Eg/8+f1yoIStXbk2w/PYM2lk+IkbqdLA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.9.0.tgz", + "integrity": "sha512-VeIZVevmnSll0AC1k5F/y398ZE89d1SRuYk8IewLUhL/tVAsFEsjl2SGgm0+aDcHmgPrkW+qsCJ+C7rWg/N4ZA==", "dev": true, "requires": { "@ethereumjs/common": "2.5.0", @@ -26521,10 +28269,10 @@ "ethereumjs-util": "^7.1.5", "scrypt-js": "^3.0.1", "uuid": "^9.0.0", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-utils": "1.8.2" + "web3-core": "1.9.0", + "web3-core-helpers": "1.9.0", + "web3-core-method": "1.9.0", + "web3-utils": "1.9.0" }, "dependencies": { "eth-lib": { @@ -26547,45 +28295,45 @@ } }, "web3-eth-contract": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.8.2.tgz", - "integrity": "sha512-ID5A25tHTSBNwOPjiXSVzxruz006ULRIDbzWTYIFTp7NJ7vXu/kynKK2ag/ObuTqBpMbobP8nXcA9b5EDkIdQA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.9.0.tgz", + "integrity": "sha512-+j26hpSaEtAdUed0TN5rnc+YZOcjPxMjFX4ZBKatvFkImdbVv/tzTvcHlltubSpgb2ZLyZ89lSL6phKYwd2zNQ==", "dev": true, "requires": { - "@types/bn.js": "^5.1.0", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-utils": "1.8.2" + "@types/bn.js": "^5.1.1", + "web3-core": "1.9.0", + "web3-core-helpers": "1.9.0", + "web3-core-method": "1.9.0", + "web3-core-promievent": "1.9.0", + "web3-core-subscriptions": "1.9.0", + "web3-eth-abi": "1.9.0", + "web3-utils": "1.9.0" } }, "web3-eth-ens": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.8.2.tgz", - "integrity": "sha512-PWph7C/CnqdWuu1+SH4U4zdrK4t2HNt0I4XzPYFdv9ugE8EuojselioPQXsVGvjql+Nt3jDLvQvggPqlMbvwRw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.9.0.tgz", + "integrity": "sha512-LOJZeN+AGe9arhuExnrPPFYQr4WSxXEkpvYIlst/joOEUNLDwfndHnJIK6PI5mXaYSROBtTx6erv+HupzGo7vA==", "dev": true, "requires": { "content-hash": "^2.5.2", "eth-ens-namehash": "2.0.8", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-promievent": "1.8.2", - "web3-eth-abi": "1.8.2", - "web3-eth-contract": "1.8.2", - "web3-utils": "1.8.2" + "web3-core": "1.9.0", + "web3-core-helpers": "1.9.0", + "web3-core-promievent": "1.9.0", + "web3-eth-abi": "1.9.0", + "web3-eth-contract": "1.9.0", + "web3-utils": "1.9.0" } }, "web3-eth-iban": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.8.2.tgz", - "integrity": "sha512-h3vNblDWkWMuYx93Q27TAJz6lhzpP93EiC3+45D6xoz983p6si773vntoQ+H+5aZhwglBtoiBzdh7PSSOnP/xQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.9.0.tgz", + "integrity": "sha512-jPAm77PuEs1kE/UrrBFJdPD2PN42pwfXA0gFuuw35bZezhskYML9W4QCxcqnUtceyEA4FUn7K2qTMuCk+23fog==", "dev": true, "requires": { "bn.js": "^5.2.1", - "web3-utils": "1.8.2" + "web3-utils": "1.9.0" }, "dependencies": { "bn.js": { @@ -26597,79 +28345,79 @@ } }, "web3-eth-personal": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.8.2.tgz", - "integrity": "sha512-Vg4HfwCr7doiUF/RC+Jz0wT4+cYaXcOWMAW2AHIjHX6Z7Xwa8nrURIeQgeEE62qcEHAzajyAdB1u6bJyTfuCXw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.9.0.tgz", + "integrity": "sha512-r9Ldo/luBqJlv1vCUEQnUS+C3a3ZdbYxVHyfDkj6RWMyCqqo8JE41HWE+pfa0RmB1xnGL2g8TbYcHcqItck/qg==", "dev": true, "requires": { "@types/node": "^12.12.6", - "web3-core": "1.8.2", - "web3-core-helpers": "1.8.2", - "web3-core-method": "1.8.2", - "web3-net": "1.8.2", - "web3-utils": "1.8.2" + "web3-core": "1.9.0", + "web3-core-helpers": "1.9.0", + "web3-core-method": "1.9.0", + "web3-net": "1.9.0", + "web3-utils": "1.9.0" } }, "web3-net": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.8.2.tgz", - "integrity": "sha512-1itkDMGmbgb83Dg9nporFes9/fxsU7smJ3oRXlFkg4ZHn8YJyP1MSQFPJWWwSc+GrcCFt4O5IrUTvEkHqE3xag==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.9.0.tgz", + "integrity": "sha512-L+fDZFgrLM5Y15aonl2q6L+RvfaImAngmC0Jv45hV2FJ5IfRT0/2ob9etxZmvEBWvOpbqSvghfOhJIT3XZ37Pg==", "dev": true, "requires": { - "web3-core": "1.8.2", - "web3-core-method": "1.8.2", - "web3-utils": "1.8.2" + "web3-core": "1.9.0", + "web3-core-method": "1.9.0", + "web3-utils": "1.9.0" } }, "web3-providers-http": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.8.2.tgz", - "integrity": "sha512-2xY94IIEQd16+b+vIBF4IC1p7GVaz9q4EUFscvMUjtEq4ru4Atdzjs9GP+jmcoo49p70II0UV3bqQcz0TQfVyQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.9.0.tgz", + "integrity": "sha512-5+dMNDAE0rRFz6SJpfnBqlVi2J5bB/Ivr2SanMt2YUrkxW5t8betZbzVwRkTbwtUvkqgj3xeUQzqpOttiv+IqQ==", "dev": true, "requires": { "abortcontroller-polyfill": "^1.7.3", "cross-fetch": "^3.1.4", "es6-promise": "^4.2.8", - "web3-core-helpers": "1.8.2" + "web3-core-helpers": "1.9.0" } }, "web3-providers-ipc": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.8.2.tgz", - "integrity": "sha512-p6fqKVGFg+WiXGHWnB1hu43PbvPkDHTz4RgoEzbXugv5rtv5zfYLqm8Ba6lrJOS5ks9kGKR21a0y3NzE3u7V4w==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.9.0.tgz", + "integrity": "sha512-cPXU93Du40HCylvjaa5x62DbnGqH+86HpK/+kMcFIzF6sDUBhKpag2tSbYhGbj7GMpfkmDTUiiMLdWnFV6+uBA==", "dev": true, "requires": { "oboe": "2.1.5", - "web3-core-helpers": "1.8.2" + "web3-core-helpers": "1.9.0" } }, "web3-providers-ws": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.8.2.tgz", - "integrity": "sha512-3s/4K+wHgbiN+Zrp9YjMq2eqAF6QGABw7wFftPdx+m5hWImV27/MoIx57c6HffNRqZXmCHnfWWFCNHHsi7wXnA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.9.0.tgz", + "integrity": "sha512-JRVsnQZ7j2k1a2yzBNHe39xqk1ijOv01dfIBFw52VeEkSRzvrOcsPIM/ttSyBuJqt70ntMxXY0ekCrqfleKH/w==", "dev": true, "requires": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.2", + "web3-core-helpers": "1.9.0", "websocket": "^1.0.32" } }, "web3-shh": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.8.2.tgz", - "integrity": "sha512-uZ+3MAoNcaJsXXNCDnizKJ5viBNeHOFYsCbFhV755Uu52FswzTOw6DtE7yK9nYXMtIhiSgi7nwl1RYzP8pystw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.9.0.tgz", + "integrity": "sha512-bIBZlralgz4ICCrwkefB2nPPJWfx28NuHIpjB7d9ADKynElubQuqudYhKtSEkKXACuME/BJm0pIFJcJs/gDnMg==", "dev": true, "requires": { - "web3-core": "1.8.2", - "web3-core-method": "1.8.2", - "web3-core-subscriptions": "1.8.2", - "web3-net": "1.8.2" + "web3-core": "1.9.0", + "web3-core-method": "1.9.0", + "web3-core-subscriptions": "1.9.0", + "web3-net": "1.9.0" } }, "web3-utils": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.2.tgz", - "integrity": "sha512-v7j6xhfLQfY7xQDrUP0BKbaNrmZ2/+egbqP9q3KYmOiPpnvAfol+32slgL0WX/5n8VPvKCK5EZ1HGrAVICSToA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.9.0.tgz", + "integrity": "sha512-p++69rCNNfu2jM9n5+VD/g26l+qkEOQ1m6cfRQCbH8ZRrtquTmrirJMgTmyOoax5a5XRYOuws14aypCOs51pdQ==", "dev": true, "requires": { "bn.js": "^5.2.1", @@ -26759,9 +28507,9 @@ } }, "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "dev": true }, "which-pm": { @@ -26970,9 +28718,9 @@ "dev": true }, "yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "requires": { "cliui": "^8.0.1", From 34d926dd7e8ba8b6fb306d88a319368050a34bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 9 May 2023 18:19:35 +0200 Subject: [PATCH 122/197] Implement extra suggestions from audit review of 4.9 (#4224) --- .../proxy/transparent/TransparentUpgradeableProxy.sol | 2 ++ contracts/utils/Checkpoints.sol | 8 ++++---- contracts/utils/ShortStrings.sol | 10 ++++++++-- scripts/generate/templates/Checkpoints.js | 4 ++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol index 4e2b0c759..e49768ab9 100644 --- a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol @@ -174,6 +174,8 @@ contract TransparentUpgradeableProxy is ERC1967Proxy { /** * @dev Returns the current admin. + * + * CAUTION: This function is deprecated. Use {ERC1967Upgrade-_getAdmin} instead. */ function _admin() internal view virtual returns (address) { return _getAdmin(); diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index 27da4b517..1bb66dec9 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/Checkpoints.sol @@ -234,7 +234,7 @@ library Checkpoints { } /** - * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key. + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none. */ function upperLookup(Trace224 storage self, uint32 key) internal view returns (uint224) { uint256 len = self._checkpoints.length; @@ -243,7 +243,7 @@ library Checkpoints { } /** - * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key. + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none. * * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys). */ @@ -410,7 +410,7 @@ library Checkpoints { } /** - * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key. + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none. */ function upperLookup(Trace160 storage self, uint96 key) internal view returns (uint160) { uint256 len = self._checkpoints.length; @@ -419,7 +419,7 @@ library Checkpoints { } /** - * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key. + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none. * * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys). */ diff --git a/contracts/utils/ShortStrings.sol b/contracts/utils/ShortStrings.sol index 450c7985e..a8904777a 100644 --- a/contracts/utils/ShortStrings.sol +++ b/contracts/utils/ShortStrings.sol @@ -4,13 +4,18 @@ pragma solidity ^0.8.8; import "./StorageSlot.sol"; +// | string | 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | +// | length | 0x BB | type ShortString is bytes32; /** * @dev This library provides functions to convert short memory strings * into a `ShortString` type that can be used as an immutable variable. - * Strings of arbitrary length can be optimized if they are short enough by - * the addition of a storage variable used as fallback. + * + * Strings of arbitrary length can be optimized using this library if + * they are short enough (up to 31 bytes) by packing them with their + * length (1 byte) in a single EVM word (32 bytes). Additionally, a + * fallback mechanism can be used for every other case. * * Usage example: * @@ -32,6 +37,7 @@ type ShortString is bytes32; * ``` */ library ShortStrings { + // Used as an identifier for strings longer than 31 bytes. bytes32 private constant _FALLBACK_SENTINEL = 0x00000000000000000000000000000000000000000000000000000000000000FF; error StringTooLong(string str); diff --git a/scripts/generate/templates/Checkpoints.js b/scripts/generate/templates/Checkpoints.js index 1d79031da..8c9f1f850 100644 --- a/scripts/generate/templates/Checkpoints.js +++ b/scripts/generate/templates/Checkpoints.js @@ -55,7 +55,7 @@ function lowerLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} k } /** - * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key. + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none. */ function upperLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) { uint256 len = self.${opts.checkpointFieldName}.length; @@ -64,7 +64,7 @@ function upperLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} k } /** - * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key. + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none. * * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys). */ From 51294b7480fd13e716207a621ac1d55a6290d56d Mon Sep 17 00:00:00 2001 From: Francisco Date: Tue, 9 May 2023 19:52:23 +0100 Subject: [PATCH 123/197] Make transpilation setup local to this repo (#4041) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- .github/workflows/checks.yml | 27 +- .github/workflows/upgradeable.yml | 31 +- contracts/utils/cryptography/EIP712.sol | 6 +- hardhat.config.js | 5 +- hardhat/env-artifacts.js | 24 ++ hardhat/task-test-get-files.js | 35 ++ package-lock.json | 14 +- package.json | 2 +- scripts/upgradeable/README.md | 21 + scripts/upgradeable/patch-apply.sh | 19 + scripts/upgradeable/patch-save.sh | 18 + scripts/upgradeable/transpile-onto.sh | 44 +++ scripts/upgradeable/transpile.sh | 35 ++ scripts/upgradeable/upgradeable.patch | 481 +++++++++++++++++++++++ test/migrate-imports.test.js | 1 + test/utils/Checkpoints.test.js | 161 ++++---- test/utils/cryptography/EIP712.test.js | 6 +- test/utils/structs/EnumerableMap.test.js | 93 ++--- test/utils/structs/EnumerableSet.test.js | 33 +- 19 files changed, 894 insertions(+), 162 deletions(-) create mode 100644 hardhat/env-artifacts.js create mode 100644 hardhat/task-test-get-files.js create mode 100644 scripts/upgradeable/README.md create mode 100755 scripts/upgradeable/patch-apply.sh create mode 100755 scripts/upgradeable/patch-save.sh create mode 100644 scripts/upgradeable/transpile-onto.sh create mode 100644 scripts/upgradeable/transpile.sh create mode 100644 scripts/upgradeable/upgradeable.patch diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 2d5c15159..ae48e9286 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -14,7 +14,6 @@ concurrency: jobs: lint: - if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -37,19 +36,36 @@ jobs: - name: Check linearisation of the inheritance graph run: npm run test:inheritance - name: Check proceduraly generated contracts are up-to-date - if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' run: npm run test:generation - name: Compare gas costs uses: ./.github/actions/gas-compare with: token: ${{ github.token }} + + tests-upgradeable: + runs-on: ubuntu-latest + env: + FORCE_COLOR: 1 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Include history so patch conflicts are resolved automatically + - name: Set up environment + uses: ./.github/actions/setup + - name: Transpile to upgradeable + run: bash scripts/upgradeable/transpile.sh + - name: Run tests + run: npm run test + env: + NODE_OPTIONS: --max_old_space_size=4096 + - name: Check linearisation of the inheritance graph + run: npm run test:inheritance - name: Check storage layout uses: ./.github/actions/storage-layout with: token: ${{ github.token }} - foundry-tests: - if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' + tests-foundry: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -63,7 +79,6 @@ jobs: run: forge test -vv coverage: - if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -77,7 +92,6 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} slither: - if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -89,7 +103,6 @@ jobs: node-version: 18.15 codespell: - if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/upgradeable.yml b/.github/workflows/upgradeable.yml index a7ed8da88..649596abb 100644 --- a/.github/workflows/upgradeable.yml +++ b/.github/workflows/upgradeable.yml @@ -1,4 +1,4 @@ -name: Upgradeable Trigger +name: transpile upgradeable on: push: @@ -7,17 +7,24 @@ on: - release-v* jobs: - trigger: + transpile: + environment: push-upgradeable runs-on: ubuntu-latest steps: - - id: app - uses: getsentry/action-github-app-token@v2 + - uses: actions/checkout@v3 with: - app_id: ${{ secrets.UPGRADEABLE_APP_ID }} - private_key: ${{ secrets.UPGRADEABLE_APP_PK }} - - run: | - curl -X POST \ - https://api.github.com/repos/OpenZeppelin/openzeppelin-contracts-upgradeable/dispatches \ - -H 'Accept: application/vnd.github.v3+json' \ - -H 'Authorization: token ${{ steps.app.outputs.token }}' \ - -d '{ "event_type": "Update", "client_payload": { "ref": "${{ github.ref }}" } }' + repository: OpenZeppelin/openzeppelin-contracts-upgradeable + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN_UPGRADEABLE }} + - name: Fetch current non-upgradeable branch + run: | + git fetch "https://github.com/${{ github.repository }}.git" "$REF" + git checkout FETCH_HEAD + env: + REF: ${{ github.ref }} + - name: Set up environment + uses: ./.github/actions/setup + - run: bash scripts/git-user-config.sh + - name: Transpile to upgradeable + run: bash scripts/upgradeable/transpile-onto.sh ${{ github.ref_name }} origin/${{ github.ref_name }} + - run: git push origin ${{ github.ref_name }} diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol index 56dfceaf4..6a4e1cad2 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -44,14 +44,14 @@ abstract contract EIP712 is IERC5267 { uint256 private immutable _cachedChainId; address private immutable _cachedThis; + bytes32 private immutable _hashedName; + bytes32 private immutable _hashedVersion; + ShortString private immutable _name; ShortString private immutable _version; string private _nameFallback; string private _versionFallback; - bytes32 private immutable _hashedName; - bytes32 private immutable _hashedVersion; - /** * @dev Initializes the domain separator and parameter caches. * diff --git a/hardhat.config.js b/hardhat.config.js index 32f721b65..639e10f95 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -89,10 +89,11 @@ module.exports = { }, }, exposed: { + initializers: true, exclude: [ 'vendor/**/*', - // overflow clash - 'utils/Timers.sol', + // Exclude Timers from hardhat-exposed because its overloaded functions are not transformed correctly + 'utils/Timers{,Upgradeable}.sol', ], }, docgen: require('./docs/config'), diff --git a/hardhat/env-artifacts.js b/hardhat/env-artifacts.js new file mode 100644 index 000000000..fbbea2e2d --- /dev/null +++ b/hardhat/env-artifacts.js @@ -0,0 +1,24 @@ +const { HardhatError } = require('hardhat/internal/core/errors'); + +// Modifies `artifacts.require(X)` so that instead of X it loads the XUpgradeable contract. +// This allows us to run the same test suite on both the original and the transpiled and renamed Upgradeable contracts. + +extendEnvironment(env => { + const artifactsRequire = env.artifacts.require; + + env.artifacts.require = name => { + for (const suffix of ['UpgradeableWithInit', 'Upgradeable', '']) { + try { + return artifactsRequire(name + suffix); + } catch (e) { + // HH700: Artifact not found - from https://hardhat.org/hardhat-runner/docs/errors#HH700 + if (HardhatError.isHardhatError(e) && e.number === 700 && suffix !== '') { + continue; + } else { + throw e; + } + } + } + throw new Error('Unreachable'); + }; +}); diff --git a/hardhat/task-test-get-files.js b/hardhat/task-test-get-files.js new file mode 100644 index 000000000..896bf1274 --- /dev/null +++ b/hardhat/task-test-get-files.js @@ -0,0 +1,35 @@ +const { internalTask } = require('hardhat/config'); +const { TASK_TEST_GET_TEST_FILES } = require('hardhat/builtin-tasks/task-names'); + +// Modifies `hardhat test` to skip the proxy tests after proxies are removed by the transpiler for upgradeability. + +internalTask(TASK_TEST_GET_TEST_FILES).setAction(async ({ testFiles }, { config }) => { + if (testFiles.length !== 0) { + return testFiles; + } + + const globAsync = require('glob'); + const path = require('path'); + const { promises: fs } = require('fs'); + const { promisify } = require('util'); + + const glob = promisify(globAsync); + + const hasProxies = await fs + .access(path.join(config.paths.sources, 'proxy/Proxy.sol')) + .then(() => true) + .catch(() => false); + + return await glob(path.join(config.paths.tests, '**/*.js'), { + ignore: hasProxies + ? [] + : [ + 'proxy/beacon/BeaconProxy.test.js', + 'proxy/beacon/UpgradeableBeacon.test.js', + 'proxy/ERC1967/ERC1967Proxy.test.js', + 'proxy/transparent/ProxyAdmin.test.js', + 'proxy/transparent/TransparentUpgradeableProxy.test.js', + 'proxy/utils/UUPSUpgradeable.test.js', + ].map(p => path.join(config.paths.tests, p)), + }); +}); diff --git a/package-lock.json b/package-lock.json index 2219b08f2..6611223fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "glob": "^8.0.3", "graphlib": "^2.1.8", "hardhat": "^2.9.1", - "hardhat-exposed": "^0.3.2", + "hardhat-exposed": "^0.3.3", "hardhat-gas-reporter": "^1.0.4", "hardhat-ignore-warnings": "^0.2.0", "keccak256": "^1.0.2", @@ -8269,9 +8269,9 @@ } }, "node_modules/hardhat-exposed": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.2.tgz", - "integrity": "sha512-qhi60I2bfjoPZwKgrY7BIpuUiBE7aC/bHN2MzHxPcZdxaeFnjKJ50n59LE7yK3GK2qYzE8DMjzqfnH6SlKPUjw==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.3.tgz", + "integrity": "sha512-AFBM3IUlSU9wkt3886kYbCSzZteB4OQt0ciehPUrCgY/Tazn6g2cxsmoIZT8O8va1R2PtXd9PRC4KZ6WiSVXOw==", "dev": true, "dependencies": { "micromatch": "^4.0.4", @@ -22857,9 +22857,9 @@ } }, "hardhat-exposed": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.2.tgz", - "integrity": "sha512-qhi60I2bfjoPZwKgrY7BIpuUiBE7aC/bHN2MzHxPcZdxaeFnjKJ50n59LE7yK3GK2qYzE8DMjzqfnH6SlKPUjw==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.3.tgz", + "integrity": "sha512-AFBM3IUlSU9wkt3886kYbCSzZteB4OQt0ciehPUrCgY/Tazn6g2cxsmoIZT8O8va1R2PtXd9PRC4KZ6WiSVXOw==", "dev": true, "requires": { "micromatch": "^4.0.4", diff --git a/package.json b/package.json index a3b98fe37..8ef2738b5 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "glob": "^8.0.3", "graphlib": "^2.1.8", "hardhat": "^2.9.1", - "hardhat-exposed": "^0.3.2", + "hardhat-exposed": "^0.3.3", "hardhat-gas-reporter": "^1.0.4", "hardhat-ignore-warnings": "^0.2.0", "keccak256": "^1.0.2", diff --git a/scripts/upgradeable/README.md b/scripts/upgradeable/README.md new file mode 100644 index 000000000..2309f9e10 --- /dev/null +++ b/scripts/upgradeable/README.md @@ -0,0 +1,21 @@ +The upgradeable variant of OpenZeppelin Contracts is automatically generated from the original Solidity code. We call this process "transpilation" and it is implemented by our [Upgradeability Transpiler](https://github.com/OpenZeppelin/openzeppelin-transpiler/). + +When the `master` branch or `release-v*` branches are updated, the code is transpiled and pushed to [OpenZeppelin/openzeppelin-contracts-upgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable) by the `upgradeable.yml` workflow. + +## `transpile.sh` + +Applies patches and invokes the transpiler with the command line flags we need for our requirements (for example, excluding certain files). + +## `transpile-onto.sh` + +``` +bash scripts/upgradeable/transpile-onto.sh [] +``` + +Transpiles the contents of the current git branch and commits the result as a new commit on branch ``. If branch `` doesn't exist, it will copy the commit history of `[]` (this is used in GitHub Actions, but is usually not necessary locally). + +## `patch-apply.sh` & `patch-save.sh` + +Some of the upgradeable contract variants require ad-hoc changes that are not implemented by the transpiler. These changes are implemented by patches stored in `upgradeable.patch` in this directory. `patch-apply.sh` applies these patches. + +If the patches fail to apply due to changes in the repo, the conflicts have to be resolved manually. Once fixed, `patch-save.sh` will take the changes staged in Git and update `upgradeable.patch` to match. diff --git a/scripts/upgradeable/patch-apply.sh b/scripts/upgradeable/patch-apply.sh new file mode 100755 index 000000000..d9e17589b --- /dev/null +++ b/scripts/upgradeable/patch-apply.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")" +PATCH="$DIRNAME/upgradeable.patch" + +error() { + echo Error: "$*" >&2 + exit 1 +} + +if ! git diff-files --quiet ":!$PATCH" || ! git diff-index --quiet HEAD ":!$PATCH"; then + error "Repository must have no staged or unstaged changes" +fi + +if ! git apply -3 "$PATCH"; then + error "Fix conflicts and run $DIRNAME/patch-save.sh" +fi diff --git a/scripts/upgradeable/patch-save.sh b/scripts/upgradeable/patch-save.sh new file mode 100755 index 000000000..111e6f157 --- /dev/null +++ b/scripts/upgradeable/patch-save.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")" +PATCH="$DIRNAME/upgradeable.patch" + +error() { + echo Error: "$*" >&2 + exit 1 +} + +if ! git diff-files --quiet ":!$PATCH"; then + error "Unstaged changes. Stage to include in patch or temporarily stash." +fi + +git diff-index --cached --patch --output="$PATCH" HEAD +git restore --staged --worktree ":!$PATCH" diff --git a/scripts/upgradeable/transpile-onto.sh b/scripts/upgradeable/transpile-onto.sh new file mode 100644 index 000000000..a966a9b9a --- /dev/null +++ b/scripts/upgradeable/transpile-onto.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [ $# -lt 1 ]; then + echo "usage: bash $0 []" >&2 + exit 1 +fi + +set -x + +target="$1" +base="${2-}" + +bash scripts/upgradeable/transpile.sh + +commit="$(git rev-parse --short HEAD)" +branch="$(git rev-parse --abbrev-ref HEAD)" + +git add contracts + +# detach from the current branch to avoid making changes to it +git checkout --quiet --detach + +# switch to the target branch, creating it if necessary +if git rev-parse -q --verify "$target"; then + # if the branch exists, make it the current HEAD without checking out its contents + git reset --soft "$target" + git checkout "$target" +else + # if the branch doesn't exist, create it as an orphan and check it out + git checkout --orphan "$target" + if [ -n "$base" ] && git rev-parse -q --verify "$base"; then + # if base was specified and it exists, set it as the branch history + git reset --soft "$base" + fi +fi + +# commit if there are changes to commit +if ! git diff --quiet --cached; then + git commit -m "Transpile $commit" +fi + +git checkout "$branch" diff --git a/scripts/upgradeable/transpile.sh b/scripts/upgradeable/transpile.sh new file mode 100644 index 000000000..e3aa31cc0 --- /dev/null +++ b/scripts/upgradeable/transpile.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +set -euo pipefail -x + +DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")" + +bash "$DIRNAME/patch-apply.sh" + +npm run clean +npm run compile + +build_info=($(jq -r '.input.sources | keys | if any(test("^contracts/mocks/.*\\bunreachable\\b")) then empty else input_filename end' artifacts/build-info/*)) +build_info_num=${#build_info[@]} + +if [ $build_info_num -ne 1 ]; then + echo "found $build_info_num relevant build info files but expected just 1" + exit 1 +fi + +# -D: delete original and excluded files +# -b: use this build info file +# -i: use included Initializable +# -x: exclude proxy-related contracts with a few exceptions +# -p: emit public initializer +npx @openzeppelin/upgrade-safe-transpiler@latest -D \ + -b "$build_info" \ + -i contracts/proxy/utils/Initializable.sol \ + -x 'contracts-exposed/**/*' \ + -x 'contracts/proxy/**/*' \ + -x '!contracts/proxy/Clones.sol' \ + -x '!contracts/proxy/ERC1967/ERC1967Storage.sol' \ + -x '!contracts/proxy/ERC1967/ERC1967Upgrade.sol' \ + -x '!contracts/proxy/utils/UUPSUpgradeable.sol' \ + -x '!contracts/proxy/beacon/IBeacon.sol' \ + -p 'contracts/**/presets/**/*' diff --git a/scripts/upgradeable/upgradeable.patch b/scripts/upgradeable/upgradeable.patch new file mode 100644 index 000000000..e1988c8e9 --- /dev/null +++ b/scripts/upgradeable/upgradeable.patch @@ -0,0 +1,481 @@ +diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md +deleted file mode 100644 +index 2797a088..00000000 +--- a/.github/ISSUE_TEMPLATE/bug_report.md ++++ /dev/null +@@ -1,21 +0,0 @@ +---- +-name: Bug report +-about: Report a bug in OpenZeppelin Contracts +- +---- +- +- +- +- +- +-**💻 Environment** +- +- +- +-**📝 Details** +- +- +- +-**🔢 Code to reproduce bug** +- +- +diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml +index 4018cef2..d343a53d 100644 +--- a/.github/ISSUE_TEMPLATE/config.yml ++++ b/.github/ISSUE_TEMPLATE/config.yml +@@ -1,4 +1,8 @@ ++blank_issues_enabled: false + contact_links: ++ - name: Bug Reports & Feature Requests ++ url: https://github.com/OpenZeppelin/openzeppelin-contracts/issues/new/choose ++ about: Visit the OpenZeppelin Contracts repository + - name: Questions & Support Requests + url: https://forum.openzeppelin.com/c/support/contracts/18 + about: Ask in the OpenZeppelin Forum +diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md +deleted file mode 100644 +index ff596b0c..00000000 +--- a/.github/ISSUE_TEMPLATE/feature_request.md ++++ /dev/null +@@ -1,14 +0,0 @@ +---- +-name: Feature request +-about: Suggest an idea for OpenZeppelin Contracts +- +---- +- +-**🧐 Motivation** +- +- +-**📝 Details** +- +- +- +- +diff --git a/README.md b/README.md +index 9fc95518..53130e3c 100644 +--- a/README.md ++++ b/README.md +@@ -16,17 +16,20 @@ + + :building_construction: **Want to scale your decentralized application?** Check out [OpenZeppelin Defender](https://openzeppelin.com/defender) — a secure platform for automating and monitoring your operations. + ++> **Note** ++> You are looking at the upgradeable variant of OpenZeppelin Contracts. Be sure to review the documentation on [Using OpenZeppelin Contracts with Upgrades](https://docs.openzeppelin.com/contracts/4.x/upgradeable). ++ + ## Overview + + ### Installation + + ``` +-$ npm install @openzeppelin/contracts ++$ npm install @openzeppelin/contracts-upgradeable + ``` + + OpenZeppelin Contracts features a [stable API](https://docs.openzeppelin.com/contracts/releases-stability#api-stability), which means that your contracts won't break unexpectedly when upgrading to a newer minor version. + +-An alternative to npm is to use the GitHub repository (`openzeppelin/openzeppelin-contracts`) to retrieve the contracts. When doing this, make sure to specify the tag for a release such as `v4.5.0`, instead of using the `master` branch. ++An alternative to npm is to use the GitHub repository (`openzeppelin/openzeppelin-contracts-upgradeable`) to retrieve the contracts. When doing this, make sure to specify the tag for a release such as `v4.5.0`, instead of using the `master` branch. + + ### Usage + +@@ -35,10 +38,11 @@ Once installed, you can use the contracts in the library by importing them: + ```solidity + pragma solidity ^0.8.0; + +-import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; ++import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; + +-contract MyCollectible is ERC721 { +- constructor() ERC721("MyCollectible", "MCO") { ++contract MyCollectible is ERC721Upgradeable { ++ function initialize() initializer public { ++ __ERC721_init("MyCollectible", "MCO"); + } + } + ``` +diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol +index fe67eb54..d26ea4e1 100644 +--- a/contracts/finance/VestingWallet.sol ++++ b/contracts/finance/VestingWallet.sol +@@ -15,6 +15,8 @@ import "../utils/Context.sol"; + * Any token transferred to this contract will follow the vesting schedule as if they were locked from the beginning. + * Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly) + * be immediately releasable. ++ * ++ * @custom:storage-size 52 + */ + contract VestingWallet is Context { + event EtherReleased(uint256 amount); +diff --git a/contracts/governance/TimelockControllerWith46Migration.sol b/contracts/governance/TimelockControllerWith46Migration.sol +new file mode 100644 +index 00000000..3315e7bd +--- /dev/null ++++ b/contracts/governance/TimelockControllerWith46Migration.sol +@@ -0,0 +1,39 @@ ++// SPDX-License-Identifier: MIT ++// OpenZeppelin Contracts v4.6.0 (governance/TimelockControllerWith46Migration.sol) ++ ++pragma solidity ^0.8.0; ++ ++import "./TimelockController.sol"; ++ ++/** ++ * @dev Extension of the TimelockController that includes an additional ++ * function to migrate from OpenZeppelin Upgradeable Contracts <4.6 to >=4.6. ++ * ++ * This migration is necessary to setup administration rights over the new ++ * `CANCELLER_ROLE`. ++ * ++ * The migration is trustless and can be performed by anyone. ++ * ++ * _Available since v4.6._ ++ */ ++contract TimelockControllerWith46Migration is TimelockController { ++ constructor( ++ uint256 minDelay, ++ address[] memory proposers, ++ address[] memory executors, ++ address admin ++ ) TimelockController(minDelay, proposers, executors, admin) {} ++ ++ /** ++ * @dev Migration function. Running it is necessary for upgradeable ++ * instances that were initially setup with code <4.6 and that upgraded ++ * to >=4.6. ++ */ ++ function migrateTo46() public virtual { ++ require( ++ getRoleAdmin(PROPOSER_ROLE) == TIMELOCK_ADMIN_ROLE && getRoleAdmin(CANCELLER_ROLE) == DEFAULT_ADMIN_ROLE, ++ "TimelockController: already migrated" ++ ); ++ _setRoleAdmin(CANCELLER_ROLE, TIMELOCK_ADMIN_ROLE); ++ } ++} +diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol +index 64431711..885f0e42 100644 +--- a/contracts/governance/extensions/GovernorVotes.sol ++++ b/contracts/governance/extensions/GovernorVotes.sol +@@ -10,6 +10,8 @@ import "../../interfaces/IERC5805.sol"; + * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token, or since v4.5 an {ERC721Votes} token. + * + * _Available since v4.3._ ++ * ++ * @custom:storage-size 51 + */ + abstract contract GovernorVotes is Governor { + IERC5805 public immutable token; +diff --git a/contracts/governance/extensions/GovernorVotesComp.sol b/contracts/governance/extensions/GovernorVotesComp.sol +index 17250ad7..1d26b72e 100644 +--- a/contracts/governance/extensions/GovernorVotesComp.sol ++++ b/contracts/governance/extensions/GovernorVotesComp.sol +@@ -10,6 +10,8 @@ import "../../token/ERC20/extensions/ERC20VotesComp.sol"; + * @dev Extension of {Governor} for voting weight extraction from a Comp token. + * + * _Available since v4.3._ ++ * ++ * @custom:storage-size 51 + */ + abstract contract GovernorVotesComp is Governor { + ERC20VotesComp public immutable token; +diff --git a/contracts/package.json b/contracts/package.json +index 55e70b17..ceefb984 100644 +--- a/contracts/package.json ++++ b/contracts/package.json +@@ -1,5 +1,5 @@ + { +- "name": "@openzeppelin/contracts", ++ "name": "@openzeppelin/contracts-upgradeable", + "description": "Secure Smart Contract library for Solidity", + "version": "4.8.2", + "files": [ +@@ -13,7 +13,7 @@ + }, + "repository": { + "type": "git", +- "url": "https://github.com/OpenZeppelin/openzeppelin-contracts.git" ++ "url": "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable.git" + }, + "keywords": [ + "solidity", +diff --git a/contracts/security/PullPayment.sol b/contracts/security/PullPayment.sol +index 65b4980f..f336592e 100644 +--- a/contracts/security/PullPayment.sol ++++ b/contracts/security/PullPayment.sol +@@ -22,6 +22,8 @@ import "../utils/escrow/Escrow.sol"; + * To use, derive from the `PullPayment` contract, and use {_asyncTransfer} + * instead of Solidity's `transfer` function. Payees can query their due + * payments with {payments}, and retrieve them with {withdrawPayments}. ++ * ++ * @custom:storage-size 51 + */ + abstract contract PullPayment { + Escrow private immutable _escrow; +diff --git a/contracts/token/ERC20/extensions/ERC20Capped.sol b/contracts/token/ERC20/extensions/ERC20Capped.sol +index 16f830d1..9ef98148 100644 +--- a/contracts/token/ERC20/extensions/ERC20Capped.sol ++++ b/contracts/token/ERC20/extensions/ERC20Capped.sol +@@ -7,6 +7,8 @@ import "../ERC20.sol"; + + /** + * @dev Extension of {ERC20} that adds a cap to the supply of tokens. ++ * ++ * @custom:storage-size 51 + */ + abstract contract ERC20Capped is ERC20 { + uint256 private immutable _cap; +diff --git a/contracts/token/ERC20/extensions/ERC20Permit.sol b/contracts/token/ERC20/extensions/ERC20Permit.sol +index a357199b..9dc8e894 100644 +--- a/contracts/token/ERC20/extensions/ERC20Permit.sol ++++ b/contracts/token/ERC20/extensions/ERC20Permit.sol +@@ -18,6 +18,8 @@ import "../../../utils/Counters.sol"; + * need to send a transaction, and thus is not required to hold Ether at all. + * + * _Available since v3.4._ ++ * ++ * @custom:storage-size 51 + */ + abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712 { + using Counters for Counters.Counter; +diff --git a/contracts/token/ERC20/extensions/ERC20Wrapper.sol b/contracts/token/ERC20/extensions/ERC20Wrapper.sol +index bfe782e4..7264fe32 100644 +--- a/contracts/token/ERC20/extensions/ERC20Wrapper.sol ++++ b/contracts/token/ERC20/extensions/ERC20Wrapper.sol +@@ -14,6 +14,8 @@ import "../utils/SafeERC20.sol"; + * wrapping of an existing "basic" ERC20 into a governance token. + * + * _Available since v4.2._ ++ * ++ * @custom:storage-size 51 + */ + abstract contract ERC20Wrapper is ERC20 { + IERC20 private immutable _underlying; +diff --git a/contracts/token/ERC20/utils/TokenTimelock.sol b/contracts/token/ERC20/utils/TokenTimelock.sol +index ed855b7b..3d30f59d 100644 +--- a/contracts/token/ERC20/utils/TokenTimelock.sol ++++ b/contracts/token/ERC20/utils/TokenTimelock.sol +@@ -11,6 +11,8 @@ import "./SafeERC20.sol"; + * + * Useful for simple vesting schedules like "advisors get all of their tokens + * after 1 year". ++ * ++ * @custom:storage-size 53 + */ + contract TokenTimelock { + using SafeERC20 for IERC20; +diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol +index 6a4e1cad..55d8eced 100644 +--- a/contracts/utils/cryptography/EIP712.sol ++++ b/contracts/utils/cryptography/EIP712.sol +@@ -4,7 +4,6 @@ + pragma solidity ^0.8.8; + + import "./ECDSA.sol"; +-import "../ShortStrings.sol"; + import "../../interfaces/IERC5267.sol"; + + /** +@@ -30,27 +29,19 @@ import "../../interfaces/IERC5267.sol"; + * + * _Available since v3.4._ + * +- * @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment ++ * @custom:storage-size 52 + */ + abstract contract EIP712 is IERC5267 { +- using ShortStrings for *; +- + bytes32 private constant _TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + +- // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to +- // invalidate the cached domain separator if the chain id changes. +- bytes32 private immutable _cachedDomainSeparator; +- uint256 private immutable _cachedChainId; +- address private immutable _cachedThis; +- ++ /// @custom:oz-renamed-from _HASHED_NAME + bytes32 private immutable _hashedName; ++ /// @custom:oz-renamed-from _HASHED_VERSION + bytes32 private immutable _hashedVersion; + +- ShortString private immutable _name; +- ShortString private immutable _version; +- string private _nameFallback; +- string private _versionFallback; ++ string private _name; ++ string private _version; + + /** + * @dev Initializes the domain separator and parameter caches. +@@ -65,29 +56,23 @@ abstract contract EIP712 is IERC5267 { + * contract upgrade]. + */ + constructor(string memory name, string memory version) { +- _name = name.toShortStringWithFallback(_nameFallback); +- _version = version.toShortStringWithFallback(_versionFallback); +- _hashedName = keccak256(bytes(name)); +- _hashedVersion = keccak256(bytes(version)); +- +- _cachedChainId = block.chainid; +- _cachedDomainSeparator = _buildDomainSeparator(); +- _cachedThis = address(this); ++ _name = name; ++ _version = version; ++ ++ // Reset prior values in storage if upgrading ++ _hashedName = 0; ++ _hashedVersion = 0; + } + + /** + * @dev Returns the domain separator for the current chain. + */ + function _domainSeparatorV4() internal view returns (bytes32) { +- if (address(this) == _cachedThis && block.chainid == _cachedChainId) { +- return _cachedDomainSeparator; +- } else { +- return _buildDomainSeparator(); +- } ++ return _buildDomainSeparator(); + } + + function _buildDomainSeparator() private view returns (bytes32) { +- return keccak256(abi.encode(_TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this))); ++ return keccak256(abi.encode(_TYPE_HASH, _EIP712NameHash(), _EIP712VersionHash(), block.chainid, address(this))); + } + + /** +@@ -129,14 +114,80 @@ abstract contract EIP712 is IERC5267 { + uint256[] memory extensions + ) + { ++ // If the hashed name and version in storage are non-zero, the contract hasn't been properly initialized ++ // and the EIP712 domain is not reliable, as it will be missing name and version. ++ require(_hashedName == 0 && _hashedVersion == 0, "EIP712: Uninitialized"); ++ + return ( + hex"0f", // 01111 +- _name.toStringWithFallback(_nameFallback), +- _version.toStringWithFallback(_versionFallback), ++ _EIP712Name(), ++ _EIP712Version(), + block.chainid, + address(this), + bytes32(0), + new uint256[](0) + ); + } ++ ++ /** ++ * @dev The name parameter for the EIP712 domain. ++ * ++ * NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs ++ * are a concern. ++ */ ++ function _EIP712Name() internal virtual view returns (string memory) { ++ return _name; ++ } ++ ++ /** ++ * @dev The version parameter for the EIP712 domain. ++ * ++ * NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs ++ * are a concern. ++ */ ++ function _EIP712Version() internal virtual view returns (string memory) { ++ return _version; ++ } ++ ++ /** ++ * @dev The hash of the name parameter for the EIP712 domain. ++ * ++ * NOTE: In previous versions this function was virtual. In this version you should override `_EIP712Name` instead. ++ */ ++ function _EIP712NameHash() internal view returns (bytes32) { ++ string memory name = _EIP712Name(); ++ if (bytes(name).length > 0) { ++ return keccak256(bytes(name)); ++ } else { ++ // If the name is empty, the contract may have been upgraded without initializing the new storage. ++ // We return the name hash in storage if non-zero, otherwise we assume the name is empty by design. ++ bytes32 hashedName = _hashedName; ++ if (hashedName != 0) { ++ return hashedName; ++ } else { ++ return keccak256(""); ++ } ++ } ++ } ++ ++ /** ++ * @dev The hash of the version parameter for the EIP712 domain. ++ * ++ * NOTE: In previous versions this function was virtual. In this version you should override `_EIP712Version` instead. ++ */ ++ function _EIP712VersionHash() internal view returns (bytes32) { ++ string memory version = _EIP712Version(); ++ if (bytes(version).length > 0) { ++ return keccak256(bytes(version)); ++ } else { ++ // If the version is empty, the contract may have been upgraded without initializing the new storage. ++ // We return the version hash in storage if non-zero, otherwise we assume the version is empty by design. ++ bytes32 hashedVersion = _hashedVersion; ++ if (hashedVersion != 0) { ++ return hashedVersion; ++ } else { ++ return keccak256(""); ++ } ++ } ++ } + } +diff --git a/package.json b/package.json +index 8458dd61..b4672240 100644 +--- a/package.json ++++ b/package.json +@@ -36,7 +36,7 @@ + }, + "repository": { + "type": "git", +- "url": "https://github.com/OpenZeppelin/openzeppelin-contracts.git" ++ "url": "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable.git" + }, + "keywords": [ + "solidity", +diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js +index 54a4e772..ba4602ed 100644 +--- a/test/utils/cryptography/EIP712.test.js ++++ b/test/utils/cryptography/EIP712.test.js +@@ -47,26 +47,6 @@ contract('EIP712', function (accounts) { + const rebuildDomain = await getDomain(this.eip712); + expect(mapValues(rebuildDomain, String)).to.be.deep.equal(mapValues(this.domain, String)); + }); +- +- if (shortOrLong === 'short') { +- // Long strings are in storage, and the proxy will not be properly initialized unless +- // the upgradeable contract variant is used and the initializer is invoked. +- +- it('adjusts when behind proxy', async function () { +- const factory = await Clones.new(); +- const cloneReceipt = await factory.$clone(this.eip712.address); +- const cloneAddress = cloneReceipt.logs.find(({ event }) => event === 'return$clone').args.instance; +- const clone = new EIP712Verifier(cloneAddress); +- +- const cloneDomain = { ...this.domain, verifyingContract: clone.address }; +- +- const reportedDomain = await getDomain(clone); +- expect(mapValues(reportedDomain, String)).to.be.deep.equal(mapValues(cloneDomain, String)); +- +- const expectedSeparator = await domainSeparator(cloneDomain); +- expect(await clone.$_domainSeparatorV4()).to.equal(expectedSeparator); +- }); +- } + }); + + it('hash digest', async function () { diff --git a/test/migrate-imports.test.js b/test/migrate-imports.test.js index 7ec9a97ab..5a5c37a23 100644 --- a/test/migrate-imports.test.js +++ b/test/migrate-imports.test.js @@ -13,6 +13,7 @@ describe('migrate-imports.js', function () { try { await fs.access(path.join('contracts', p), F_OK); } catch (e) { + if (p.startsWith('proxy/')) continue; // excluded from transpilation of upgradeable contracts await fs.access(path.join('contracts', getUpgradeablePath(p)), F_OK); } } diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js index 48a00e31a..f395663be 100644 --- a/test/utils/Checkpoints.test.js +++ b/test/utils/Checkpoints.test.js @@ -6,28 +6,48 @@ const { batchInBlock } = require('../helpers/txpool'); const $Checkpoints = artifacts.require('$Checkpoints'); +// The library name may be 'Checkpoints' or 'CheckpointsUpgradeable' +const libraryName = $Checkpoints._json.contractName.replace(/^\$/, ''); + +const traceLengths = [160, 224]; + const first = array => (array.length ? array[0] : undefined); const last = array => (array.length ? array[array.length - 1] : undefined); contract('Checkpoints', function () { beforeEach(async function () { this.mock = await $Checkpoints.new(); + + this.methods = { trace: {} }; + this.methods.history = { + latest: (...args) => this.mock.methods[`$latest_${libraryName}_History(uint256)`](0, ...args), + latestCheckpoint: (...args) => this.mock.methods[`$latestCheckpoint_${libraryName}_History(uint256)`](0, ...args), + length: (...args) => this.mock.methods[`$length_${libraryName}_History(uint256)`](0, ...args), + push: (...args) => this.mock.methods['$push(uint256,uint256)'](0, ...args), + getAtBlock: (...args) => this.mock.$getAtBlock(0, ...args), + getAtRecentBlock: (...args) => this.mock.$getAtProbablyRecentBlock(0, ...args), + }; + for (const length of traceLengths) { + this.methods.trace[length] = { + latest: (...args) => this.mock.methods[`$latest_${libraryName}_Trace${length}(uint256)`](0, ...args), + latestCheckpoint: (...args) => + this.mock.methods[`$latestCheckpoint_${libraryName}_Trace${length}(uint256)`](0, ...args), + length: (...args) => this.mock.methods[`$length_${libraryName}_Trace${length}(uint256)`](0, ...args), + push: (...args) => this.mock.methods[`$push(uint256,uint${256 - length},uint${length})`](0, ...args), + lowerLookup: (...args) => this.mock.methods[`$lowerLookup(uint256,uint${256 - length})`](0, ...args), + upperLookup: (...args) => this.mock.methods[`$upperLookup(uint256,uint${256 - length})`](0, ...args), + upperLookupRecent: (...args) => + this.mock.methods[`$upperLookupRecent(uint256,uint${256 - length})`](0, ...args), + }; + } }); describe('History checkpoints', function () { - const latest = (self, ...args) => self.methods['$latest_Checkpoints_History(uint256)'](0, ...args); - const latestCheckpoint = (self, ...args) => - self.methods['$latestCheckpoint_Checkpoints_History(uint256)'](0, ...args); - const push = (self, ...args) => self.methods['$push(uint256,uint256)'](0, ...args); - const getAtBlock = (self, ...args) => self.methods['$getAtBlock(uint256,uint256)'](0, ...args); - const getAtRecentBlock = (self, ...args) => self.methods['$getAtProbablyRecentBlock(uint256,uint256)'](0, ...args); - const getLength = (self, ...args) => self.methods['$length_Checkpoints_History(uint256)'](0, ...args); - describe('without checkpoints', function () { it('returns zero as latest value', async function () { - expect(await latest(this.mock)).to.be.bignumber.equal('0'); + expect(await this.methods.history.latest()).to.be.bignumber.equal('0'); - const ckpt = await latestCheckpoint(this.mock); + const ckpt = await this.methods.history.latestCheckpoint(); expect(ckpt[0]).to.be.equal(false); expect(ckpt[1]).to.be.bignumber.equal('0'); expect(ckpt[2]).to.be.bignumber.equal('0'); @@ -35,49 +55,63 @@ contract('Checkpoints', function () { it('returns zero as past value', async function () { await time.advanceBlock(); - expect(await getAtBlock(this.mock, (await web3.eth.getBlockNumber()) - 1)).to.be.bignumber.equal('0'); - expect(await getAtRecentBlock(this.mock, (await web3.eth.getBlockNumber()) - 1)).to.be.bignumber.equal('0'); + expect(await this.methods.history.getAtBlock((await web3.eth.getBlockNumber()) - 1)).to.be.bignumber.equal('0'); + expect( + await this.methods.history.getAtRecentBlock((await web3.eth.getBlockNumber()) - 1), + ).to.be.bignumber.equal('0'); }); }); describe('with checkpoints', function () { beforeEach('pushing checkpoints', async function () { - this.tx1 = await push(this.mock, 1); - this.tx2 = await push(this.mock, 2); + this.tx1 = await this.methods.history.push(1); + this.tx2 = await this.methods.history.push(2); await time.advanceBlock(); - this.tx3 = await push(this.mock, 3); + this.tx3 = await this.methods.history.push(3); await time.advanceBlock(); await time.advanceBlock(); }); it('returns latest value', async function () { - expect(await latest(this.mock)).to.be.bignumber.equal('3'); + expect(await this.methods.history.latest()).to.be.bignumber.equal('3'); - const ckpt = await latestCheckpoint(this.mock); + const ckpt = await this.methods.history.latestCheckpoint(); expect(ckpt[0]).to.be.equal(true); expect(ckpt[1]).to.be.bignumber.equal(web3.utils.toBN(this.tx3.receipt.blockNumber)); expect(ckpt[2]).to.be.bignumber.equal(web3.utils.toBN('3')); }); - for (const getAtBlockVariant of [getAtBlock, getAtRecentBlock]) { + for (const getAtBlockVariant of ['getAtBlock', 'getAtRecentBlock']) { describe(`lookup: ${getAtBlockVariant}`, function () { it('returns past values', async function () { - expect(await getAtBlockVariant(this.mock, this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await getAtBlockVariant(this.mock, this.tx1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await getAtBlockVariant(this.mock, this.tx2.receipt.blockNumber)).to.be.bignumber.equal('2'); + expect( + await this.methods.history[getAtBlockVariant](this.tx1.receipt.blockNumber - 1), + ).to.be.bignumber.equal('0'); + expect(await this.methods.history[getAtBlockVariant](this.tx1.receipt.blockNumber)).to.be.bignumber.equal( + '1', + ); + expect(await this.methods.history[getAtBlockVariant](this.tx2.receipt.blockNumber)).to.be.bignumber.equal( + '2', + ); // Block with no new checkpoints - expect(await getAtBlockVariant(this.mock, this.tx2.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); - expect(await getAtBlockVariant(this.mock, this.tx3.receipt.blockNumber)).to.be.bignumber.equal('3'); - expect(await getAtBlockVariant(this.mock, this.tx3.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); + expect( + await this.methods.history[getAtBlockVariant](this.tx2.receipt.blockNumber + 1), + ).to.be.bignumber.equal('2'); + expect(await this.methods.history[getAtBlockVariant](this.tx3.receipt.blockNumber)).to.be.bignumber.equal( + '3', + ); + expect( + await this.methods.history[getAtBlockVariant](this.tx3.receipt.blockNumber + 1), + ).to.be.bignumber.equal('3'); }); it('reverts if block number >= current block', async function () { await expectRevert( - getAtBlockVariant(this.mock, await web3.eth.getBlockNumber()), + this.methods.history[getAtBlockVariant](await web3.eth.getBlockNumber()), 'Checkpoints: block not yet mined', ); await expectRevert( - getAtBlockVariant(this.mock, (await web3.eth.getBlockNumber()) + 1), + this.methods.history[getAtBlockVariant]((await web3.eth.getBlockNumber()) + 1), 'Checkpoints: block not yet mined', ); }); @@ -85,58 +119,48 @@ contract('Checkpoints', function () { } it('multiple checkpoints in the same block', async function () { - const lengthBefore = await getLength(this.mock); + const lengthBefore = await this.methods.history.length(); await batchInBlock([ - () => push(this.mock, 8, { gas: 100000 }), - () => push(this.mock, 9, { gas: 100000 }), - () => push(this.mock, 10, { gas: 100000 }), + () => this.methods.history.push(8, { gas: 100000 }), + () => this.methods.history.push(9, { gas: 100000 }), + () => this.methods.history.push(10, { gas: 100000 }), ]); - expect(await getLength(this.mock)).to.be.bignumber.equal(lengthBefore.addn(1)); - expect(await latest(this.mock)).to.be.bignumber.equal('10'); + expect(await this.methods.history.length()).to.be.bignumber.equal(lengthBefore.addn(1)); + expect(await this.methods.history.latest()).to.be.bignumber.equal('10'); }); it('more than 5 checkpoints', async function () { for (let i = 4; i <= 6; i++) { - await push(this.mock, i); + await this.methods.history.push(i); } - expect(await getLength(this.mock)).to.be.bignumber.equal('6'); + expect(await this.methods.history.length()).to.be.bignumber.equal('6'); const block = await web3.eth.getBlockNumber(); // recent - expect(await getAtRecentBlock(this.mock, block - 1)).to.be.bignumber.equal('5'); + expect(await this.methods.history.getAtRecentBlock(block - 1)).to.be.bignumber.equal('5'); // non-recent - expect(await getAtRecentBlock(this.mock, block - 9)).to.be.bignumber.equal('0'); + expect(await this.methods.history.getAtRecentBlock(block - 9)).to.be.bignumber.equal('0'); }); }); }); - for (const length of [160, 224]) { + for (const length of traceLengths) { describe(`Trace${length}`, function () { - const latest = (self, ...args) => self.methods[`$latest_Checkpoints_Trace${length}(uint256)`](0, ...args); - const latestCheckpoint = (self, ...args) => - self.methods[`$latestCheckpoint_Checkpoints_Trace${length}(uint256)`](0, ...args); - const push = (self, ...args) => self.methods[`$push(uint256,uint${256 - length},uint${length})`](0, ...args); - const lowerLookup = (self, ...args) => self.methods[`$lowerLookup(uint256,uint${256 - length})`](0, ...args); - const upperLookup = (self, ...args) => self.methods[`$upperLookup(uint256,uint${256 - length})`](0, ...args); - const upperLookupRecent = (self, ...args) => - self.methods[`$upperLookupRecent(uint256,uint${256 - length})`](0, ...args); - const getLength = (self, ...args) => self.methods[`$length_Checkpoints_Trace${length}(uint256)`](0, ...args); - describe('without checkpoints', function () { it('returns zero as latest value', async function () { - expect(await latest(this.mock)).to.be.bignumber.equal('0'); + expect(await this.methods.trace[length].latest()).to.be.bignumber.equal('0'); - const ckpt = await latestCheckpoint(this.mock); + const ckpt = await this.methods.trace[length].latestCheckpoint(); expect(ckpt[0]).to.be.equal(false); expect(ckpt[1]).to.be.bignumber.equal('0'); expect(ckpt[2]).to.be.bignumber.equal('0'); }); it('lookup returns 0', async function () { - expect(await lowerLookup(this.mock, 0)).to.be.bignumber.equal('0'); - expect(await upperLookup(this.mock, 0)).to.be.bignumber.equal('0'); - expect(await upperLookupRecent(this.mock, 0)).to.be.bignumber.equal('0'); + expect(await this.methods.trace[length].lowerLookup(0)).to.be.bignumber.equal('0'); + expect(await this.methods.trace[length].upperLookup(0)).to.be.bignumber.equal('0'); + expect(await this.methods.trace[length].upperLookupRecent(0)).to.be.bignumber.equal('0'); }); }); @@ -150,46 +174,49 @@ contract('Checkpoints', function () { { key: '11', value: '99' }, ]; for (const { key, value } of this.checkpoints) { - await push(this.mock, key, value); + await this.methods.trace[length].push(key, value); } }); it('length', async function () { - expect(await getLength(this.mock)).to.be.bignumber.equal(this.checkpoints.length.toString()); + expect(await this.methods.trace[length].length()).to.be.bignumber.equal(this.checkpoints.length.toString()); }); it('returns latest value', async function () { - expect(await latest(this.mock)).to.be.bignumber.equal(last(this.checkpoints).value); + expect(await this.methods.trace[length].latest()).to.be.bignumber.equal(last(this.checkpoints).value); - const ckpt = await latestCheckpoint(this.mock); + const ckpt = await this.methods.trace[length].latestCheckpoint(); expect(ckpt[0]).to.be.equal(true); expect(ckpt[1]).to.be.bignumber.equal(last(this.checkpoints).key); expect(ckpt[2]).to.be.bignumber.equal(last(this.checkpoints).value); }); it('cannot push values in the past', async function () { - await expectRevert(push(this.mock, last(this.checkpoints).key - 1, '0'), 'Checkpoint: decreasing keys'); + await expectRevert( + this.methods.trace[length].push(last(this.checkpoints).key - 1, '0'), + 'Checkpoint: decreasing keys', + ); }); it('can update last value', async function () { const newValue = '42'; // check length before the update - expect(await getLength(this.mock)).to.be.bignumber.equal(this.checkpoints.length.toString()); + expect(await this.methods.trace[length].length()).to.be.bignumber.equal(this.checkpoints.length.toString()); // update last key - await push(this.mock, last(this.checkpoints).key, newValue); - expect(await latest(this.mock)).to.be.bignumber.equal(newValue); + await this.methods.trace[length].push(last(this.checkpoints).key, newValue); + expect(await this.methods.trace[length].latest()).to.be.bignumber.equal(newValue); // check that length did not change - expect(await getLength(this.mock)).to.be.bignumber.equal(this.checkpoints.length.toString()); + expect(await this.methods.trace[length].length()).to.be.bignumber.equal(this.checkpoints.length.toString()); }); it('lower lookup', async function () { for (let i = 0; i < 14; ++i) { const value = first(this.checkpoints.filter(x => i <= x.key))?.value || '0'; - expect(await lowerLookup(this.mock, i)).to.be.bignumber.equal(value); + expect(await this.methods.trace[length].lowerLookup(i)).to.be.bignumber.equal(value); } }); @@ -197,8 +224,8 @@ contract('Checkpoints', function () { for (let i = 0; i < 14; ++i) { const value = last(this.checkpoints.filter(x => i >= x.key))?.value || '0'; - expect(await upperLookup(this.mock, i)).to.be.bignumber.equal(value); - expect(await upperLookupRecent(this.mock, i)).to.be.bignumber.equal(value); + expect(await this.methods.trace[length].upperLookup(i)).to.be.bignumber.equal(value); + expect(await this.methods.trace[length].upperLookupRecent(i)).to.be.bignumber.equal(value); } }); @@ -213,13 +240,13 @@ contract('Checkpoints', function () { const allCheckpoints = [].concat(this.checkpoints, moreCheckpoints); for (const { key, value } of moreCheckpoints) { - await push(this.mock, key, value); + await this.methods.trace[length].push(key, value); } for (let i = 0; i < 25; ++i) { const value = last(allCheckpoints.filter(x => i >= x.key))?.value || '0'; - expect(await upperLookup(this.mock, i)).to.be.bignumber.equal(value); - expect(await upperLookupRecent(this.mock, i)).to.be.bignumber.equal(value); + expect(await this.methods.trace[length].upperLookup(i)).to.be.bignumber.equal(value); + expect(await this.methods.trace[length].upperLookupRecent(i)).to.be.bignumber.equal(value); } }); }); diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js index 52e322d3d..54a4e7723 100644 --- a/test/utils/cryptography/EIP712.test.js +++ b/test/utils/cryptography/EIP712.test.js @@ -60,11 +60,11 @@ contract('EIP712', function (accounts) { const cloneDomain = { ...this.domain, verifyingContract: clone.address }; - const expectedSeparator = await domainSeparator(cloneDomain); - expect(await clone.$_domainSeparatorV4()).to.equal(expectedSeparator); - const reportedDomain = await getDomain(clone); expect(mapValues(reportedDomain, String)).to.be.deep.equal(mapValues(cloneDomain, String)); + + const expectedSeparator = await domainSeparator(cloneDomain); + expect(await clone.$_domainSeparatorV4()).to.equal(expectedSeparator); }); } }); diff --git a/test/utils/structs/EnumerableMap.test.js b/test/utils/structs/EnumerableMap.test.js index 548e73775..f540ec99e 100644 --- a/test/utils/structs/EnumerableMap.test.js +++ b/test/utils/structs/EnumerableMap.test.js @@ -14,6 +14,9 @@ const getMethods = ms => { ); }; +// Get the name of the library. In the transpiled code it will be EnumerableMapUpgradeable. +const library = EnumerableMap._json.contractName.replace(/^\$/, ''); + contract('EnumerableMap', function (accounts) { const [accountA, accountB, accountC] = accounts; @@ -41,14 +44,14 @@ contract('EnumerableMap', function (accounts) { getWithMessage: '$get(uint256,address,string)', tryGet: '$tryGet(uint256,address)', remove: '$remove(uint256,address)', - length: '$length_EnumerableMap_AddressToUintMap(uint256)', - at: '$at_EnumerableMap_AddressToUintMap(uint256,uint256)', + length: `$length_${library}_AddressToUintMap(uint256)`, + at: `$at_${library}_AddressToUintMap(uint256,uint256)`, contains: '$contains(uint256,address)', - keys: '$keys_EnumerableMap_AddressToUintMap(uint256)', + keys: `$keys_${library}_AddressToUintMap(uint256)`, }), { - setReturn: 'return$set_EnumerableMap_AddressToUintMap_address_uint256', - removeReturn: 'return$remove_EnumerableMap_AddressToUintMap_address', + setReturn: `return$set_${library}_AddressToUintMap_address_uint256`, + removeReturn: `return$remove_${library}_AddressToUintMap_address`, }, ); }); @@ -61,18 +64,18 @@ contract('EnumerableMap', function (accounts) { constants.ZERO_ADDRESS, getMethods({ set: '$set(uint256,uint256,address)', - get: '$get_EnumerableMap_UintToAddressMap(uint256,uint256)', - getWithMessage: '$get_EnumerableMap_UintToAddressMap(uint256,uint256,string)', - tryGet: '$tryGet_EnumerableMap_UintToAddressMap(uint256,uint256)', - remove: '$remove_EnumerableMap_UintToAddressMap(uint256,uint256)', - length: '$length_EnumerableMap_UintToAddressMap(uint256)', - at: '$at_EnumerableMap_UintToAddressMap(uint256,uint256)', - contains: '$contains_EnumerableMap_UintToAddressMap(uint256,uint256)', - keys: '$keys_EnumerableMap_UintToAddressMap(uint256)', + get: `$get_${library}_UintToAddressMap(uint256,uint256)`, + getWithMessage: `$get_${library}_UintToAddressMap(uint256,uint256,string)`, + tryGet: `$tryGet_${library}_UintToAddressMap(uint256,uint256)`, + remove: `$remove_${library}_UintToAddressMap(uint256,uint256)`, + length: `$length_${library}_UintToAddressMap(uint256)`, + at: `$at_${library}_UintToAddressMap(uint256,uint256)`, + contains: `$contains_${library}_UintToAddressMap(uint256,uint256)`, + keys: `$keys_${library}_UintToAddressMap(uint256)`, }), { - setReturn: 'return$set_EnumerableMap_UintToAddressMap_uint256_address', - removeReturn: 'return$remove_EnumerableMap_UintToAddressMap_uint256', + setReturn: `return$set_${library}_UintToAddressMap_uint256_address`, + removeReturn: `return$remove_${library}_UintToAddressMap_uint256`, }, ); }); @@ -85,18 +88,18 @@ contract('EnumerableMap', function (accounts) { constants.ZERO_BYTES32, getMethods({ set: '$set(uint256,bytes32,bytes32)', - get: '$get_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32)', - getWithMessage: '$get_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32,string)', - tryGet: '$tryGet_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32)', - remove: '$remove_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32)', - length: '$length_EnumerableMap_Bytes32ToBytes32Map(uint256)', - at: '$at_EnumerableMap_Bytes32ToBytes32Map(uint256,uint256)', - contains: '$contains_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32)', - keys: '$keys_EnumerableMap_Bytes32ToBytes32Map(uint256)', + get: `$get_${library}_Bytes32ToBytes32Map(uint256,bytes32)`, + getWithMessage: `$get_${library}_Bytes32ToBytes32Map(uint256,bytes32,string)`, + tryGet: `$tryGet_${library}_Bytes32ToBytes32Map(uint256,bytes32)`, + remove: `$remove_${library}_Bytes32ToBytes32Map(uint256,bytes32)`, + length: `$length_${library}_Bytes32ToBytes32Map(uint256)`, + at: `$at_${library}_Bytes32ToBytes32Map(uint256,uint256)`, + contains: `$contains_${library}_Bytes32ToBytes32Map(uint256,bytes32)`, + keys: `$keys_${library}_Bytes32ToBytes32Map(uint256)`, }), { - setReturn: 'return$set_EnumerableMap_Bytes32ToBytes32Map_bytes32_bytes32', - removeReturn: 'return$remove_EnumerableMap_Bytes32ToBytes32Map_bytes32', + setReturn: `return$set_${library}_Bytes32ToBytes32Map_bytes32_bytes32`, + removeReturn: `return$remove_${library}_Bytes32ToBytes32Map_bytes32`, }, ); }); @@ -109,18 +112,18 @@ contract('EnumerableMap', function (accounts) { new BN('0'), getMethods({ set: '$set(uint256,uint256,uint256)', - get: '$get_EnumerableMap_UintToUintMap(uint256,uint256)', - getWithMessage: '$get_EnumerableMap_UintToUintMap(uint256,uint256,string)', - tryGet: '$tryGet_EnumerableMap_UintToUintMap(uint256,uint256)', - remove: '$remove_EnumerableMap_UintToUintMap(uint256,uint256)', - length: '$length_EnumerableMap_UintToUintMap(uint256)', - at: '$at_EnumerableMap_UintToUintMap(uint256,uint256)', - contains: '$contains_EnumerableMap_UintToUintMap(uint256,uint256)', - keys: '$keys_EnumerableMap_UintToUintMap(uint256)', + get: `$get_${library}_UintToUintMap(uint256,uint256)`, + getWithMessage: `$get_${library}_UintToUintMap(uint256,uint256,string)`, + tryGet: `$tryGet_${library}_UintToUintMap(uint256,uint256)`, + remove: `$remove_${library}_UintToUintMap(uint256,uint256)`, + length: `$length_${library}_UintToUintMap(uint256)`, + at: `$at_${library}_UintToUintMap(uint256,uint256)`, + contains: `$contains_${library}_UintToUintMap(uint256,uint256)`, + keys: `$keys_${library}_UintToUintMap(uint256)`, }), { - setReturn: 'return$set_EnumerableMap_UintToUintMap_uint256_uint256', - removeReturn: 'return$remove_EnumerableMap_UintToUintMap_uint256', + setReturn: `return$set_${library}_UintToUintMap_uint256_uint256`, + removeReturn: `return$remove_${library}_UintToUintMap_uint256`, }, ); }); @@ -133,18 +136,18 @@ contract('EnumerableMap', function (accounts) { new BN('0'), getMethods({ set: '$set(uint256,bytes32,uint256)', - get: '$get_EnumerableMap_Bytes32ToUintMap(uint256,bytes32)', - getWithMessage: '$get_EnumerableMap_Bytes32ToUintMap(uint256,bytes32,string)', - tryGet: '$tryGet_EnumerableMap_Bytes32ToUintMap(uint256,bytes32)', - remove: '$remove_EnumerableMap_Bytes32ToUintMap(uint256,bytes32)', - length: '$length_EnumerableMap_Bytes32ToUintMap(uint256)', - at: '$at_EnumerableMap_Bytes32ToUintMap(uint256,uint256)', - contains: '$contains_EnumerableMap_Bytes32ToUintMap(uint256,bytes32)', - keys: '$keys_EnumerableMap_Bytes32ToUintMap(uint256)', + get: `$get_${library}_Bytes32ToUintMap(uint256,bytes32)`, + getWithMessage: `$get_${library}_Bytes32ToUintMap(uint256,bytes32,string)`, + tryGet: `$tryGet_${library}_Bytes32ToUintMap(uint256,bytes32)`, + remove: `$remove_${library}_Bytes32ToUintMap(uint256,bytes32)`, + length: `$length_${library}_Bytes32ToUintMap(uint256)`, + at: `$at_${library}_Bytes32ToUintMap(uint256,uint256)`, + contains: `$contains_${library}_Bytes32ToUintMap(uint256,bytes32)`, + keys: `$keys_${library}_Bytes32ToUintMap(uint256)`, }), { - setReturn: 'return$set_EnumerableMap_Bytes32ToUintMap_bytes32_uint256', - removeReturn: 'return$remove_EnumerableMap_Bytes32ToUintMap_bytes32', + setReturn: `return$set_${library}_Bytes32ToUintMap_bytes32_uint256`, + removeReturn: `return$remove_${library}_Bytes32ToUintMap_bytes32`, }, ); }); diff --git a/test/utils/structs/EnumerableSet.test.js b/test/utils/structs/EnumerableSet.test.js index 862cb6d9e..3ba9d7ff7 100644 --- a/test/utils/structs/EnumerableSet.test.js +++ b/test/utils/structs/EnumerableSet.test.js @@ -12,6 +12,9 @@ const getMethods = ms => { ); }; +// Get the name of the library. In the transpiled code it will be EnumerableSetUpgradeable. +const library = EnumerableSet._json.contractName.replace(/^\$/, ''); + contract('EnumerableSet', function (accounts) { beforeEach(async function () { this.set = await EnumerableSet.new(); @@ -25,13 +28,13 @@ contract('EnumerableSet', function (accounts) { add: '$add(uint256,bytes32)', remove: '$remove(uint256,bytes32)', contains: '$contains(uint256,bytes32)', - length: '$length_EnumerableSet_Bytes32Set(uint256)', - at: '$at_EnumerableSet_Bytes32Set(uint256,uint256)', - values: '$values_EnumerableSet_Bytes32Set(uint256)', + length: `$length_${library}_Bytes32Set(uint256)`, + at: `$at_${library}_Bytes32Set(uint256,uint256)`, + values: `$values_${library}_Bytes32Set(uint256)`, }), { - addReturn: 'return$add_EnumerableSet_Bytes32Set_bytes32', - removeReturn: 'return$remove_EnumerableSet_Bytes32Set_bytes32', + addReturn: `return$add_${library}_Bytes32Set_bytes32`, + removeReturn: `return$remove_${library}_Bytes32Set_bytes32`, }, ); }); @@ -44,13 +47,13 @@ contract('EnumerableSet', function (accounts) { add: '$add(uint256,address)', remove: '$remove(uint256,address)', contains: '$contains(uint256,address)', - length: '$length_EnumerableSet_AddressSet(uint256)', - at: '$at_EnumerableSet_AddressSet(uint256,uint256)', - values: '$values_EnumerableSet_AddressSet(uint256)', + length: `$length_${library}_AddressSet(uint256)`, + at: `$at_${library}_AddressSet(uint256,uint256)`, + values: `$values_${library}_AddressSet(uint256)`, }), { - addReturn: 'return$add_EnumerableSet_AddressSet_address', - removeReturn: 'return$remove_EnumerableSet_AddressSet_address', + addReturn: `return$add_${library}_AddressSet_address`, + removeReturn: `return$remove_${library}_AddressSet_address`, }, ); }); @@ -63,13 +66,13 @@ contract('EnumerableSet', function (accounts) { add: '$add(uint256,uint256)', remove: '$remove(uint256,uint256)', contains: '$contains(uint256,uint256)', - length: '$length_EnumerableSet_UintSet(uint256)', - at: '$at_EnumerableSet_UintSet(uint256,uint256)', - values: '$values_EnumerableSet_UintSet(uint256)', + length: `$length_${library}_UintSet(uint256)`, + at: `$at_${library}_UintSet(uint256,uint256)`, + values: `$values_${library}_UintSet(uint256)`, }), { - addReturn: 'return$add_EnumerableSet_UintSet_uint256', - removeReturn: 'return$remove_EnumerableSet_UintSet_uint256', + addReturn: `return$add_${library}_UintSet_uint256`, + removeReturn: `return$remove_${library}_UintSet_uint256`, }, ); }); From 0ee84342b72e7874433b47a591d460d3f22ebf8e Mon Sep 17 00:00:00 2001 From: Francisco Date: Tue, 9 May 2023 20:09:52 +0100 Subject: [PATCH 124/197] Add PDF report for v4.9 audit (#4227) --- audits/2023-05-v4.9.pdf | Bin 0 -> 483005 bytes audits/README.md | 1 + 2 files changed, 1 insertion(+) create mode 100644 audits/2023-05-v4.9.pdf diff --git a/audits/2023-05-v4.9.pdf b/audits/2023-05-v4.9.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e7e6d715b04b0fffb92f424b63120a01e2207f72 GIT binary patch literal 483005 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 literal 0 HcmV?d00001 diff --git a/audits/README.md b/audits/README.md index 40098a44d..c629f51d3 100644 --- a/audits/README.md +++ b/audits/README.md @@ -2,6 +2,7 @@ | Date | Version | Commit | Auditor | Scope | Links | | ------------ | ------- | --------- | ------------ | -------------------- | ----------------------------------------------------------- | +| May 2023 | v4.9.0 | `91df66c` | OpenZeppelin | v4.9 Changes | [🔗](./2023-05-v4.9.pdf) | | October 2022 | v4.8.0 | `14f98db` | OpenZeppelin | ERC4626, Checkpoints | [🔗](./2022-10-ERC4626.pdf) [🔗](./2022-10-Checkpoints.pdf) | | October 2018 | v2.0.0 | `dac5bcc` | LevelK | Everything | [🔗](./2018-10.pdf) | | March 2017 | v1.0.4 | `9c5975a` | New Alchemy | Everything | [🔗](./2017-03.md) | From df3f1fc4db6955d0fe4736b6c43d4861b7436cda Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Tue, 9 May 2023 16:37:25 -0300 Subject: [PATCH 125/197] Specify changeset commit manually --- .changeset/tender-needles-dance.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/tender-needles-dance.md b/.changeset/tender-needles-dance.md index d75adec95..04d1784a4 100644 --- a/.changeset/tender-needles-dance.md +++ b/.changeset/tender-needles-dance.md @@ -3,3 +3,5 @@ --- `ERC20Wrapper`: self wrapping and deposit by the wrapper itself are now explicitelly forbiden. + +commit: 3214f6c25 From dff520afae768f1e94471e1ca4e08293d4d8e159 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Tue, 9 May 2023 16:53:55 -0300 Subject: [PATCH 126/197] Specify changeset PRs manually --- .changeset/beige-buses-drop.md | 4 +++- .changeset/curvy-shrimps-enjoy.md | 4 +++- .changeset/curvy-suns-sort.md | 4 +++- .changeset/famous-rules-burn.md | 4 +++- .changeset/funny-rockets-compete.md | 4 +++- .changeset/gold-chicken-clean.md | 4 +++- .changeset/healthy-squids-stare.md | 4 +++- .changeset/lemon-dogs-kiss.md | 4 +++- .changeset/little-kiwis-ring.md | 4 +++- .changeset/pretty-hornets-play.md | 4 +++- .changeset/tame-ladybugs-sit.md | 4 +++- 11 files changed, 33 insertions(+), 11 deletions(-) diff --git a/.changeset/beige-buses-drop.md b/.changeset/beige-buses-drop.md index 4566eccb0..ecfd08b35 100644 --- a/.changeset/beige-buses-drop.md +++ b/.changeset/beige-buses-drop.md @@ -2,4 +2,6 @@ 'openzeppelin-solidity': patch --- -`Initializable`: optimize `_disableInitializers` by using `!=` instead of `<`. ([#3787](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3787)) +`Initializable`: optimize `_disableInitializers` by using `!=` instead of `<`. + +pr: #3787 diff --git a/.changeset/curvy-shrimps-enjoy.md b/.changeset/curvy-shrimps-enjoy.md index 4bc410abf..22c2bc54c 100644 --- a/.changeset/curvy-shrimps-enjoy.md +++ b/.changeset/curvy-shrimps-enjoy.md @@ -2,4 +2,6 @@ 'openzeppelin-solidity': minor --- -`ReentrancyGuard`: Add a `_reentrancyGuardEntered` function to expose the guard status. ([#3714](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3714)) +`ReentrancyGuard`: Add a `_reentrancyGuardEntered` function to expose the guard status. + +pr: #3714 diff --git a/.changeset/curvy-suns-sort.md b/.changeset/curvy-suns-sort.md index 97b51fed7..201f45ca7 100644 --- a/.changeset/curvy-suns-sort.md +++ b/.changeset/curvy-suns-sort.md @@ -2,4 +2,6 @@ 'openzeppelin-solidity': patch --- -`Ownable2Step`: make `acceptOwnership` public virtual to enable usecases that require overriding it. ([#3960](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3960)) +`Ownable2Step`: make `acceptOwnership` public virtual to enable usecases that require overriding it. + +pr: #3960 diff --git a/.changeset/famous-rules-burn.md b/.changeset/famous-rules-burn.md index a746dc21d..a97aca0b3 100644 --- a/.changeset/famous-rules-burn.md +++ b/.changeset/famous-rules-burn.md @@ -2,4 +2,6 @@ 'openzeppelin-solidity': minor --- -`EnumerableMap`: add a `keys()` function that returns an array containing all the keys. ([#3920](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3920)) +`EnumerableMap`: add a `keys()` function that returns an array containing all the keys. + +pr: #3920 diff --git a/.changeset/funny-rockets-compete.md b/.changeset/funny-rockets-compete.md index a8c77c619..3f665bc9e 100644 --- a/.changeset/funny-rockets-compete.md +++ b/.changeset/funny-rockets-compete.md @@ -2,4 +2,6 @@ 'openzeppelin-solidity': patch --- -Reformatted codebase with latest version of Prettier Solidity. ([#3898](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3898)) +Reformatted codebase with latest version of Prettier Solidity. + +pr: #3898 diff --git a/.changeset/gold-chicken-clean.md b/.changeset/gold-chicken-clean.md index 0d64fde6d..1353e9c9c 100644 --- a/.changeset/gold-chicken-clean.md +++ b/.changeset/gold-chicken-clean.md @@ -2,4 +2,6 @@ 'openzeppelin-solidity': minor --- -`Strings`: add `equal` method. ([#3774](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3774)) +`Strings`: add `equal` method. + +pr: #3774 diff --git a/.changeset/healthy-squids-stare.md b/.changeset/healthy-squids-stare.md index fad0872e2..9e2c9f3dd 100644 --- a/.changeset/healthy-squids-stare.md +++ b/.changeset/healthy-squids-stare.md @@ -2,4 +2,6 @@ 'openzeppelin-solidity': patch --- -`Math`: optimize `log256` rounding check. ([#3745](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3745)) +`Math`: optimize `log256` rounding check. + +pr: #3745 diff --git a/.changeset/lemon-dogs-kiss.md b/.changeset/lemon-dogs-kiss.md index 976949d2c..5e2787cf1 100644 --- a/.changeset/lemon-dogs-kiss.md +++ b/.changeset/lemon-dogs-kiss.md @@ -2,4 +2,6 @@ 'openzeppelin-solidity': patch --- -`ERC20Votes`: optimize by using unchecked arithmetic. ([#3748](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3748)) +`ERC20Votes`: optimize by using unchecked arithmetic. + +pr: #3748 diff --git a/.changeset/little-kiwis-ring.md b/.changeset/little-kiwis-ring.md index a1cb7bb95..81909a513 100644 --- a/.changeset/little-kiwis-ring.md +++ b/.changeset/little-kiwis-ring.md @@ -2,4 +2,6 @@ 'openzeppelin-solidity': patch --- -`Multicall`: annotate `multicall` function as upgrade safe to not raise a flag for its delegatecall. ([#3961](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3961)) +`Multicall`: annotate `multicall` function as upgrade safe to not raise a flag for its delegatecall. + +pr: #3961 diff --git a/.changeset/pretty-hornets-play.md b/.changeset/pretty-hornets-play.md index 230a53bb2..e7d15c24a 100644 --- a/.changeset/pretty-hornets-play.md +++ b/.changeset/pretty-hornets-play.md @@ -2,4 +2,6 @@ 'openzeppelin-solidity': minor --- -`Strings`: add `toString` method for signed integers. ([#3773](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3773)) +`Strings`: add `toString` method for signed integers. + +pr: #3773 diff --git a/.changeset/tame-ladybugs-sit.md b/.changeset/tame-ladybugs-sit.md index 8a1e416de..4cddc219e 100644 --- a/.changeset/tame-ladybugs-sit.md +++ b/.changeset/tame-ladybugs-sit.md @@ -2,4 +2,6 @@ 'openzeppelin-solidity': patch --- -`MerkleProof`: optimize by using unchecked arithmetic. ([#3745](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3745)) +`MerkleProof`: optimize by using unchecked arithmetic. + +pr: #3745 From d095542fa4e35212e822fca1df166211f9b68186 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Tue, 9 May 2023 17:36:33 -0300 Subject: [PATCH 127/197] Disable code size warnings on exposed contracts --- hardhat.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hardhat.config.js b/hardhat.config.js index 639e10f95..5fd703d06 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -76,6 +76,9 @@ module.exports = { }, }, warnings: { + 'contracts-exposed/**/*': { + 'code-size': 'off', + }, '*': { 'code-size': withOptimizations, 'unused-param': !argv.coverage, // coverage causes unused-param warnings From 9a2e4cb3a71326bc91a177f7b1f184448ccc799f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 May 2023 20:59:22 -0300 Subject: [PATCH 128/197] Update codespell-project/actions-codespell action to v2 (#4229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index ae48e9286..1f03aa38c 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -107,7 +107,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Run CodeSpell - uses: codespell-project/actions-codespell@v1.0 + uses: codespell-project/actions-codespell@v2.0 with: check_filenames: true skip: package-lock.json,*.pdf From e5dbc7435e5995cd539d49fe51a508b48856da64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 10 May 2023 12:59:11 -0600 Subject: [PATCH 129/197] Add final PDF report for v4.9 audit (#4235) --- audits/2023-05-v4.9.pdf | Bin 483005 -> 485395 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/audits/2023-05-v4.9.pdf b/audits/2023-05-v4.9.pdf index e7e6d715b04b0fffb92f424b63120a01e2207f72..21cd7f59307483e93957a3b2c94aa11c0efe40ec 100644 GIT binary patch delta 257244 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%0AkFbk-zM@;TDwre>t|o-6^}I z*ki*@DEp*1rd}O0dj{ES!)+)B205<6fA`qp5ClrZDA~jl6iB3WZIR|p6|+hZ4iM%= zu=H!>Eqz2Ex**_?s8BMWAx7`N>__Pj>6h*w>Zk25k;t1gVJBt`vZ3j>>_6-W2>Vm| zHT$`@ZH!xgMgFS%mC4eTY*A07mj~uvPd3pp(6P`l(Xr7n(y`JpD-JR{2r^6#GF|95 zF%F%y)3MVrbS6+i$5SVq4yTW>%Nsc0C?sYJPw8hcZe$rqwjO1xWZffHC-&}tw@tLI zwavTjxsA9ju}x*1^Q&&se#K}7uv#%&v0E`*v1DL9W5pwu2`}hh?PuL4+t%9_G;XTt z*03yRRU@_ze~Lf~Z|L{$=QVDt>2e>uAcX7BzO26dds$pF=|1Yd;@<5(CHPGcU>9T+

#_f=Odl%uU3ShN$WoV-~BgWxv9dq52ZhEAV7ixE@7 zwSzWd*ypp9!PHuPc55rhFkw#!KEF&$OIKQY35Lbfv<{7;gxB9V=4If*!5$XTON32E zDA&{1XyjLdkNLB}oxh{ju6+X<0$|&3ByhvzDXB+Koc0I&$ip3>jS#Hyf|%6D`TTyL z&+qg3e13l*(9+Tu7nece8VWO0xNgiCFypVDOn(qCeRAAf;Nc6 zQ|cO4x}eva&j#g1XQ3Mnwf(XT)7{fIv$R2iPo`+Kg4RVvDQoMN4GlxF5UJ+bIdfkT zi(^qvrb#AM7EGW0@O}^LWsnh0Xzw&~Q=mef+q-1(dMWfMB+*ogOp-XGsG+O7hj$#Y zyZb6C7mGy(no0&^1gWOHVnKIrA9%|ObGQ?+OJ1xBDSv){;KIc#<0q&|axzV8p9Sua z#Q-iHP6o!N@7Re`CFP69#H2(;C1UbqipW4V>v|H`%4LZ%sb=z|)YsO&)%CE4LD2Mb z%{u~4M!WY?Nd1^$jmO93G!P!1{cw-^cnn*6-yH&Ik2BpC2V9Bbx}` z$?O^64i?_e0C#2-aDNmD+zpt-n2}m40C(Os2mxPExbHbGlj zpcK}+0S6M^x+CXX^q!tUm&fI0*&Dao$BlnJaOZKE%jpaZ50#Y96U!j}8qC=-O?ml( z{(;}+;jX5^Da#<0$IY3yW^j-P+#}N{KL%YMx6kKmZf>JV1K7j?CDb6`eh%uOsOX3s zV>|Cva(O%rqH+^KBvJ~^`~_>?+PVMY`Nk6`E^gVjFK>E{ND@m@=4dhvmm4uc zg>o{zZg($uh4CHqD7#p=<)6AMo}Mevlqbx?P@h7xv7KR{1jupz-ah;K4Lf6%X;Lg6 z2bC@a3(K*oC8hPy!2eVmNW`s&e&qtg{bB$9LnlO{cv_*O6?(Z$pPXELyQL`$9}XZdKmJo8#|iLfhkr7x+dnkCYwv03o*|McEOiW(9u+lZ)yg+m zHo$v@A;8BW;I0QuOqnp;IOR`OxO-TRZST0RNwh-f4zylrV8Z1-ZeePM@AQ-a=qt7e-ThOrDsMQtDt|Hx@QE zb@+U|5g9x$TrSU|MQafg4_V3-)j?+olE6n4&YxYhgzw7#R^bl)3Iv%jX|mz^%{#-7 zAG7cP^aO_eXU<-nFiB4kN`lnkq@EsQ*t+vA4pz~FLJSYT)I`NlhV{0#ciOTlWO6mF zFhaX0rI%xBktjhVR*J+bkw`6(XvJb(lt>*VR*EDEVzEjrP85kuBC%F1R!2p}C&XKC z-nzwb$S9KGHg4Jjl}JKQQ%Q1poXM1X`*ss*tQlEhhQB5%%rITuJ!WG*Aqe)h@l#Vk z;SNtLcnYH$+q1x38v)$;6;bFGM>V;?5O?WPp}tzW-ap)lg40Y}vZoYab>38E;KM5LES8KWesD2X~sk|-8yL{VCaNGlR)L?RU$ z7%G`e85K3=)mPsN1csb2R020) zSfWHSRj<$5@zx=S6SyiLNFF-CD6bfR`xKJakW?a0suqHWduZYusaXn9)z8eGk)!9u z>kE4g4i4PBe)sJ556|rY)P4>zcR02M&uj^v-W)u;{mI3B?>CFIu+kyFdI%Fc`EC zzGE;HWM$1xwN@uvE0eQ|)3Qs`vn#AQmDb$Kti19JYr$*lH~;#B-|$HNZ~yl1iwY}J zGpjOks?)Q}a`MX_JRAhB{n1B{zw%YL+w<37`(J_KuMR!_>&IXH8`j4?e&>H}+;Aj0 zsr0QK=imDkIL}-<=2_hr|mg(>cdYy`{i#wd}GVL zG+Vhfr#dr#wmGffr8RH7_rY(UfWo~ue|nuQyCTIp^W?c}AAJ1rd%yn8o&zV6QwyzG zvodq*jn$d&mCqE4agWvzb4+jS9fA!VB@ee)ztFQfyfA}5$@K*xwe6^{)H@BcF zJ-hUU!u>D%__6?ZLF9hBrSVSdw-EnoZS5v(;wFYHITn(`h1 z;dkW4cWB8Awh?ZSjks>zYE4vS;!+JofcTWAbR-SU_>fIRYH3Qf|$pdi5brh||3Ej9D%Z{CgSdgd#&y3^cm!MMZO@5|x5XCV*C} z_4@qVO>LZyr(oemgh;bp{;xm#B_mEK{05J)MqM@JD>{^)UnLQ%z)~NOw6d;lrI+yy z4-XFw4Gj-J8X6iN3IqavKMSn}u);-=Zv6H*=A7o&W z>?N4(99AZHqXo26z+mhPju)S@>*J- z&%b;3A+cl%4eCMzO(jVr@#{Bi4-7v>0my=yJH+-OQP%Al0OAdMZJ@Kan%btsrr}T# zG2*ykOtj_Dk+Td>JrJ+&?|0PJu9QjC6d+NvSQ1xWv55D=Mm$3BR7H1()8ick^2;(0 zy1J)LpC!izHRY=*V0y`wim;g0TPeroVQ0mu0ue*XiCzir)4m-F%CM>RF`k=?U} zrY&-LLPf>Go}NJz1uR@kUgH`H+Xx5C33DQ_A+NWyv)g1UkjWqj6mn}x?Y8ZE!Alzn zijhl<9X%s>KII-79^SU|aFi$(g<)DSsU|JGptbc5a^rEs$3tU?<+xqDk7H5;IIs~0 zxlC(GDsFDM$NSeuuv&f+fHCIt`!h2tC1Q2h!+q7N4Z;UT)li4swRrJsa=24qBuFGG zTXyw>hrQ^#Ag^brO(JO{{0KV_+;85xKYoG{LMAC4CRb~<>4SqVR5;^jQy67|fPc=M zm%-kbg1k^nn$R$3`JmmwFub>~Ag^|~J(X3whdYF(O2o>!^HvYqoe;4yipB`1m-G9Y zn%fkR?}`#WFD9(|3u{cg5j#nvjCr(U@635Dj zSW2O#2t9!%l0@Q5FR#L`{a^q9AOJ~3K~(SWx1%e}KVQN#cBtO=?88iW#0nF_$baPX z9+9eCsGYk&qwWhlK70O}T9bl9t-?q`R=FxRF17h?2g7FzkNk-6b37t6b$IB&!PBBB zRkXrPlLlOBOinItYH0yCEupz!L>qui=R$_dC@CWx?k)Tg!5iEaUXQUp|Bc(XwT5gA zH-n3s!Wbo*`0~mPj~=7hY-BGhn#ofw zXV2gA!i33xU*7V9ug61puiyVpL;cFAsHqAbxXa?ItCsfnLuVD5LEQ}Jz1MzUt;_%{ zPQa(76slKV-RutpP?>?hxGq$gaEB*dFUzi8vy)aBP-l_>qcn?_uW-W5hQzdYpQF7A8+n(0ZsI(OR3Wx~ghLq0r^OwK0@qT9yG92eSHsC?9c=4+Q0j4VyX~AR(MFmLV4q_J+gcGT= zc^$2~tTQoi&MTCQdY7y?UMVu`WoI1(CJG>8YKy6io0* z9YvTixh^@au({aP{?zXf-G21=J`F-1W9!8y% zCYW+@wN96PKk6hz$q??BF!>fQ&qFLZ(tBPF+ve8^xo*t!zRdg`SSIc7!ytc?1l}+MDyWeC;fiD zvBTkZIT*&bapQsLXlRx~3pwqWvHG&|#Wj`7sw$S$R4%WoSzKAYpaQP`!m8>8l~oI? zs~1&PEv~FyT3NZcrgmvb+1w+?FL97W&F8oak1(`Kr9z=zx_slHo$nD0Wq=?L_rk(iax5MKhbTQpX%;M61M&)VBkOA!0!C9^U3+0PvAGV2hVH+_ zy!?&t{NTsIV9?=uY)L81$(@yzTbpjH%E&{a@2m=2UKNSSBRoQ1Fav`nY;J#j>n=l*EoXZ9^cmIpGb;BTK69hF^Xkn=y#4~X ze*wTfVjC&^AMi@I$HTE)b5m!MDVLDxDBK!Nr-((d#WNfF`_V#;SBr&2Mj`cFc)tf? zUS8UagoV7)6VgOokXYqgzi#UoI#EF+k+=bNr-WLDC&;h_8LpIJDohRsjwQ-viE_CX z!!&YCjmecHuA^`RO_*syKY4=j%&Cik0OxW5An-!gBK$;$%QbJo+NdZEtuT_5Zp=8t zjvWUB!vO&c3SSx4vMlRAapKa{*c6gXq$qWce>j}f*oRo#NWleM0{p(c`%g`rlmwxvv>7L~v2iKQciUk6z<35JSvYPHd}$d^XXk@y z)8^n?dD9vgQFgbt$K|zq8K$$VCx6;(nOsW>F@UPs^()_b^cVt89nRs$k5{eT z6csf#dW;F03FMlLw6d1F9pIP@n6(>CLE)Pn#*&v|x_kS|DjP)dDUfkZm?V<8s_Nwf zc0?#0&f%eV7A<-eYItxS3{qLFEw{YA<36$|0QeiaA6!V*Jm_@0e1YM8ht5WU$~cLP z)=RP24C~BD;4W161lhFPiAFx>;|87fD~-3-zPdHpQiOD)1`;>Jn@~nb6Qwm#IBE(SHYsw6vUb+;fdR)K@U%l6KV1*{l9CGLG9^KP z)jLV5wrtz&1|huN;~GTT=K;5SkQbRkEH}rkd2LISWU7Knp@>vWrc6uEZ*9F9vcKoY zrHkd*J-d%#GCf6sdw^V`w-`(AwzQ%0OvIBQG(_EQkI(PRw9b-j>28HUT2l^f$Z4~;(9A1_?bolbvu~0Lk zDJ?En>9lEf0PcuO!r_F_)S*Cq{Svus3g{&%BZei^)h)2w?F_>QO^*Qgit1$|u?kIk zMyV)q?z~ll4k1Wyi;C?Tf0<}IA%W-WSgr=VJ`M~d9n90z?gkfAaZ#K`GwK68g z3OTn_GC`VYDhbC;IAH*122w4)fQXp1pFX{OFBAgnq2f!Z ziHvXJLzo;rg83?q)8lYBIL`ORo7?CyP^+NHBns0_8lQUj@EK09^7wCo`x%%DD6@hz zV^WjFJmc2w+brC+ku47V=M%h8gb3lMlD**n-sK3 zERKEY<&FNK;b(w*xb@-p0QaR)QE|wY+9<~WxOa8;ds#l-IRx87X5HxN!`#2$J1xIj zE{%_dB8>sZl)1Up9S@*kg`e3umc4lC!I*I=C+Wa5&AYT zv7^Wee_7cwq_qd&E|bMqS1sx9v-8o=Zm*kRz3rX%6BDy>EQutNNg_!mow{JbYVh=M zIiW5EU!8wkf`l2yH;8+gipoVYJb@8PlwG@V3|$#S_`skWZI@HWeVcgi!s9cELO^S0mb8aF->6&m#rni`jO^(uI{!yJzO z46JejfjM(m;h2gdlPNMuPU0sqCzjpHU5a0n(b;-q%m zc+a{z|m%px7W>GR7g~2|EM2|2CpNLj% z-F|@Mqg+FcVGP3-7dN1`MGHZha7wpe@oJPu56wMrqdbjXUjX+n>8rQ5>(bfYv)i5l z?jy&Ob2~pf{}wuSf(ri3R)Ftkw+Aom{^;_7UtK%?^PA^>&~p7Bx;nn%utz-3;PnEy zf8pfr%YLM1muKXcXXKV<FuSvDzwAGRsmj zN|Awkr7f=_DJA#j?T*ia!QXuN(Wx^RA9VEugTY_?;+L;)I*@LinU`Okky)~8&DM`T z1+SLdO+8t_=u5M5Dsu9wY*}R)Syidlip;#qbX)nfg4!SbZ2!5uHI-(PA$x|mD_CPnYO|+=dV5q2A@0$ z-tX+spI(`jJu@X^+Q~ClfAjGt?|=B)eTPmZrRLi5OVhGTGV^QGvP#&Y$H8FmU%&tE zoQ11Wtc6+m@EL4*m6>@}DH)}oq`j_2@{j|>^CtZXG69Mbjp!NSU8k=kfBPso~X$hvV1_7jKOnt3@>fGwkcIgjus+f(5d$ z%MUwU@FyZ9{SGkz;e_)FQ?w<3hHt0q@jLIVUb6vNtfnaNG+d^G__dY?$ZRBXFCBr} zpXpfVSrrS$-@Rr-ApsVs;2t28X=|$I1blwDWFZY*h`fj41;42at-l48onz4Msi<5k z0<#MfO`9arg!vI3L^I{vc*dG6~Xq6;7zfjW_Suc@W_V zNSh1^aKaA7@Tr8FirdpL?o15E^j7tE)p(_l0!sqkczSBH4HXW&k!NXlFik%0R zGCXh>E*9hU+6M=#>Ryr)YLWs?u0*8H$*J~uU^6CUEefy*ZR}mp8~vcuo@6PM;Yt$x zk&FbPUbcJ_3=ot{>~gw>1H-3JpOMKYkqR@Iv*W5bW%ikKjU$#a{7vCwj~%;AQA&`w zB89s_pWWWx$Gfu&Xv*m;Dw-=2PmUe~X}1`rE-ak;@L@lQ(fB$==v(u~#(a!e#Ely< z=0{3G-e-Zk$KxFwbXQd_5<$`?zy*rb#l&dOoV^6ru5KQhglaHO{+1L}lU}r6z1@6o zYOEE*G~jxK>BdGUA3A)71uO-I4{o=EVSOuJ-YklWQz)RoAj6er%e0%fn>lEWcR@{S zWH=yY8I+p*ckZ;t$EHEfDvVye98X;H%9i2bfWzT*B7g1eI}cI{@PGnW1VTS)V$!8c z*L{9J0!mMJe?#{J!?xaQw`R`7aTPQSLHEPtMN8KChoH*hKsn4ym%kPjH3@uRQ8!Oq zeA>-B&3>PMq-Ywt(GU)UdgPm%4@>2-;I}}VrP9Of_J%f{BIkw%_kInxUSetZd|!sGSY zoz8g+R*9wY3b>n+30a~wwY0VA9uM5Z%4wG4cJDeWmuREuB!$8(6=`hNst4U&9*|f@ z;;_(T64Z;V%~~mmQiI_yX&_{#RjW6k;evGQZZE@fn>Oze$tEF3V53y3Gw6%1U%$h$ zUN=1NpkgYB?BQ{ZB1RZ?^X9`OrNQ7T8|4zMPGbY$&Ocz#lL?AC!$ZZz4H8KlNhZ@| z5`k+gD&`OL4}$Tlpl^m(7`R7|F)C=iI7+!<LUCs?koz2{n!>$H(Z$#~5P9=qJP&r%XvnNJuxDvR+=fuC46> z!}!75iWhJ`Jv>I-$41QnJh>7|-hoO`@uQOr4-0;taXOt0<85v096!-QU>cgT z;FxCOgyemDk0Se^(S$;nW%yqRzbdU=Iz9hlj8 zX7`NbFjP?BmrQ&#Jj-z{?e{V=%Vcs5O`8=8b5zu%)vvty_#M=77+G!~vDk_{C80F| z#4>}zT^uzz8WKC8+)~IZ++mdPZ-w-=Xktf;JCLz=Jsd17tdmM&X|N(O%Vesotg3tM z-B5r;>p+%eE?sVo8E+;r4N2&5JaNj@)U)S6;m$)(Gy)MG=cjbWbN@k4PF|^8uA)d2 zO(x3$xGx+SunU`s}i@d1udFb2#9B6rP{_2^QW2hE#U;Ob7s=pb=A!si#e^=aD|cx>$yRBwYRo zy}aP%g}k^Ju=NT@C5M^-_|Vj~P<$ecByPs!am6$0Kms4pPxf>zW%RF~@tonM1Fqpm z!y8}UffMl*rJ-m8MHnVdFkWdyz#ZKV$WF!M_XoCa+b@$%RVYlT25*iYW7xa@s1F1s zf>*u}Xzg^ezDKiWtrA5kDbf;6TQGTiaY@6&o<0V;GSSy&A`e(E!U+JUJTFKIyy$G8 z59oFe1OkDhN6*Qmr~?ct#9BI9{noAnL&L+!O6%#b>w%UA_SUVAaT9bnXw3DPToV_U ze)HA?hGm3m6nd%!0)dA5Wisdkv%naZYpZ81>mNjW2LV+hZ3l9k1D~jX|LC!^GWir* zp(jyh@1)78hmW2L`28^UV1jfqEO+wcjq&4?a2)D1IH4UoPIvtHRhIRk8_NCc)#nEQ zteY7Yje$^PL0IJ?=tB6u5VUq;7>@P3UEX_b-EY3JSEaHdZ+EcBhh`V?h2zf&M_kCIqGIAsFMsXd zzV~i081(uc6%<$1)-NrqT3A}OsG@FZW$lve{EGCf(v-~N?YoZt>et}q(%0{`+6wCD zF8}s-eh>@>zwyuCU;5I9w9KN+tdi35`QP}~cZ0#;zkUCwt6teUt)M1nT2=0}%IQUQ z<<(2eYZh5^%F}GcxzlTU`ne~;;76YZ+d2knY8Mq0&nhadE3cZjdB=%@ne)u4`O8;t z`0l$<2KdU~{6ksg{G9x%{ONVeR&4x_pMo?#7z}oH3{0C=m6=sy$tc{m>)6kJ^(*+z zZf9n8aqhJ0{2BF`(`rrWg{Lkye)QzYho60Rr?qEV@vPi}%Iy5gto(|C(m6HtFXb22 zW@MLVfJgine_dW2U)&k^(g63c)csaVV^iDJTX!4pv|YQ^cKSl&sdLvFZ?|8&1BNqK zZZtphS6*1ke4)S{tu;|<6EvPb+VbWeiDZgmOcF_IXj&taPcAKO;3Y^Qkx-<`2`H$* zmjJc8M~H{{Z6ipKd_GvhPMTyPaUD%2D<~5tk2`qiH0$%*oerlPM8crKbB7PN-RT;1 zI-PE|zG0O_8i!i@%_N~So2NIo+yg)fMAXiU=^ZZj`~~a8A|0);5Ts#TjCsfQ!vQd` zej2#*x~&^GS~a>%3|B!y6k);%mEMqZ_S}tW)8@!!YKk;SC5bsXbuQE)+A37BX3_BdKULOtIg)Z`tG$!P)A|UCffcr)PxD%8T71|qg++SERRd1Dz%B%uH0oQA2JVq% z0|B@PhKBM5;I0n=_m@8ha2GCa_>Iy3K-lAKG8xwhk)Hm@J_eo( zq5Ll9;^iwcT!r+oW|{;K_xbZ7G<7s^cLQ**St-Z01d&V;$#SXAXq)q5An+Fy?lj1*<%A|qnR5md?$C!m z3b-Ffz@3CdN=&X%tF70sw+WUqun}|)4L_=@Um_BNGdN8eC{jClQfhNcC(HQ|mkGOo z@Bo)jO^C2idS++%9JYks0$sI7Z)|>xAfeeGTCi!YTsEb&q;AmeK(w8Au@BW?gqCdK z8a@xWTX^7(Mwi3wvik#pLx<0f9cu<%IhX}&NLqR1_?f`)u+z!Ugdry@eki*=EbBXc z`UZg|P!yyW;6&o&*wn*E&iVa}3)qE|<$Pz)-B7BsFkC}|-IhTno4R-33BKk08Kel8 z$M5%Fxq2feMoSWj6k(#Nq%qODEnD{ohKJxi;hYa`-m*^tiPJhDIHYdeIK#=)S6P@m zBL*AV@$($nZg($Pyg@FHqaaw%B$cT%tQ8&i`jN_#Crxm<1eO_M^{AwR#H*E%sM0m_6OVgw}0u-^M!U3vLcm`qKRNi=C8 zWNK?_S!;6#g0Q22`>x$bWm1@C!E#Ebvsj9nTJQ4x5YGed5EcT!UBm--Js~q9;EqbO za2h@Sz|i4iXQbFgx29SX@3CN0{7drC0fP2`*&JBdw5o(g*8Fx7R96e>i zk+y*Cn63vs6DFqNgr2~m|7Pr18@HOZx?BtcV{4i)M32$!+I3_I#J2w}aOa;&=vgKJ z_k}!grzyx^Ei9}@z#S>vk+D)}LO|7CkITa{ciSFVGfFU-64dCF8N=22)9X6!_agr^ zcxRUJT)ld4{CFctXut~y*G`yd*|X;i;?#nrxKK+*z@784SFYaB>oet$v#b3kkGUdvff5EZcewW-|jY>r^{t3 z3JkAJ6ro?dbc4$i%4tCBTx7JxHr{Blq~?pp zg!M3*Egh~1GZTL3y4;>ci`Rl5I-#fNBn*!)DykLIFa8+d9$Lw`oX-Gv{lp1|(ZHQ| zRPlCo^%oS@N@ZX)ry$ZWT$_|Uz4 zBabbh_HXTTRn*u303ZNKL_t(OLD7k5;bD|xn%KD1tBufL#GmSrhr7R_ei??vk%Sp4 zF@&LLMniWGjQr5!l%M(>DDBDNc6--v*v`kBlMo4@A3r|nz=2}{=n!?fLHptGc)WL8 zdJ>fBIH7^MDQSovqu;viFvD?9mlGnR!UG`U#Y>fh?iv^Pwxh{h5bA^#2H{+H_!ejZ z%b*m@t5@&Zvdd+1HHy78%CH2prSR5mWMGBXz;2I=dC{n6il>pTDQ{r`Mv^&47qR%PwN|MQJ+e-aG- z%XhwCQa&duw$%x< zzwy?+_6Lq1{P@2f{q^6PQVMdWRilttazWu$Q z{O5b0Rn#s|%`BW*Rqx{bPlCY@KYj9zfBOFIyAK_nue|sE$M3!W!H?ek@n=th_a8WN z^D1pQW$D?aa~Hqz-GBd&U@-Wz_kQDW1#UJy=;-m1JK!w;B>2_;{mq+q zAKbe8@cNxjkN<1$fAnP4x~=B4!i=2q7as0^*~ga%xHlrCe(g3ecOI`d-tN41^WOD4 z9gR&LSCRf5eBNIG_b(4{N1GxiT0}yRy3gmkcI8Hb+A7DC2)OGhTC1QFckVpwWqoKD z3(O9-`U1TQ2e>)WDl=rA$QzOZHU;72;-%|E;M!@Xa1)7Z6B9G9Ha2m7KYRp+LE$b) z^#ZGCv=U%oX!?hT_U=C!GahybU@SwKq>{M(2hK9AAN_PBUPcbY3l^;tML~QMP9~0v zG4Fiqm=6rBN1`3WFPF>X3j|)@w1Xm5B%y&-IHe^hjn!6Tv6Nu)M4C<#i&VC(T8Co@ z?T*6xS-=qU;m0-=>Puxo4`^3$>a9=P-tq*m^2DEC@3vK z=wikt@7#IVCs@h|XD1{=M70i=hw}~X-gkJyM2JizDGfnupw*XDty}+=Z)g}U5Ra>P zPaA$?hZrKmar`ck-{J6H?V;*L#K(O~NR9p&Z>Smq?y(A5hk(1Pre?+O0PZ9279zOe z_vAqfsLC0i@6h3MQ)5$LF9hRROA!edF5LF{0Jw)U^uj02?RKwQ|0d|xA<4k3h)#}* zn%XdDX~@G}_;ZZcWgi@g7z&wuIdw0;)I@57%@^8ugp7h?mFjZ!N?+1 zx?nl(Xecxlup_2XC)%!FYhgLY1-KKM{(XM`maV&DV)O(aPvbfo*O9n(_gg0!uyGF& z8KH4JBtjSXXZUUz9Y6TgeJ!96URB97x8An^V4VW~zl4q?)MLl0Ut7N!{BT2k_oyp^ zHvksO5%4n+aF2Z&xSt9EcR&LS%ig}zX|YU)XjX-Rjy8#;Q&+Fu=Js;Pw!qE{zr*DM zWZ>hOUAL0Jl+lzSTA`E4V)dr%7O-gN%ZqTvJMQ<}Y?TlgK!FNgj>XO>p4~S98MYxK zfv|A|pAf*TU%yi>kBg>^G;ZNB>(#5x9LKs`cDLK<^ZCx6y{b}WV7QJ1i*18Urd+yw zn*i7a<{nl9gJ7_;vn$DzPhcvFFw&G!DpO`y%SS8R`SKz1?ihI!K86+^1gNLP85kZu za`gPf7`=kh(hyZ=88T6$e^4?jfk0)fl<@S2)E|+^?z>$_( zBA4nQS5pA)w!s0gl;K}||AIddm^*hh2D}Ec2W0ZZ`Z+7?b|>Vu@-D(?Qge7b6;(^c z;slBY?1jm+4Re?E+xvNzJ@ViYuDi$QXYSl>2JFZKcbPVBO6KX)=K;8f#~7*#oIZPX za%{3p9*dKSW5%h@oV(;73UH8D?Dum%AH%^X@j9KJKDTGk?S&MQU3V zkQgyEJbd8L8Hrd46kDN_NmQw6g{^JPC?;XV@+&l%3tCjjCvZDlq1=&(16@-zwW0@x zhw*Z6ys<|ro&s(dgh?(}X*5}HzkQnnk9ht;_RKjM`30}*hXO+=G522}Ol%Q%wsBHz!xacH$_v|}5aU%Fvpvt{j1bG$9dV2U+6jWMBicaFMkyAXl~d;Suzdab=|xt={PLV%@E3@8s&P4nA3fUm`Zk=L z8VTI50&r*f#XSGOb-CP}FL2_-g~?OQ7?wccsT7$kk*em-UG3oAB9WUsFQ#S~?&A4o zl@h8!5SK#gFnRp8tw+2J7qaO?RyiZ}Fcd)nz?$dn{2+BA|2ik*VcXjtW@px*_zFEu zYh}{7!h$&uy9Q8tgm6W|%K-GGXKU+T#&ABF18N>|u_@QCwXh77vH4UUFb4Gd=hQEi z%O*n=84S>M@$sqc?cEGu?6Aorh)djV7sD_;z5Ubjt5DyVo}>%}p_w?*a`@oMA%6h% zf57L5SWAbi2;xaq5I_y4Seo+6#rGd}yI^ND;C=o85b8s2o8B>(Bnr3j+~WY38);NgyC9EV&3J;w@{J__`-SMeq;*^tK>3R`H-Xu!?b1P z3mp!)RfL5o|9wY3Sfny0TpkX4zQ1^VVK0w|8MJpdzTJ0f>u-8VlS^=82jKMCIJ>bGPR+Vab6d8JwTrK?`s%)Rs1AAj;`_(y;A>8D@&yKk3N zE=tcXO14d(HD__)D_`UPI{yd0{;hxA^yaSHt*zky-sR58tF~sBW#pEn*^2iby70qy ze-{3e-+TAxnR&H{q|c~nSa!R$`)BXH&;M8Sxt@IVN$@-G{%rozHJ0>gnYk~jP=Dda zmk79Dxp^0m`kmIRw?W>1@!G9j`%l)-Tk+DW*H53nas3Wr?tij^{@Uo{?Hf&Z+wXgU zrGy?Ozw6un8NU7}LsLV)gBIKlmurw$Rj>@Ze&g;jWBDd=SWObRE+IbW;GuJzFW?3~ z?r|X>O##&~Za4thFf7Ay&{oMJTs%ajxa8!ZrriQ zV628rPLRdw<98mf1`qcMiZKRCVV2^`RO`%^dtIF052Xm`MrFJa2LlJ+v>H;Q2&;NG z%eZ@cdrih^IH4q{M3T}-@wn>RhQRO;P&(o7{;tErFo@{!{ilqpf52H*vQWkYcM5!> zXE&_2J3S113%K5+4{!KDD%4TLo%8X)J(a?Z3Q9-d+L*DaP#OU2rvG34tqG&Fqb>=}%jLMedI$#G4*GVk=c8$Lhhg1cKd{VeA@HUiv@a=BKg&uwk% z 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�%Y5@MSyC#8E-c4W=h3xls!eWGApPoE(_giI^5 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