Merge branch 'master' into assembly-consistency/add_0x20

pull/5325/head
Hadrien Croubois 2 months ago
commit 551945a56e
No known key found for this signature in database
GPG Key ID: B53810561A746A06
  1. 5
      .changeset/brave-islands-sparkle.md
  2. 5
      .changeset/cyan-taxis-travel.md
  3. 5
      .changeset/seven-insects-taste.md
  4. 5
      .changeset/ten-fishes-fold.md
  5. 5
      .changeset/thin-eels-cross.md
  6. 8
      .githooks/pre-push
  7. 6
      .github/workflows/checks.yml
  8. 2
      .github/workflows/formal-verification.yml
  9. 2
      .husky/pre-commit
  10. BIN
      audits/2024-12-v5.2.pdf
  11. 17
      audits/README.md
  12. 19
      contracts/account/utils/draft-ERC4337Utils.sol
  13. 58
      contracts/account/utils/draft-ERC7579Utils.sol
  14. 25
      contracts/governance/Governor.sol
  15. 15
      contracts/governance/IGovernor.sol
  16. 14
      contracts/governance/extensions/GovernorCountingOverridable.sol
  17. 76
      contracts/governance/extensions/GovernorSequentialProposalId.sol
  18. 2
      contracts/governance/extensions/GovernorTimelockCompound.sol
  19. 14
      contracts/governance/utils/VotesExtended.sol
  20. 41
      contracts/interfaces/draft-IERC4337.sol
  21. 7
      contracts/interfaces/draft-IERC7579.sol
  22. 39
      contracts/mocks/governance/GovernorSequentialProposalIdMock.sol
  23. 10
      contracts/proxy/Clones.sol
  24. 7
      contracts/token/ERC20/extensions/ERC1363.sol
  25. 10
      contracts/token/ERC20/extensions/ERC4626.sol
  26. 2
      contracts/token/ERC721/README.adoc
  27. 4
      contracts/utils/Address.sol
  28. 4
      contracts/utils/NoncesKeyed.sol
  29. 2
      contracts/utils/README.adoc
  30. 18
      contracts/utils/Strings.sol
  31. 2
      docs/modules/ROOT/pages/erc1155.adoc
  32. 1
      foundry.toml
  33. 2
      fv-requirements.txt
  34. 951
      package-lock.json
  35. 24
      package.json
  36. 2
      scripts/checks/coverage.sh
  37. 2
      scripts/checks/inheritance-ordering.js
  38. 2
      scripts/checks/pragma-consistency.js
  39. 5
      scripts/prepare.sh
  40. 10
      scripts/set-max-old-space-size.sh
  41. 5
      scripts/solhint-custom/package.json
  42. 3
      slither.config.json
  43. 2
      solhint.config.js
  44. 2
      test/TESTING.md
  45. 48
      test/account/utils/draft-ERC4337Utils.test.js
  46. 421
      test/account/utils/draft-ERC7579Utils.t.sol
  47. 28
      test/account/utils/draft-ERC7579Utils.test.js
  48. 1
      test/bin/EntryPoint070.abi
  49. BIN
      test/bin/EntryPoint070.bytecode
  50. 1
      test/bin/SenderCreator070.abi
  51. BIN
      test/bin/SenderCreator070.bytecode
  52. 2
      test/governance/Governor.test.js
  53. 2
      test/governance/extensions/GovernorCountingFractional.test.js
  54. 4
      test/governance/extensions/GovernorCountingOverridable.test.js
  55. 2
      test/governance/extensions/GovernorERC721.test.js
  56. 2
      test/governance/extensions/GovernorPreventLateQuorum.test.js
  57. 202
      test/governance/extensions/GovernorSequentialProposalId.test.js
  58. 2
      test/governance/extensions/GovernorStorage.test.js
  59. 2
      test/governance/extensions/GovernorTimelockAccess.test.js
  60. 2
      test/governance/extensions/GovernorTimelockCompound.test.js
  61. 2
      test/governance/extensions/GovernorTimelockControl.test.js
  62. 2
      test/governance/extensions/GovernorVotesQuorumFraction.test.js
  63. 2
      test/governance/extensions/GovernorWithParams.test.js
  64. 31
      test/helpers/erc4337-entrypoint.js
  65. 26
      test/helpers/governance.js
  66. 9
      test/helpers/iterate.js
  67. 2
      test/metatx/ERC2771Context.test.js
  68. 78
      test/utils/introspection/SupportsInterface.behavior.js

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`GovernorSequentialProposalId`: Adds a `Governor` extension that sequentially numbers proposal ids instead of using the hash.

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`Address`: bubble up revert data on `sendValue` failed call

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': patch
---
`ERC7579Utils`: Add ABI decoding checks on calldata bounds within `decodeBatch`

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`IGovernor`: Add the `getProposalId` function to the governor interface.

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': patch
---
`ERC4626`: Use the `asset` getter in `totalAssets`, `_deposit` and `_withdraw`.

@ -1,8 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "${CI:-"false"}" != "true" ]; then
npm run test:generation
npm run lint
fi

@ -97,7 +97,7 @@ jobs:
uses: ./.github/actions/setup
- name: Run coverage
run: npm run coverage
- uses: codecov/codecov-action@v4
- uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
@ -118,11 +118,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up environment
uses: ./.github/actions/setup
- run: rm foundry.toml
- uses: crytic/slither-action@v0.4.0
with:
node-version: 18.15
slither-version: 0.10.1
codespell:
runs-on: ubuntu-latest

@ -52,7 +52,7 @@ jobs:
- name: Install python packages
run: pip install -r fv-requirements.txt
- name: Install java
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: ${{ env.JAVA_VERSION }}

@ -0,0 +1,2 @@
npm run test:generation
npx lint-staged

Binary file not shown.

@ -1,13 +1,14 @@
# Audits
| Date | Version | Commit | Auditor | Scope | Links |
| ------------ | ------- | --------- | ------------ | -------------------- | ----------------------------------------------------------- |
| October 2024 | v5.1.0 | TBD | OpenZeppelin | v5.1 Changes | [🔗](./2024-10-v5.1.pdf) |
| October 2023 | v5.0.0 | `b5a3e69` | OpenZeppelin | v5.0 Changes | [🔗](./2023-10-v5.0.pdf) |
| 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) |
| Date | Version | Commit | Auditor | Scope | Links |
| ------------- | ------- | -------------------------------------------------------------------------------- | ------------ | -------------------- | ----------------------------------------------------------- |
| December 2024 | v5.2.0 | [`98d28f9`](https://github.com/openzeppelin/openzeppelin-contracts/tree/98d28f9) | OpenZeppelin | v5.2 Changes | [🔗](./2024-12-v5.2.pdf) |
| October 2024 | v5.1.0 | [`aba9ff6`](https://github.com/openzeppelin/openzeppelin-contracts/tree/aba9ff6) | OpenZeppelin | v5.1 Changes | [🔗](./2024-10-v5.1.pdf) |
| October 2023 | v5.0.0 | [`b5a3e69`](https://github.com/openzeppelin/openzeppelin-contracts/tree/b5a3e69) | OpenZeppelin | v5.0 Changes | [🔗](./2023-10-v5.0.pdf) |
| May 2023 | v4.9.0 | [`91df66c`](https://github.com/openzeppelin/openzeppelin-contracts/tree/91df66c) | OpenZeppelin | v4.9 Changes | [🔗](./2023-05-v4.9.pdf) |
| October 2022 | v4.8.0 | [`14f98db`](https://github.com/openzeppelin/openzeppelin-contracts/tree/14f98db) | OpenZeppelin | ERC4626, Checkpoints | [🔗](./2022-10-ERC4626.pdf) [🔗](./2022-10-Checkpoints.pdf) |
| October 2018 | v2.0.0 | [`dac5bcc`](https://github.com/openzeppelin/openzeppelin-contracts/tree/dac5bcc) | LevelK | Everything | [🔗](./2018-10.pdf) |
| March 2017 | v1.0.4 | [`9c5975a`](https://github.com/openzeppelin/openzeppelin-contracts/tree/9c5975a) | New Alchemy | Everything | [🔗](./2017-03.md) |
# Formal Verification

@ -24,9 +24,9 @@ library ERC4337Utils {
function parseValidationData(
uint256 validationData
) internal pure returns (address aggregator, uint48 validAfter, uint48 validUntil) {
validAfter = uint48(bytes32(validationData).extract_32_6(0x00));
validUntil = uint48(bytes32(validationData).extract_32_6(0x06));
aggregator = address(bytes32(validationData).extract_32_20(0x0c));
validAfter = uint48(bytes32(validationData).extract_32_6(0));
validUntil = uint48(bytes32(validationData).extract_32_6(6));
aggregator = address(bytes32(validationData).extract_32_20(12));
if (validUntil == 0) validUntil = type(uint48).max;
}
@ -59,7 +59,8 @@ library ERC4337Utils {
(address aggregator1, uint48 validAfter1, uint48 validUntil1) = parseValidationData(validationData1);
(address aggregator2, uint48 validAfter2, uint48 validUntil2) = parseValidationData(validationData2);
bool success = aggregator1 == address(0) && aggregator2 == address(0);
bool success = aggregator1 == address(uint160(SIG_VALIDATION_SUCCESS)) &&
aggregator2 == address(uint160(SIG_VALIDATION_SUCCESS));
uint48 validAfter = uint48(Math.max(validAfter1, validAfter2));
uint48 validUntil = uint48(Math.min(validUntil1, validUntil2));
return packValidationData(success, validAfter, validUntil);
@ -110,22 +111,22 @@ library ERC4337Utils {
/// @dev Returns `verificationGasLimit` from the {PackedUserOperation}.
function verificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) {
return uint128(self.accountGasLimits.extract_32_16(0x00));
return uint128(self.accountGasLimits.extract_32_16(0));
}
/// @dev Returns `accountGasLimits` from the {PackedUserOperation}.
function callGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) {
return uint128(self.accountGasLimits.extract_32_16(0x10));
return uint128(self.accountGasLimits.extract_32_16(16));
}
/// @dev Returns the first section of `gasFees` from the {PackedUserOperation}.
function maxPriorityFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) {
return uint128(self.gasFees.extract_32_16(0x00));
return uint128(self.gasFees.extract_32_16(0));
}
/// @dev Returns the second section of `gasFees` from the {PackedUserOperation}.
function maxFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) {
return uint128(self.gasFees.extract_32_16(0x10));
return uint128(self.gasFees.extract_32_16(16));
}
/// @dev Returns the total gas price for the {PackedUserOperation} (ie. `maxFeePerGas` or `maxPriorityFeePerGas + basefee`).
@ -153,7 +154,7 @@ library ERC4337Utils {
return self.paymasterAndData.length < 52 ? 0 : uint128(bytes16(self.paymasterAndData[36:52]));
}
/// @dev Returns the forth section of `paymasterAndData` from the {PackedUserOperation}.
/// @dev Returns the fourth section of `paymasterAndData` from the {PackedUserOperation}.
function paymasterData(PackedUserOperation calldata self) internal pure returns (bytes calldata) {
return self.paymasterAndData.length < 52 ? _emptyCalldataBytes() : self.paymasterAndData[52:];
}

@ -38,9 +38,10 @@ library ERC7579Utils {
/**
* @dev Emits when an {EXECTYPE_TRY} execution fails.
* @param batchExecutionIndex The index of the failed transaction in the execution batch.
* @param batchExecutionIndex The index of the failed call in the execution batch.
* @param returndata The returned data from the failed call.
*/
event ERC7579TryExecuteFail(uint256 batchExecutionIndex, bytes result);
event ERC7579TryExecuteFail(uint256 batchExecutionIndex, bytes returndata);
/// @dev The provided {CallType} is not supported.
error ERC7579UnsupportedCallType(CallType callType);
@ -60,10 +61,13 @@ library ERC7579Utils {
/// @dev The module type is not supported.
error ERC7579UnsupportedModuleType(uint256 moduleTypeId);
/// @dev Input calldata not properly formatted and possibly malicious.
error ERC7579DecodingError();
/// @dev Executes a single call.
function execSingle(
ExecType execType,
bytes calldata executionCalldata
bytes calldata executionCalldata,
ExecType execType
) internal returns (bytes[] memory returnData) {
(address target, uint256 value, bytes calldata callData) = decodeSingle(executionCalldata);
returnData = new bytes[](1);
@ -72,8 +76,8 @@ library ERC7579Utils {
/// @dev Executes a batch of calls.
function execBatch(
ExecType execType,
bytes calldata executionCalldata
bytes calldata executionCalldata,
ExecType execType
) internal returns (bytes[] memory returnData) {
Execution[] calldata executionBatch = decodeBatch(executionCalldata);
returnData = new bytes[](executionBatch.length);
@ -90,8 +94,8 @@ library ERC7579Utils {
/// @dev Executes a delegate call.
function execDelegateCall(
ExecType execType,
bytes calldata executionCalldata
bytes calldata executionCalldata,
ExecType execType
) internal returns (bytes[] memory returnData) {
(address target, bytes calldata callData) = decodeDelegate(executionCalldata);
returnData = new bytes[](1);
@ -168,12 +172,40 @@ library ERC7579Utils {
}
/// @dev Decodes a batch of executions. See {encodeBatch}.
///
/// NOTE: This function runs some checks and will throw a {ERC7579DecodingError} if the input is not properly formatted.
function decodeBatch(bytes calldata executionCalldata) internal pure returns (Execution[] calldata executionBatch) {
assembly ("memory-safe") {
let ptr := add(executionCalldata.offset, calldataload(executionCalldata.offset))
// Extract the ERC7579 Executions
executionBatch.offset := add(ptr, 0x20)
executionBatch.length := calldataload(ptr)
unchecked {
uint256 bufferLength = executionCalldata.length;
// Check executionCalldata is not empty.
if (bufferLength < 32) revert ERC7579DecodingError();
// Get the offset of the array (pointer to the array length).
uint256 arrayLengthOffset = uint256(bytes32(executionCalldata[0:32]));
// The array length (at arrayLengthOffset) should be 32 bytes long. We check that this is within the
// buffer bounds. Since we know bufferLength is at least 32, we can subtract with no overflow risk.
if (arrayLengthOffset > bufferLength - 32) revert ERC7579DecodingError();
// Get the array length. arrayLengthOffset + 32 is bounded by bufferLength so it does not overflow.
uint256 arrayLength = uint256(bytes32(executionCalldata[arrayLengthOffset:arrayLengthOffset + 32]));
// Check that the buffer is long enough to store the array elements as "offset pointer":
// - each element of the array is an "offset pointer" to the data.
// - each "offset pointer" (to an array element) takes 32 bytes.
// - validity of the calldata at that location is checked when the array element is accessed, so we only
// need to check that the buffer is large enough to hold the pointers.
//
// Since we know bufferLength is at least arrayLengthOffset + 32, we can subtract with no overflow risk.
// Solidity limits length of such arrays to 2**64-1, this guarantees `arrayLength * 32` does not overflow.
if (arrayLength > type(uint64).max || bufferLength - arrayLengthOffset - 32 < arrayLength * 32)
revert ERC7579DecodingError();
assembly ("memory-safe") {
executionBatch.offset := add(add(executionCalldata.offset, arrayLengthOffset), 0x20)
executionBatch.length := arrayLength
}
}
}

@ -92,6 +92,7 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) {
return
interfaceId == type(IGovernor).interfaceId ||
interfaceId == type(IGovernor).interfaceId ^ IGovernor.getProposalId.selector ||
interfaceId == type(IERC1155Receiver).interfaceId ||
super.supportsInterface(interfaceId);
}
@ -132,6 +133,18 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
return uint256(keccak256(abi.encode(targets, values, calldatas, descriptionHash)));
}
/**
* @dev See {IGovernor-getProposalId}.
*/
function getProposalId(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) public view virtual returns (uint256) {
return hashProposal(targets, values, calldatas, descriptionHash);
}
/**
* @dev See {IGovernor-state}.
*/
@ -317,7 +330,7 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
string memory description,
address proposer
) internal virtual returns (uint256 proposalId) {
proposalId = hashProposal(targets, values, calldatas, keccak256(bytes(description)));
proposalId = getProposalId(targets, values, calldatas, keccak256(bytes(description)));
if (targets.length != values.length || targets.length != calldatas.length || targets.length == 0) {
revert GovernorInvalidProposalLength(targets.length, calldatas.length, values.length);
@ -358,7 +371,7 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
bytes[] memory calldatas,
bytes32 descriptionHash
) public virtual returns (uint256) {
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
uint256 proposalId = getProposalId(targets, values, calldatas, descriptionHash);
_validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Succeeded));
@ -406,7 +419,7 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
bytes[] memory calldatas,
bytes32 descriptionHash
) public payable virtual returns (uint256) {
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
uint256 proposalId = getProposalId(targets, values, calldatas, descriptionHash);
_validateStateBitmap(
proposalId,
@ -468,8 +481,8 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
) public virtual returns (uint256) {
// The proposalId will be recomputed in the `_cancel` call further down. However we need the value before we
// do the internal call, because we need to check the proposal state BEFORE the internal `_cancel` call
// changes it. The `hashProposal` duplication has a cost that is limited, and that we accept.
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
// changes it. The `getProposalId` duplication has a cost that is limited, and that we accept.
uint256 proposalId = getProposalId(targets, values, calldatas, descriptionHash);
// public cancel restrictions (on top of existing _cancel restrictions).
_validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Pending));
@ -492,7 +505,7 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
bytes[] memory calldatas,
bytes32 descriptionHash
) internal virtual returns (uint256) {
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
uint256 proposalId = getProposalId(targets, values, calldatas, descriptionHash);
_validateStateBitmap(
proposalId,

@ -203,7 +203,9 @@ interface IGovernor is IERC165, IERC6372 {
/**
* @notice module:core
* @dev Hashing function used to (re)build the proposal id from the proposal details..
* @dev Hashing function used to (re)build the proposal id from the proposal details.
*
* NOTE: For all off-chain and external calls, use {getProposalId}.
*/
function hashProposal(
address[] memory targets,
@ -212,6 +214,17 @@ interface IGovernor is IERC165, IERC6372 {
bytes32 descriptionHash
) external pure returns (uint256);
/**
* @notice module:core
* @dev Function used to get the proposal id from the proposal details.
*/
function getProposalId(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) external view returns (uint256);
/**
* @notice module:core
* @dev Current state of a proposal, following Compound's convention

@ -8,7 +8,7 @@ import {VotesExtended} from "../utils/VotesExtended.sol";
import {GovernorVotes} from "./GovernorVotes.sol";
/**
* @dev Extension of {Governor} which enables delegatees to override the vote of their delegates. This module requires a
* @dev Extension of {Governor} which enables delegators to override the vote of their delegates. This module requires a
* token that inherits {VotesExtended}.
*/
abstract contract GovernorCountingOverridable is GovernorVotes {
@ -144,9 +144,9 @@ abstract contract GovernorCountingOverridable is GovernorVotes {
revert GovernorAlreadyOverridenVote(account);
}
uint256 proposalSnapshot = proposalSnapshot(proposalId);
uint256 overridenWeight = VotesExtended(address(token())).getPastBalanceOf(account, proposalSnapshot);
address delegate = VotesExtended(address(token())).getPastDelegate(account, proposalSnapshot);
uint256 snapshot = proposalSnapshot(proposalId);
uint256 overridenWeight = VotesExtended(address(token())).getPastBalanceOf(account, snapshot);
address delegate = VotesExtended(address(token())).getPastDelegate(account, snapshot);
uint8 delegateCasted = proposalVote.voteReceipt[delegate].casted;
proposalVote.voteReceipt[account].hasOverriden = true;
@ -162,7 +162,7 @@ abstract contract GovernorCountingOverridable is GovernorVotes {
return overridenWeight;
}
/// @dev Variant of {Governor-_castVote} that deals with vote overrides.
/// @dev Variant of {Governor-_castVote} that deals with vote overrides. Returns the overridden weight.
function _castOverride(
uint256 proposalId,
address account,
@ -180,7 +180,7 @@ abstract contract GovernorCountingOverridable is GovernorVotes {
return overridenWeight;
}
/// @dev Public function for casting an override vote
/// @dev Public function for casting an override vote. Returns the overridden weight.
function castOverrideVote(
uint256 proposalId,
uint8 support,
@ -190,7 +190,7 @@ abstract contract GovernorCountingOverridable is GovernorVotes {
return _castOverride(proposalId, voter, support, reason);
}
/// @dev Public function for casting an override vote using a voter's signature
/// @dev Public function for casting an override vote using a voter's signature. Returns the overridden weight.
function castOverrideVoteBySig(
uint256 proposalId,
uint8 support,

@ -0,0 +1,76 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Governor} from "../Governor.sol";
/**
* @dev Extension of {Governor} that changes the numbering of proposal ids from the default hash-based approach to
* sequential ids.
*/
abstract contract GovernorSequentialProposalId is Governor {
uint256 private _latestProposalId;
mapping(uint256 proposalHash => uint256 proposalId) private _proposalIds;
/**
* @dev The {latestProposalId} may only be initialized if it hasn't been set yet
* (through initialization or the creation of a proposal).
*/
error GovernorAlreadyInitializedLatestProposalId();
/**
* @dev See {IGovernor-getProposalId}.
*/
function getProposalId(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) public view virtual override returns (uint256) {
uint256 proposalHash = hashProposal(targets, values, calldatas, descriptionHash);
uint256 storedProposalId = _proposalIds[proposalHash];
if (storedProposalId == 0) {
revert GovernorNonexistentProposal(0);
}
return storedProposalId;
}
/**
* @dev Returns the latest proposal id. A return value of 0 means no proposals have been created yet.
*/
function latestProposalId() public view virtual returns (uint256) {
return _latestProposalId;
}
/**
* @dev See {IGovernor-_propose}.
* Hook into the proposing mechanism to increment proposal count.
*/
function _propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description,
address proposer
) internal virtual override returns (uint256) {
uint256 proposalHash = hashProposal(targets, values, calldatas, keccak256(bytes(description)));
uint256 storedProposalId = _proposalIds[proposalHash];
if (storedProposalId == 0) {
_proposalIds[proposalHash] = ++_latestProposalId;
}
return super._propose(targets, values, calldatas, description, proposer);
}
/**
* @dev Internal function to set the {latestProposalId}. This function is helpful when transitioning
* from another governance system. The next proposal id will be `newLatestProposalId` + 1.
*
* May only call this function if the current value of {latestProposalId} is 0.
*/
function _initializeLatestProposalId(uint256 newLatestProposalId) internal virtual {
if (_latestProposalId != 0) {
revert GovernorAlreadyInitializedLatestProposalId();
}
_latestProposalId = newLatestProposalId;
}
}

@ -10,7 +10,7 @@ import {SafeCast} from "../../utils/math/SafeCast.sol";
/**
* @dev Extension of {Governor} that binds the execution process to a Compound Timelock. This adds a delay, enforced by
* the external timelock to all successful proposal (in addition to the voting duration). The {Governor} needs to be
* the external timelock to all successful proposals (in addition to the voting duration). The {Governor} needs to be
* the admin of the timelock for any operation to be performed. A public, unrestricted,
* {GovernorTimelockCompound-__acceptAdmin} is available to accept ownership of the timelock.
*

@ -34,8 +34,8 @@ abstract contract VotesExtended is Votes {
using Checkpoints for Checkpoints.Trace160;
using Checkpoints for Checkpoints.Trace208;
mapping(address delegator => Checkpoints.Trace160) private _delegateCheckpoints;
mapping(address account => Checkpoints.Trace208) private _balanceOfCheckpoints;
mapping(address delegator => Checkpoints.Trace160) private _userDelegationCheckpoints;
mapping(address account => Checkpoints.Trace208) private _userVotingUnitsCheckpoints;
/**
* @dev Returns the delegate of an `account` at a specific moment in the past. If the `clock()` is
@ -46,7 +46,7 @@ abstract contract VotesExtended is Votes {
* - `timepoint` must be in the past. If operating using block numbers, the block must be already mined.
*/
function getPastDelegate(address account, uint256 timepoint) public view virtual returns (address) {
return address(_delegateCheckpoints[account].upperLookupRecent(_validateTimepoint(timepoint)));
return address(_userDelegationCheckpoints[account].upperLookupRecent(_validateTimepoint(timepoint)));
}
/**
@ -58,14 +58,14 @@ abstract contract VotesExtended is Votes {
* - `timepoint` must be in the past. If operating using block numbers, the block must be already mined.
*/
function getPastBalanceOf(address account, uint256 timepoint) public view virtual returns (uint256) {
return _balanceOfCheckpoints[account].upperLookupRecent(_validateTimepoint(timepoint));
return _userVotingUnitsCheckpoints[account].upperLookupRecent(_validateTimepoint(timepoint));
}
/// @inheritdoc Votes
function _delegate(address account, address delegatee) internal virtual override {
super._delegate(account, delegatee);
_delegateCheckpoints[account].push(clock(), uint160(delegatee));
_userDelegationCheckpoints[account].push(clock(), uint160(delegatee));
}
/// @inheritdoc Votes
@ -73,10 +73,10 @@ abstract contract VotesExtended is Votes {
super._transferVotingUnits(from, to, amount);
if (from != to) {
if (from != address(0)) {
_balanceOfCheckpoints[from].push(clock(), SafeCast.toUint208(_getVotingUnits(from)));
_userVotingUnitsCheckpoints[from].push(clock(), SafeCast.toUint208(_getVotingUnits(from)));
}
if (to != address(0)) {
_balanceOfCheckpoints[to].push(clock(), SafeCast.toUint208(_getVotingUnits(to)));
_userVotingUnitsCheckpoints[to].push(clock(), SafeCast.toUint208(_getVotingUnits(to)));
}
}
}

@ -45,10 +45,18 @@ struct PackedUserOperation {
/**
* @dev Aggregates and validates multiple signatures for a batch of user operations.
*
* A contract could implement this interface with custom validation schemes that allow signature aggregation,
* enabling significant optimizations and gas savings for execution and transaction data cost.
*
* Bundlers and clients whitelist supported aggregators.
*
* See https://eips.ethereum.org/EIPS/eip-7766[ERC-7766]
*/
interface IAggregator {
/**
* @dev Validates the signature for a user operation.
* Returns an alternative signature that should be used during bundling.
*/
function validateUserOpSignature(
PackedUserOperation calldata userOp
@ -73,6 +81,12 @@ interface IAggregator {
/**
* @dev Handle nonce management for accounts.
*
* Nonces are used in accounts as a replay protection mechanism and to ensure the order of user operations.
* To avoid limiting the number of operations an account can perform, the interface allows using parallel
* nonces by using a `key` parameter.
*
* See https://eips.ethereum.org/EIPS/eip-4337#semi-abstracted-nonce-support[ERC-4337 semi-abstracted nonce support].
*/
interface IEntryPointNonces {
/**
@ -84,7 +98,11 @@ interface IEntryPointNonces {
}
/**
* @dev Handle stake management for accounts.
* @dev Handle stake management for entities (i.e. accounts, paymasters, factories).
*
* The EntryPoint must implement the following API to let entities like paymasters have a stake,
* and thus have more flexibility in their storage access
* (see https://eips.ethereum.org/EIPS/eip-4337#reputation-scoring-and-throttlingbanning-for-global-entities[reputation, throttling and banning.])
*/
interface IEntryPointStake {
/**
@ -120,6 +138,8 @@ interface IEntryPointStake {
/**
* @dev Entry point for user operations.
*
* User operations are validated and executed by this contract.
*/
interface IEntryPoint is IEntryPointNonces, IEntryPointStake {
/**
@ -158,11 +178,23 @@ interface IEntryPoint is IEntryPointNonces, IEntryPointStake {
}
/**
* @dev Base interface for an account.
* @dev Base interface for an ERC-4337 account.
*/
interface IAccount {
/**
* @dev Validates a user operation.
*
* * MUST validate the caller is a trusted EntryPoint
* * MUST validate that the signature is a valid signature of the userOpHash, and SHOULD
* return SIG_VALIDATION_FAILED (and not revert) on signature mismatch. Any other error MUST revert.
* * MUST pay the entryPoint (caller) at least the missingAccountFunds (which might
* be zero, in case the current accounts deposit is high enough)
*
* Returns an encoded packed validation data that is composed of the following elements:
*
* - `authorizer` (`address`): 0 for success, 1 for failure, otherwise the address of an authorizer contract
* - `validUntil` (`uint48`): The UserOp is valid only up to this time. Zero for infinite.
* - `validAfter` (`uint48`): The UserOp is valid only after this time.
*/
function validateUserOp(
PackedUserOperation calldata userOp,
@ -195,7 +227,8 @@ interface IPaymaster {
}
/**
* @dev Validates whether the paymaster is willing to pay for the user operation.
* @dev Validates whether the paymaster is willing to pay for the user operation. See
* {IAccount-validateUserOp} for additional information on the return value.
*
* NOTE: Bundlers will reject this method if it modifies the state, unless it's whitelisted.
*/
@ -207,6 +240,8 @@ interface IPaymaster {
/**
* @dev Verifies the sender is the entrypoint.
* @param actualGasCost the actual amount paid (by account or paymaster) for this UserOperation
* @param actualUserOpFeePerGas total gas used by this UserOperation (including preVerification, creation, validation and execution)
*/
function postOp(
PostOpMode mode,

@ -50,7 +50,7 @@ interface IERC7579Validator is IERC7579Module {
*
* MUST validate that the signature is a valid signature of the userOpHash
* SHOULD return ERC-4337's SIG_VALIDATION_FAILED (and not revert) on signature mismatch
* See ERC-4337 for additional information on the return value
* See {IAccount-validateUserOp} for additional information on the return value
*/
function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256);
@ -127,6 +127,7 @@ interface IERC7579Execution {
* This function is intended to be called by Executor Modules
* @param mode The encoded execution mode of the transaction. See ModeLib.sol for details
* @param executionCalldata The encoded execution call data
* @return returnData An array with the returned data of each executed subcall
*
* MUST ensure adequate authorization control: i.e. onlyExecutorModule
* If a mode is requested that is not supported by the Account, it MUST revert
@ -140,7 +141,7 @@ interface IERC7579Execution {
/**
* @dev ERC-7579 Account Config.
*
* Accounts should implement this interface to exposes information that identifies the account, supported modules and capabilities.
* Accounts should implement this interface to expose information that identifies the account, supported modules and capabilities.
*/
interface IERC7579AccountConfig {
/**
@ -174,7 +175,7 @@ interface IERC7579AccountConfig {
/**
* @dev ERC-7579 Module Config.
*
* Accounts should implement this interface to allows installing and uninstalling modules.
* Accounts should implement this interface to allow installing and uninstalling modules.
*/
interface IERC7579ModuleConfig {
event ModuleInstalled(uint256 moduleTypeId, address module);

@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Governor} from "../../governance/Governor.sol";
import {GovernorSettings} from "../../governance/extensions/GovernorSettings.sol";
import {GovernorCountingSimple} from "../../governance/extensions/GovernorCountingSimple.sol";
import {GovernorVotesQuorumFraction} from "../../governance/extensions/GovernorVotesQuorumFraction.sol";
import {GovernorSequentialProposalId} from "../../governance/extensions/GovernorSequentialProposalId.sol";
abstract contract GovernorSequentialProposalIdMock is
GovernorSettings,
GovernorVotesQuorumFraction,
GovernorCountingSimple,
GovernorSequentialProposalId
{
function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) {
return super.proposalThreshold();
}
function getProposalId(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) public view virtual override(Governor, GovernorSequentialProposalId) returns (uint256) {
return super.getProposalId(targets, values, calldatas, descriptionHash);
}
function _propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description,
address proposer
) internal virtual override(Governor, GovernorSequentialProposalId) returns (uint256 proposalId) {
return super._propose(targets, values, calldatas, description, proposer);
}
}

@ -163,7 +163,7 @@ library Clones {
* access the arguments within the implementation, use {fetchCloneArgs}.
*
* This function uses the create2 opcode and a `salt` to deterministically deploy the clone. Using the same
* `implementation`, `args` and `salt` multiple time will revert, since the clones cannot be deployed twice
* `implementation`, `args` and `salt` multiple times will revert, since the clones cannot be deployed twice
* at the same address.
*/
function cloneDeterministicWithImmutableArgs(
@ -227,9 +227,9 @@ library Clones {
* function should only be used to check addresses that are known to be clones.
*/
function fetchCloneArgs(address instance) internal view returns (bytes memory) {
bytes memory result = new bytes(instance.code.length - 0x2d); // revert if length is too short
bytes memory result = new bytes(instance.code.length - 45); // revert if length is too short
assembly ("memory-safe") {
extcodecopy(instance, add(result, 0x20), 0x2d, mload(result))
extcodecopy(instance, add(result, 32), 45, mload(result))
}
return result;
}
@ -248,11 +248,11 @@ library Clones {
address implementation,
bytes memory args
) private pure returns (bytes memory) {
if (args.length > 0x5fd3) revert CloneArgumentsTooLong();
if (args.length > 24531) revert CloneArgumentsTooLong();
return
abi.encodePacked(
hex"61",
uint16(args.length + 0x2d),
uint16(args.length + 45),
hex"3d81600a3d39f3363d3d373d3d3d363d73",
implementation,
hex"5af43d82803e903d91602b57fd5bf3",

@ -48,7 +48,8 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 {
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
* and then calls {IERC1363Receiver-onTransferReceived} on `to`. Returns a flag that indicates
* if the call succeeded.
*
* Requirements:
*
@ -75,7 +76,8 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 {
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
* and then calls {IERC1363Receiver-onTransferReceived} on `to`. Returns a flag that indicates
* if the call succeeded.
*
* Requirements:
*
@ -108,6 +110,7 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 {
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
* Returns a flag that indicates if the call succeeded.
*
* Requirements:
*

@ -114,7 +114,7 @@ abstract contract ERC4626 is ERC20, IERC4626 {
/** @dev See {IERC4626-totalAssets}. */
function totalAssets() public view virtual returns (uint256) {
return _asset.balanceOf(address(this));
return IERC20(asset()).balanceOf(address(this));
}
/** @dev See {IERC4626-convertToShares}. */
@ -237,14 +237,14 @@ 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 ERC-777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the
// If asset() is ERC-777, `transferFrom` can trigger a reentrancy BEFORE the transfer happens through the
// `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer,
// calls the vault, which is assumed not malicious.
//
// Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the
// assets are transferred and before the shares are minted, which is a valid state.
// slither-disable-next-line reentrancy-no-eth
SafeERC20.safeTransferFrom(_asset, caller, address(this), assets);
SafeERC20.safeTransferFrom(IERC20(asset()), caller, address(this), assets);
_mint(receiver, shares);
emit Deposit(caller, receiver, assets, shares);
@ -264,14 +264,14 @@ abstract contract ERC4626 is ERC20, IERC4626 {
_spendAllowance(owner, caller, shares);
}
// If _asset is ERC-777, `transfer` can trigger a reentrancy AFTER the transfer happens through the
// If asset() is ERC-777, `transfer` can trigger a reentrancy AFTER the transfer happens through the
// `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer,
// calls the vault, which is assumed not malicious.
//
// Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the
// shares are burned and after the assets are transferred, which is a valid state.
_burn(owner, shares);
SafeERC20.safeTransfer(_asset, receiver, assets);
SafeERC20.safeTransfer(IERC20(asset()), receiver, assets);
emit Withdraw(caller, receiver, owner, assets, shares);
}

@ -22,7 +22,7 @@ OpenZeppelin Contracts provides implementations of all four interfaces:
Additionally there are a few of other extensions:
* {ERC721Consecutive}: An implementation of https://eips.ethereum.org/EIPS/eip-2309[ERC-2309] for minting batchs of tokens during construction, in accordance with ERC-721.
* {ERC721Consecutive}: An implementation of https://eips.ethereum.org/EIPS/eip-2309[ERC-2309] for minting batches of tokens during construction, in accordance with ERC-721.
* {ERC721URIStorage}: A more flexible but more expensive way of storing metadata.
* {ERC721Votes}: Support for voting and vote delegation.
* {ERC721Royalty}: A way to signal royalty information following ERC-2981.

@ -35,9 +35,9 @@ library Address {
revert Errors.InsufficientBalance(address(this).balance, amount);
}
(bool success, ) = recipient.call{value: amount}("");
(bool success, bytes memory returndata) = recipient.call{value: amount}("");
if (!success) {
revert Errors.FailedCall();
_revert(returndata);
}
}

@ -7,6 +7,10 @@ import {Nonces} from "./Nonces.sol";
* @dev Alternative to {Nonces}, that supports key-ed nonces.
*
* Follows the https://eips.ethereum.org/EIPS/eip-4337#semi-abstracted-nonce-support[ERC-4337's semi-abstracted nonce system].
*
* NOTE: This contract inherits from {Nonces} and reuses its storage for the first nonce key (i.e. `0`). This
* makes upgrading from {Nonces} to {NoncesKeyed} safe when using their upgradeable versions (e.g. `NoncesKeyedUpgradeable`).
* Doing so will NOT reset the current state of nonces, avoiding replay attacks where a nonce is reused after the upgrade.
*/
abstract contract NoncesKeyed is Nonces {
mapping(address owner => mapping(uint192 key => uint64)) private _nonces;

@ -42,7 +42,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
* {Context}: A utility for abstracting the sender and calldata in the current execution context.
* {Packing}: A library for packing and unpacking multiple values into bytes32
* {Panic}: A library to revert with https://docs.soliditylang.org/en/v0.8.20/control-structures.html#panic-via-assert-and-error-via-require[Solidity panic codes].
* {Comparators}: A library that contains comparator functions to use with with the {Heap} library.
* {Comparators}: A library that contains comparator functions to use with the {Heap} library.
* {CAIP2}, {CAIP10}: Libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers.
[NOTE]

@ -177,7 +177,8 @@ library Strings {
}
/**
* @dev Variant of {tryParseUint} that does not check bounds and returns (true, 0) if they are invalid.
* @dev Implementation of {tryParseUint} that does not check bounds. Caller should make sure that
* `begin <= end <= input.length`. Other inputs would result in undefined behavior.
*/
function _tryParseUintUncheckedBounds(
string memory input,
@ -249,7 +250,8 @@ library Strings {
}
/**
* @dev Variant of {tryParseInt} that does not check bounds and returns (true, 0) if they are invalid.
* @dev Implementation of {tryParseInt} that does not check bounds. Caller should make sure that
* `begin <= end <= input.length`. Other inputs would result in undefined behavior.
*/
function _tryParseIntUncheckedBounds(
string memory input,
@ -323,7 +325,8 @@ library Strings {
}
/**
* @dev Variant of {tryParseHexUint} that does not check bounds and returns (true, 0) if they are invalid.
* @dev Implementation of {tryParseHexUint} that does not check bounds. Caller should make sure that
* `begin <= end <= input.length`. Other inputs would result in undefined behavior.
*/
function _tryParseHexUintUncheckedBounds(
string memory input,
@ -333,7 +336,7 @@ library Strings {
bytes memory buffer = bytes(input);
// skip 0x prefix if present
bool hasPrefix = (begin < end + 1) && bytes2(_unsafeReadBytesOffset(buffer, begin)) == bytes2("0x"); // don't do out-of-bound (possibly unsafe) read if sub-string is empty
bool hasPrefix = (end > begin + 1) && bytes2(_unsafeReadBytesOffset(buffer, begin)) == bytes2("0x"); // don't do out-of-bound (possibly unsafe) read if sub-string is empty
uint256 offset = hasPrefix.toUint() * 2;
uint256 result = 0;
@ -390,12 +393,13 @@ library Strings {
uint256 begin,
uint256 end
) internal pure returns (bool success, address value) {
// check that input is the correct length
bool hasPrefix = (begin < end + 1) && bytes2(_unsafeReadBytesOffset(bytes(input), begin)) == bytes2("0x"); // don't do out-of-bound (possibly unsafe) read if sub-string is empty
if (end > bytes(input).length || begin > end) return (false, address(0));
bool hasPrefix = (end > begin + 1) && bytes2(_unsafeReadBytesOffset(bytes(input), begin)) == bytes2("0x"); // don't do out-of-bound (possibly unsafe) read if sub-string is empty
uint256 expectedLength = 40 + hasPrefix.toUint() * 2;
if (end - begin == expectedLength && end <= bytes(input).length) {
// check that input is the correct length
if (end - begin == expectedLength) {
// length guarantees that this does not overflow, and value is at most type(uint160).max
(bool s, uint256 v) = _tryParseHexUintUncheckedBounds(input, begin, end);
return (s, address(uint160(v)));

@ -106,7 +106,7 @@ A key difference when using xref:api:token/ERC1155.adoc#IERC1155-safeTransferFro
ERC1155InvalidReceiver("<ADDRESS>")
----
This is a good thing! It means that the recipient contract has not registered itself as aware of the ERC-1155 protocol, so transfers to it are disabled to *prevent tokens from being locked forever*. As an example, https://etherscan.io/token/0xa74476443119A942dE498590Fe1f2454d7D4aC0d?a=0xa74476443119A942dE498590Fe1f2454d7D4aC0d[the Golem contract currently holds over 350k `GNT` tokens], worth multiple tens of thousands of dollars, and lacks methods to get them out of there. This has happened to virtually every ERC20-backed project, usually due to user error.
This is a good thing! It means that the recipient contract has not registered itself as aware of the ERC-1155 protocol, so transfers to it are disabled to *prevent tokens from being locked forever*. As an example, https://etherscan.io/token/0xa74476443119A942dE498590Fe1f2454d7D4aC0d?a=0xa74476443119A942dE498590Fe1f2454d7D4aC0d[the Golem contract currently holds over 350k `GNT` tokens], and lacks methods to get them out of there. This has happened to virtually every ERC20-backed project, usually due to user error.
In order for our contract to receive ERC-1155 tokens we can inherit from the convenience contract xref:api:token/ERC1155.adoc#ERC1155Holder[`ERC1155Holder`] which handles the registering for us. However, we need to remember to implement functionality to allow tokens to be transferred out of our contract:

@ -8,6 +8,7 @@ out = 'out'
libs = ['node_modules', 'lib']
test = 'test'
cache_path = 'cache_forge'
fs_permissions = [{ access = "read", path = "./test/bin" }]
[fuzz]
runs = 5000

@ -1,4 +1,4 @@
certora-cli==4.13.1
# File uses a custom name (fv-requirements.txt) so that it isn't picked by Netlify's build
# whose latest Python version is 0.3.8, incompatible with most recent versions of Halmos
halmos==0.2.0
halmos==0.2.3

951
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -13,7 +13,7 @@
"coverage": "scripts/checks/coverage.sh",
"docs": "npm run prepare-docs && oz-docs",
"docs:watch": "oz-docs watch contracts docs/templates docs/config.js",
"prepare": "scripts/prepare.sh",
"prepare": "husky",
"prepare-docs": "scripts/prepare-docs.sh",
"lint": "npm run lint:js && npm run lint:sol",
"lint:fix": "npm run lint:js:fix && npm run lint:sol:fix",
@ -25,7 +25,7 @@
"prepack": "scripts/prepack.sh",
"generate": "scripts/generate/run.js",
"version": "scripts/release/version.sh",
"test": "hardhat test",
"test": "scripts/set-max-old-space-size.sh && hardhat test",
"test:generation": "scripts/checks/generation.sh",
"test:inheritance": "scripts/checks/inheritance-ordering.js artifacts/build-info/*",
"test:pragma": "scripts/checks/pragma-consistency.js artifacts/build-info/*",
@ -66,14 +66,16 @@
"chai": "^4.2.0",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.0.0",
"ethers": "^6.7.1",
"globals": "^15.3.0",
"ethers": "^6.13.4",
"glob": "^11.0.0",
"globals": "^15.3.0",
"graphlib": "^2.1.8",
"hardhat": "^2.22.2",
"hardhat-exposed": "^0.3.15",
"hardhat-gas-reporter": "^2.0.0",
"hardhat-gas-reporter": "^2.1.0",
"hardhat-ignore-warnings": "^0.2.11",
"husky": "^9.1.7",
"lint-staged": "^15.2.10",
"lodash.startcase": "^4.4.0",
"micromatch": "^4.0.2",
"p-limit": "^6.0.0",
@ -86,7 +88,17 @@
"solidity-ast": "^0.4.50",
"solidity-coverage": "^0.8.5",
"solidity-docgen": "^0.6.0-beta.29",
"undici": "^6.11.1",
"undici": "^7.0.0",
"yargs": "^17.0.0"
},
"lint-staged": {
"*.{js,ts}": [
"prettier --log-level warn --ignore-path .gitignore --check",
"eslint"
],
"*.sol": [
"prettier --log-level warn --ignore-path .gitignore --check",
"solhint"
]
}
}

@ -5,6 +5,8 @@ set -euo pipefail
export COVERAGE=true
export FOUNDRY_FUZZ_RUNS=10
scripts/set-max-old-space-size.sh
# Hardhat coverage
hardhat coverage

@ -31,7 +31,7 @@ for (const artifact of artifacts) {
}
/// graphlib.alg.findCycles will not find minimal cycles.
/// We are only interested int cycles of lengths 2 (needs proof)
/// We are only interested in cycles of lengths 2 (needs proof)
graph.nodes().forEach((x, i, nodes) =>
nodes
.slice(i + 1)

@ -31,7 +31,7 @@ for (const artifact of artifacts) {
const minVersion = semver.minVersion(pragma[source]);
// loop over all imports in source
for (const { absolutePath } of findAll('ImportDirective', solcOutput.sources[source].ast)) {
// So files that only import without declaring anything cause issues, because they don't shop in in "pragma"
// So files that only import without declaring anything cause issues, because they don't shop in "pragma"
if (!pragma[absolutePath]) continue;
// Check that the minVersion for source satisfies the requirements of the imported files
if (!semver.satisfies(minVersion, pragma[absolutePath])) {

@ -1,5 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if git status &>/dev/null; then git config core.hooksPath .githooks; fi

@ -0,0 +1,10 @@
#!/usr/bin/env bash
# This script sets the node `--max-old-space-size` to 8192 if it is not set already.
# All existing `NODE_OPTIONS` are retained as is.
export NODE_OPTIONS="${NODE_OPTIONS:-}"
if [[ $NODE_OPTIONS != *"--max-old-space-size"* ]]; then
export NODE_OPTIONS="${NODE_OPTIONS} --max-old-space-size=8192"
fi

@ -1,5 +1,8 @@
{
"name": "solhint-plugin-openzeppelin",
"version": "0.0.0",
"private": true
"private": true,
"dependencies": {
"minimatch": "^3.1.2"
}
}

@ -1,5 +1,4 @@
{
"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/vendor,contracts-exposed",
"compile_force_framework": "hardhat"
"filter_paths": "contracts/mocks,contracts/vendor,contracts-exposed"
}

@ -1,4 +1,4 @@
const customRules = require('./scripts/solhint-custom');
const customRules = require('solhint-plugin-openzeppelin');
const rules = [
'avoid-tx-origin',

@ -1,3 +1,3 @@
## Testing
Unit test are critical to OpenZeppelin Contracts. They help ensure code quality and mitigate against security vulnerabilities. The directory structure within the `/test` directory corresponds to the `/contracts` directory.
Unit tests are critical to OpenZeppelin Contracts. They help ensure code quality and mitigate against security vulnerabilities. The directory structure within the `/test` directory corresponds to the `/contracts` directory.

@ -3,12 +3,17 @@ const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { packValidationData, UserOperation } = require('../../helpers/erc4337');
const { deployEntrypoint } = require('../../helpers/erc4337-entrypoint');
const { MAX_UINT48 } = require('../../helpers/constants');
const ADDRESS_ONE = '0x0000000000000000000000000000000000000001';
const fixture = async () => {
const [authorizer, sender, entrypoint, factory, paymaster] = await ethers.getSigners();
const { entrypoint } = await deployEntrypoint();
const [authorizer, sender, factory, paymaster] = await ethers.getSigners();
const utils = await ethers.deployContract('$ERC4337Utils');
return { utils, authorizer, sender, entrypoint, factory, paymaster };
const SIG_VALIDATION_SUCCESS = await utils.$SIG_VALIDATION_SUCCESS();
const SIG_VALIDATION_FAILED = await utils.$SIG_VALIDATION_FAILED();
return { utils, authorizer, sender, entrypoint, factory, paymaster, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED };
};
describe('ERC4337Utils', function () {
@ -41,6 +46,20 @@ describe('ERC4337Utils', function () {
MAX_UINT48,
]);
});
it('parse canonical values', async function () {
expect(this.utils.$parseValidationData(this.SIG_VALIDATION_SUCCESS)).to.eventually.deep.equal([
ethers.ZeroAddress,
0n,
MAX_UINT48,
]);
expect(this.utils.$parseValidationData(this.SIG_VALIDATION_FAILED)).to.eventually.deep.equal([
ADDRESS_ONE,
0n,
MAX_UINT48,
]);
});
});
describe('packValidationData', function () {
@ -65,6 +84,21 @@ describe('ERC4337Utils', function () {
validationData,
);
});
it('packing reproduced canonical values', async function () {
expect(this.utils.$packValidationData(ethers.Typed.address(ethers.ZeroAddress), 0n, 0n)).to.eventually.equal(
this.SIG_VALIDATION_SUCCESS,
);
expect(this.utils.$packValidationData(ethers.Typed.bool(true), 0n, 0n)).to.eventually.equal(
this.SIG_VALIDATION_SUCCESS,
);
expect(this.utils.$packValidationData(ethers.Typed.address(ADDRESS_ONE), 0n, 0n)).to.eventually.equal(
this.SIG_VALIDATION_FAILED,
);
expect(this.utils.$packValidationData(ethers.Typed.bool(false), 0n, 0n)).to.eventually.equal(
this.SIG_VALIDATION_FAILED,
);
});
});
describe('combineValidationData', function () {
@ -135,11 +169,19 @@ describe('ERC4337Utils', function () {
describe('hash', function () {
it('returns the operation hash with specified entrypoint and chainId', async function () {
const userOp = new UserOperation({ sender: this.sender, nonce: 1 });
const chainId = 0xdeadbeef;
const chainId = await ethers.provider.getNetwork().then(({ chainId }) => chainId);
const otherChainId = 0xdeadbeef;
// check that helper matches entrypoint logic
expect(this.entrypoint.getUserOpHash(userOp.packed)).to.eventually.equal(userOp.hash(this.entrypoint, chainId));
// check library against helper
expect(this.utils.$hash(userOp.packed, this.entrypoint, chainId)).to.eventually.equal(
userOp.hash(this.entrypoint, chainId),
);
expect(this.utils.$hash(userOp.packed, this.entrypoint, otherChainId)).to.eventually.equal(
userOp.hash(this.entrypoint, otherChainId),
);
});
});

@ -0,0 +1,421 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
// Parts of this test file are adapted from Adam Egyed (@adamegyed) proof of concept available at:
// https://github.com/adamegyed/erc7579-execute-vulnerability/tree/4589a30ff139e143d6c57183ac62b5c029217a90
//
// solhint-disable no-console
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import {PackedUserOperation, IAccount, IEntryPoint} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
import {ERC7579Utils, Mode, CallType, ExecType, ModeSelector, ModePayload, Execution} from "@openzeppelin/contracts/account/utils/draft-ERC7579Utils.sol";
import {Test, Vm, console} from "forge-std/Test.sol";
contract SampleAccount is IAccount, Ownable {
using ECDSA for *;
using MessageHashUtils for *;
using ERC4337Utils for *;
using ERC7579Utils for *;
IEntryPoint internal constant ENTRY_POINT = IEntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032));
event Log(bool duringValidation, Execution[] calls);
error UnsupportedCallType(CallType callType);
constructor(address initialOwner) Ownable(initialOwner) {}
function validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external override returns (uint256 validationData) {
require(msg.sender == address(ENTRY_POINT), "only from EP");
// Check signature
if (userOpHash.toEthSignedMessageHash().recover(userOp.signature) != owner()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
// If this is an execute call with a batch operation, log the call details from the calldata
if (bytes4(userOp.callData[0x00:0x04]) == this.execute.selector) {
(CallType callType, , , ) = Mode.wrap(bytes32(userOp.callData[0x04:0x24])).decodeMode();
if (callType == ERC7579Utils.CALLTYPE_BATCH) {
// Remove the selector
bytes calldata params = userOp.callData[0x04:];
// Use the same vulnerable assignment technique here, but assert afterwards that the checks aren't
// broken here by comparing to the result of `abi.decode(...)`.
bytes calldata executionCalldata;
assembly ("memory-safe") {
let dataptr := add(params.offset, calldataload(add(params.offset, 0x20)))
executionCalldata.offset := add(dataptr, 32)
executionCalldata.length := calldataload(dataptr)
}
// Check that this decoding step is done correctly.
(, bytes memory executionCalldataMemory) = abi.decode(params, (bytes32, bytes));
require(
keccak256(executionCalldata) == keccak256(executionCalldataMemory),
"decoding during validation failed"
);
// Now, we know that we have `bytes calldata executionCalldata` as would be decoded by the solidity
// builtin decoder for the `execute` function.
// This is where the vulnerability from ExecutionLib results in a different result between validation
// andexecution.
emit Log(true, executionCalldata.decodeBatch());
}
}
if (missingAccountFunds > 0) {
(bool success, ) = payable(msg.sender).call{value: missingAccountFunds}("");
success; // Silence warning. The entrypoint should validate the result.
}
return ERC4337Utils.SIG_VALIDATION_SUCCESS;
}
function execute(Mode mode, bytes calldata executionCalldata) external payable {
require(msg.sender == address(this) || msg.sender == address(ENTRY_POINT), "not auth");
(CallType callType, ExecType execType, , ) = mode.decodeMode();
// check if calltype is batch or single
if (callType == ERC7579Utils.CALLTYPE_SINGLE) {
executionCalldata.execSingle(execType);
} else if (callType == ERC7579Utils.CALLTYPE_BATCH) {
executionCalldata.execBatch(execType);
emit Log(false, executionCalldata.decodeBatch());
} else if (callType == ERC7579Utils.CALLTYPE_DELEGATECALL) {
executionCalldata.execDelegateCall(execType);
} else {
revert UnsupportedCallType(callType);
}
}
}
contract ERC7579UtilsTest is Test {
using MessageHashUtils for *;
using ERC4337Utils for *;
using ERC7579Utils for *;
IEntryPoint private constant ENTRYPOINT = IEntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032));
address private _owner;
uint256 private _ownerKey;
address private _account;
address private _beneficiary;
address private _recipient1;
address private _recipient2;
constructor() {
vm.etch(0x0000000071727De22E5E9d8BAf0edAc6f37da032, vm.readFileBinary("test/bin/EntryPoint070.bytecode"));
vm.etch(0xEFC2c1444eBCC4Db75e7613d20C6a62fF67A167C, vm.readFileBinary("test/bin/SenderCreator070.bytecode"));
// signing key
(_owner, _ownerKey) = makeAddrAndKey("owner");
// ERC-4337 account
_account = address(new SampleAccount(_owner));
vm.deal(_account, 1 ether);
// other
_beneficiary = makeAddr("beneficiary");
_recipient1 = makeAddr("recipient1");
_recipient2 = makeAddr("recipient2");
}
function testExecuteBatchDecodeCorrectly() public {
Execution[] memory calls = new Execution[](2);
calls[0] = Execution({target: _recipient1, value: 1 wei, callData: ""});
calls[1] = Execution({target: _recipient2, value: 1 wei, callData: ""});
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
userOps[0] = PackedUserOperation({
sender: _account,
nonce: 0,
initCode: "",
callData: abi.encodeCall(
SampleAccount.execute,
(
ERC7579Utils.encodeMode(
ERC7579Utils.CALLTYPE_BATCH,
ERC7579Utils.EXECTYPE_DEFAULT,
ModeSelector.wrap(0x00),
ModePayload.wrap(0x00)
),
ERC7579Utils.encodeBatch(calls)
)
),
accountGasLimits: _packGas(500_000, 500_000),
preVerificationGas: 0,
gasFees: _packGas(1, 1),
paymasterAndData: "",
signature: ""
});
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
_ownerKey,
this.hashUserOperation(userOps[0]).toEthSignedMessageHash()
);
userOps[0].signature = abi.encodePacked(r, s, v);
vm.recordLogs();
ENTRYPOINT.handleOps(userOps, payable(_beneficiary));
assertEq(_recipient1.balance, 1 wei);
assertEq(_recipient2.balance, 1 wei);
_collectAndPrintLogs(false);
}
function testExecuteBatchDecodeEmpty() public {
bytes memory fakeCalls = abi.encodePacked(
uint256(1), // Length of execution[]
uint256(0x20), // offset
uint256(uint160(_recipient1)), // target
uint256(1), // value: 1 wei
uint256(0x60), // offset of data
uint256(0) // length of
);
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
userOps[0] = PackedUserOperation({
sender: _account,
nonce: 0,
initCode: "",
callData: abi.encodeCall(
SampleAccount.execute,
(
ERC7579Utils.encodeMode(
ERC7579Utils.CALLTYPE_BATCH,
ERC7579Utils.EXECTYPE_DEFAULT,
ModeSelector.wrap(0x00),
ModePayload.wrap(0x00)
),
abi.encodePacked(
uint256(0x70) // fake offset pointing to paymasterAndData
)
)
),
accountGasLimits: _packGas(500_000, 500_000),
preVerificationGas: 0,
gasFees: _packGas(1, 1),
paymasterAndData: abi.encodePacked(address(0), fakeCalls),
signature: ""
});
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
_ownerKey,
this.hashUserOperation(userOps[0]).toEthSignedMessageHash()
);
userOps[0].signature = abi.encodePacked(r, s, v);
vm.expectRevert(
abi.encodeWithSelector(
IEntryPoint.FailedOpWithRevert.selector,
0,
"AA23 reverted",
abi.encodeWithSelector(ERC7579Utils.ERC7579DecodingError.selector)
)
);
ENTRYPOINT.handleOps(userOps, payable(_beneficiary));
_collectAndPrintLogs(false);
}
function testExecuteBatchDecodeDifferent() public {
bytes memory execCallData = abi.encodePacked(
uint256(0x20), // offset pointing to the next segment
uint256(5), // Length of execution[]
uint256(0), // offset of calls[0], and target (!!)
uint256(0x20), // offset of calls[1], and value (!!)
uint256(0), // offset of calls[2], and rel offset of data (!!)
uint256(0) // offset of calls[3].
// There is one more to read by the array length, but it's not present here. This will be
// paymasterAndData.length during validation, pointing to an all-zero call.
// During execution, the offset will be 0, pointing to a call with value.
);
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
userOps[0] = PackedUserOperation({
sender: _account,
nonce: 0,
initCode: "",
callData: abi.encodePacked(
SampleAccount.execute.selector,
ERC7579Utils.encodeMode(
ERC7579Utils.CALLTYPE_BATCH,
ERC7579Utils.EXECTYPE_DEFAULT,
ModeSelector.wrap(0x00),
ModePayload.wrap(0x00)
),
uint256(0x5c), // offset pointing to the next segment
uint224(type(uint224).max), // Padding to align the `bytes` types
// type(uint256).max, // unknown padding
uint256(execCallData.length), // Length of the data
execCallData
),
accountGasLimits: _packGas(500_000, 500_000),
preVerificationGas: 0,
gasFees: _packGas(1, 1),
paymasterAndData: abi.encodePacked(uint256(0), uint256(0)), // padding length to create an offset
signature: ""
});
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
_ownerKey,
this.hashUserOperation(userOps[0]).toEthSignedMessageHash()
);
userOps[0].signature = abi.encodePacked(r, s, v);
vm.expectRevert(
abi.encodeWithSelector(
IEntryPoint.FailedOpWithRevert.selector,
0,
"AA23 reverted",
abi.encodeWithSelector(ERC7579Utils.ERC7579DecodingError.selector)
)
);
ENTRYPOINT.handleOps(userOps, payable(_beneficiary));
_collectAndPrintLogs(true);
}
function testDecodeBatch() public {
// BAD: buffer empty
vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector);
this.callDecodeBatch("");
// BAD: buffer too short
vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector);
this.callDecodeBatch(abi.encodePacked(uint128(0)));
// GOOD
this.callDecodeBatch(abi.encode(0));
// Note: Solidity also supports this even though it's odd. Offset 0 means array is at the same location, which
// is interpreted as an array of length 0, which doesn't require any more data
// solhint-disable-next-line var-name-mixedcase
uint256[] memory _1 = abi.decode(abi.encode(0), (uint256[]));
_1;
// BAD: offset is out of bounds
vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector);
this.callDecodeBatch(abi.encode(1));
// GOOD
this.callDecodeBatch(abi.encode(32, 0));
// BAD: reported array length extends beyond bounds
vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector);
this.callDecodeBatch(abi.encode(32, 1));
// GOOD
this.callDecodeBatch(abi.encode(32, 1, 0));
// GOOD
//
// 0000000000000000000000000000000000000000000000000000000000000020 (32) offset
// 0000000000000000000000000000000000000000000000000000000000000001 ( 1) array length
// 0000000000000000000000000000000000000000000000000000000000000020 (32) element 0 offset
// 000000000000000000000000xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (recipient) target for element #0
// 000000000000000000000000000000000000000000000000000000000000002a (42) value for element #0
// 0000000000000000000000000000000000000000000000000000000000000060 (96) offset to calldata for element #0
// 000000000000000000000000000000000000000000000000000000000000000c (12) length of the calldata for element #0
// 48656c6c6f20576f726c64210000000000000000000000000000000000000000 (..) buffer for the calldata for element #0
assertEq(
bytes("Hello World!"),
this.callDecodeBatchAndGetFirstBytes(
abi.encode(32, 1, 32, _recipient1, 42, 96, 12, bytes12("Hello World!"))
)
);
// This is invalid, the first element of the array points is out of bounds
// but we allow it past initial validation, because solidity will validate later when the bytes field is accessed
//
// 0000000000000000000000000000000000000000000000000000000000000020 (32) offset
// 0000000000000000000000000000000000000000000000000000000000000001 ( 1) array length
// 0000000000000000000000000000000000000000000000000000000000000020 (32) element 0 offset
// <missing element>
bytes memory invalid = abi.encode(32, 1, 32);
this.callDecodeBatch(invalid);
vm.expectRevert();
this.callDecodeBatchAndGetFirst(invalid);
// this is invalid: the bytes field of the first element of the array is out of bounds
// but we allow it past initial validation, because solidity will validate later when the bytes field is accessed
//
// 0000000000000000000000000000000000000000000000000000000000000020 (32) offset
// 0000000000000000000000000000000000000000000000000000000000000001 ( 1) array length
// 0000000000000000000000000000000000000000000000000000000000000020 (32) element 0 offset
// 000000000000000000000000xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (recipient) target for element #0
// 000000000000000000000000000000000000000000000000000000000000002a (42) value for element #0
// 0000000000000000000000000000000000000000000000000000000000000060 (96) offset to calldata for element #0
// <missing data>
bytes memory invalidDeeply = abi.encode(32, 1, 32, _recipient1, 42, 96);
this.callDecodeBatch(invalidDeeply);
// Note that this is ok because we don't return the value. Returning it would introduce a check that would fails.
this.callDecodeBatchAndGetFirst(invalidDeeply);
vm.expectRevert();
this.callDecodeBatchAndGetFirstBytes(invalidDeeply);
}
function callDecodeBatch(bytes calldata executionCalldata) public pure {
ERC7579Utils.decodeBatch(executionCalldata);
}
function callDecodeBatchAndGetFirst(bytes calldata executionCalldata) public pure {
ERC7579Utils.decodeBatch(executionCalldata)[0];
}
function callDecodeBatchAndGetFirstBytes(bytes calldata executionCalldata) public pure returns (bytes calldata) {
return ERC7579Utils.decodeBatch(executionCalldata)[0].callData;
}
function hashUserOperation(PackedUserOperation calldata useroperation) public view returns (bytes32) {
return useroperation.hash(address(ENTRYPOINT), block.chainid);
}
function _collectAndPrintLogs(bool includeTotalValue) internal {
Vm.Log[] memory logs = vm.getRecordedLogs();
for (uint256 i = 0; i < logs.length; i++) {
if (logs[i].emitter == _account) {
_printDecodedCalls(logs[i].data, includeTotalValue);
}
}
}
function _printDecodedCalls(bytes memory logData, bool includeTotalValue) internal pure {
(bool duringValidation, Execution[] memory calls) = abi.decode(logData, (bool, Execution[]));
console.log(
string.concat(
"Batch execute contents, as read during ",
duringValidation ? "validation" : "execution",
": "
)
);
console.log(" Execution[] length: %s", calls.length);
uint256 totalValue = 0;
for (uint256 i = 0; i < calls.length; ++i) {
console.log(string.concat(" calls[", vm.toString(i), "].target = ", vm.toString(calls[i].target)));
console.log(string.concat(" calls[", vm.toString(i), "].value = ", vm.toString(calls[i].value)));
console.log(string.concat(" calls[", vm.toString(i), "].data = ", vm.toString(calls[i].callData)));
totalValue += calls[i].value;
}
if (includeTotalValue) {
console.log(" Total value: %s", totalValue);
}
}
function _packGas(uint256 upper, uint256 lower) internal pure returns (bytes32) {
return bytes32(uint256((upper << 128) | uint128(lower)));
}
}

@ -34,7 +34,7 @@ describe('ERC7579Utils', function () {
const value = 0x012;
const data = encodeSingle(this.target, value, this.target.interface.encodeFunctionData('mockFunction'));
await expect(this.utils.$execSingle(EXEC_TYPE_DEFAULT, data)).to.emit(this.target, 'MockFunctionCalled');
await expect(this.utils.$execSingle(data, EXEC_TYPE_DEFAULT)).to.emit(this.target, 'MockFunctionCalled');
expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value);
});
@ -47,7 +47,7 @@ describe('ERC7579Utils', function () {
this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']),
);
await expect(this.utils.$execSingle(EXEC_TYPE_DEFAULT, data))
await expect(this.utils.$execSingle(data, EXEC_TYPE_DEFAULT))
.to.emit(this.target, 'MockFunctionCalledWithArgs')
.withArgs(42, '0x1234');
@ -62,7 +62,7 @@ describe('ERC7579Utils', function () {
this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
);
await expect(this.utils.$execSingle(EXEC_TYPE_DEFAULT, data)).to.be.revertedWith('CallReceiverMock: reverting');
await expect(this.utils.$execSingle(data, EXEC_TYPE_DEFAULT)).to.be.revertedWith('CallReceiverMock: reverting');
});
it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () {
@ -73,7 +73,7 @@ describe('ERC7579Utils', function () {
this.target.interface.encodeFunctionData('mockFunctionRevertsReason'),
);
await expect(this.utils.$execSingle(EXEC_TYPE_TRY, data))
await expect(this.utils.$execSingle(data, EXEC_TYPE_TRY))
.to.emit(this.utils, 'ERC7579TryExecuteFail')
.withArgs(
CALL_TYPE_CALL,
@ -88,7 +88,7 @@ describe('ERC7579Utils', function () {
const value = 0x012;
const data = encodeSingle(this.target, value, this.target.interface.encodeFunctionData('mockFunction'));
await expect(this.utils.$execSingle('0x03', data))
await expect(this.utils.$execSingle(data, '0x03'))
.to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType')
.withArgs('0x03');
});
@ -103,7 +103,7 @@ describe('ERC7579Utils', function () {
[this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunction')],
);
await expect(this.utils.$execBatch(EXEC_TYPE_DEFAULT, data))
await expect(this.utils.$execBatch(data, EXEC_TYPE_DEFAULT))
.to.emit(this.target, 'MockFunctionCalled')
.to.emit(this.anotherTarget, 'MockFunctionCalled');
@ -123,7 +123,7 @@ describe('ERC7579Utils', function () {
],
);
await expect(this.utils.$execBatch(EXEC_TYPE_DEFAULT, data))
await expect(this.utils.$execBatch(data, EXEC_TYPE_DEFAULT))
.to.emit(this.target, 'MockFunctionCalledWithArgs')
.to.emit(this.anotherTarget, 'MockFunctionCalledWithArgs');
@ -139,7 +139,7 @@ describe('ERC7579Utils', function () {
[this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason')],
);
await expect(this.utils.$execBatch(EXEC_TYPE_DEFAULT, data)).to.be.revertedWith('CallReceiverMock: reverting');
await expect(this.utils.$execBatch(data, EXEC_TYPE_DEFAULT)).to.be.revertedWith('CallReceiverMock: reverting');
});
it('emits ERC7579TryExecuteFail event when any target reverts in try ExecType', async function () {
@ -150,7 +150,7 @@ describe('ERC7579Utils', function () {
[this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason')],
);
await expect(this.utils.$execBatch(EXEC_TYPE_TRY, data))
await expect(this.utils.$execBatch(data, EXEC_TYPE_TRY))
.to.emit(this.utils, 'ERC7579TryExecuteFail')
.withArgs(
CALL_TYPE_BATCH,
@ -173,7 +173,7 @@ describe('ERC7579Utils', function () {
[this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunction')],
);
await expect(this.utils.$execBatch('0x03', data))
await expect(this.utils.$execBatch(data, '0x03'))
.to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType')
.withArgs('0x03');
});
@ -189,20 +189,20 @@ describe('ERC7579Utils', function () {
);
expect(ethers.provider.getStorage(this.utils.target, slot)).to.eventually.equal(ethers.ZeroHash);
await this.utils.$execDelegateCall(EXEC_TYPE_DEFAULT, data);
await this.utils.$execDelegateCall(data, EXEC_TYPE_DEFAULT);
expect(ethers.provider.getStorage(this.utils.target, slot)).to.eventually.equal(value);
});
it('reverts when target reverts in default ExecType', async function () {
const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunctionRevertsReason'));
await expect(this.utils.$execDelegateCall(EXEC_TYPE_DEFAULT, data)).to.be.revertedWith(
await expect(this.utils.$execDelegateCall(data, EXEC_TYPE_DEFAULT)).to.be.revertedWith(
'CallReceiverMock: reverting',
);
});
it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () {
const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunctionRevertsReason'));
await expect(this.utils.$execDelegateCall(EXEC_TYPE_TRY, data))
await expect(this.utils.$execDelegateCall(data, EXEC_TYPE_TRY))
.to.emit(this.utils, 'ERC7579TryExecuteFail')
.withArgs(
CALL_TYPE_CALL,
@ -215,7 +215,7 @@ describe('ERC7579Utils', function () {
it('reverts with an invalid exec type', async function () {
const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunction'));
await expect(this.utils.$execDelegateCall('0x03', data))
await expect(this.utils.$execDelegateCall(data, '0x03'))
.to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType')
.withArgs('0x03');
});

File diff suppressed because one or more lines are too long

Binary file not shown.

@ -0,0 +1 @@
[{"inputs":[{"internalType":"bytes","name":"initCode","type":"bytes"}],"name":"createSender","outputs":[{"internalType":"address","name":"sender","type":"address"}],"stateMutability":"nonpayable","type":"function"}]

@ -96,7 +96,7 @@ describe('Governor', function () {
);
});
shouldSupportInterfaces(['ERC1155Receiver', 'Governor']);
shouldSupportInterfaces(['ERC1155Receiver', 'Governor', 'Governor_5_3']);
shouldBehaveLikeERC6372(mode);
it('deployment check', async function () {

@ -27,7 +27,7 @@ describe('GovernorCountingFractional', function () {
const [owner, proposer, voter1, voter2, voter3, voter4, other] = await ethers.getSigners();
const receiver = await ethers.deployContract('CallReceiverMock');
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]);
const mock = await ethers.deployContract('$GovernorFractionalMock', [
name, // name
votingDelay, // initialVotingDelay

@ -269,7 +269,9 @@ describe('GovernorCountingOverridable', function () {
});
it('can not vote twice', async function () {
await expect(this.mock.connect(this.voter1).castVote(this.helper.id, VoteType.Against));
await expect(this.mock.connect(this.voter1).castVote(this.helper.id, VoteType.Against))
.to.emit(this.mock, 'VoteCast')
.withArgs(this.voter1, this.helper.id, VoteType.Against, ethers.parseEther('5'), '');
await expect(this.mock.connect(this.voter1).castVote(this.helper.id, VoteType.Abstain))
.to.be.revertedWithCustomError(this.mock, 'GovernorAlreadyCastVote')
.withArgs(this.voter1.address);

@ -29,7 +29,7 @@ describe('GovernorERC721', function () {
const [owner, voter1, voter2, voter3, voter4] = await ethers.getSigners();
const receiver = await ethers.deployContract('CallReceiverMock');
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]);
const mock = await ethers.deployContract('$GovernorMock', [
name, // name
votingDelay, // initialVotingDelay

@ -28,7 +28,7 @@ describe('GovernorPreventLateQuorum', function () {
const [owner, proposer, voter1, voter2, voter3, voter4] = await ethers.getSigners();
const receiver = await ethers.deployContract('CallReceiverMock');
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]);
const mock = await ethers.deployContract('$GovernorPreventLateQuorumMock', [
name, // name
votingDelay, // initialVotingDelay

@ -0,0 +1,202 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs');
const { GovernorHelper } = require('../../helpers/governance');
const { VoteType } = require('../../helpers/enums');
const iterate = require('../../helpers/iterate');
const TOKENS = [
{ Token: '$ERC20Votes', mode: 'blocknumber' },
{ Token: '$ERC20VotesTimestampMock', mode: 'timestamp' },
];
const name = 'OZ-Governor';
const version = '1';
const tokenName = 'MockToken';
const tokenSymbol = 'MTKN';
const tokenSupply = ethers.parseEther('100');
const votingDelay = 4n;
const votingPeriod = 16n;
const value = ethers.parseEther('1');
async function deployToken(contractName) {
try {
return await ethers.deployContract(contractName, [tokenName, tokenSymbol, tokenName, version]);
} catch (error) {
if (error.message == 'incorrect number of arguments to constructor') {
// ERC20VotesLegacyMock has a different construction that uses version='1' by default.
return ethers.deployContract(contractName, [tokenName, tokenSymbol, tokenName]);
}
throw error;
}
}
describe('GovernorSequentialProposalId', function () {
for (const { Token, mode } of TOKENS) {
const fixture = async () => {
const [owner, proposer, voter1, voter2, voter3, voter4, userEOA] = await ethers.getSigners();
const receiver = await ethers.deployContract('CallReceiverMock');
const token = await deployToken(Token, [tokenName, tokenSymbol, version]);
const mock = await ethers.deployContract('$GovernorSequentialProposalIdMock', [
name, // name
votingDelay, // initialVotingDelay
votingPeriod, // initialVotingPeriod
0n, // initialProposalThreshold
token, // tokenAddress
10n, // quorumNumeratorValue
]);
await owner.sendTransaction({ to: mock, value });
await token.$_mint(owner, tokenSupply);
const helper = new GovernorHelper(mock, mode);
await helper.connect(owner).delegate({ token: token, to: voter1, value: ethers.parseEther('10') });
await helper.connect(owner).delegate({ token: token, to: voter2, value: ethers.parseEther('7') });
await helper.connect(owner).delegate({ token: token, to: voter3, value: ethers.parseEther('5') });
await helper.connect(owner).delegate({ token: token, to: voter4, value: ethers.parseEther('2') });
return {
owner,
proposer,
voter1,
voter2,
voter3,
voter4,
userEOA,
receiver,
token,
mock,
helper,
};
};
describe(`using ${Token}`, function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
this.proposal = this.helper.setProposal(
[
{
target: this.receiver.target,
data: this.receiver.interface.encodeFunctionData('mockFunction'),
value,
},
],
'<proposal description>',
);
});
it('sequential proposal ids', async function () {
for (const i of iterate.range(1, 10)) {
this.proposal.description = `<proposal description #${i}>`;
expect(this.mock.hashProposal(...this.proposal.shortProposal)).to.eventually.equal(this.proposal.hash);
await expect(this.mock.getProposalId(...this.proposal.shortProposal)).revertedWithCustomError(
this.mock,
'GovernorNonexistentProposal',
);
expect(this.mock.latestProposalId()).to.eventually.equal(i - 1);
await expect(this.helper.connect(this.proposer).propose())
.to.emit(this.mock, 'ProposalCreated')
.withArgs(
i,
this.proposer,
this.proposal.targets,
this.proposal.values,
this.proposal.signatures,
this.proposal.data,
anyValue,
anyValue,
this.proposal.description,
);
expect(this.mock.hashProposal(...this.proposal.shortProposal)).to.eventually.equal(this.proposal.hash);
expect(this.mock.getProposalId(...this.proposal.shortProposal)).to.eventually.equal(i);
expect(this.mock.latestProposalId()).to.eventually.equal(i);
}
});
it('sequential proposal ids with offset start', async function () {
const offset = 69420;
await this.mock.$_initializeLatestProposalId(offset);
for (const i of iterate.range(offset + 1, offset + 10)) {
this.proposal.description = `<proposal description #${i}>`;
expect(this.mock.hashProposal(...this.proposal.shortProposal)).to.eventually.equal(this.proposal.hash);
await expect(this.mock.getProposalId(...this.proposal.shortProposal)).revertedWithCustomError(
this.mock,
'GovernorNonexistentProposal',
);
expect(this.mock.latestProposalId()).to.eventually.equal(i - 1);
await expect(this.helper.connect(this.proposer).propose())
.to.emit(this.mock, 'ProposalCreated')
.withArgs(
i,
this.proposer,
this.proposal.targets,
this.proposal.values,
this.proposal.signatures,
this.proposal.data,
anyValue,
anyValue,
this.proposal.description,
);
expect(this.mock.hashProposal(...this.proposal.shortProposal)).to.eventually.equal(this.proposal.hash);
expect(this.mock.getProposalId(...this.proposal.shortProposal)).to.eventually.equal(i);
expect(this.mock.latestProposalId()).to.eventually.equal(i);
}
});
it('can only initialize latest proposal id from 0', async function () {
await this.helper.propose();
expect(this.mock.latestProposalId()).to.eventually.equal(1);
await expect(this.mock.$_initializeLatestProposalId(2)).to.be.revertedWithCustomError(
this.mock,
'GovernorAlreadyInitializedLatestProposalId',
);
});
it('cannot repropose same proposal', async function () {
await this.helper.connect(this.proposer).propose();
await expect(this.helper.connect(this.proposer).propose())
.to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState')
.withArgs(await this.proposal.id, 0, ethers.ZeroHash);
});
it('nominal workflow', async function () {
await this.helper.connect(this.proposer).propose();
await this.helper.waitForSnapshot();
await expect(this.mock.connect(this.voter1).castVote(1, VoteType.For))
.to.emit(this.mock, 'VoteCast')
.withArgs(this.voter1, 1, VoteType.For, ethers.parseEther('10'), '');
await expect(this.mock.connect(this.voter2).castVote(1, VoteType.For))
.to.emit(this.mock, 'VoteCast')
.withArgs(this.voter2, 1, VoteType.For, ethers.parseEther('7'), '');
await expect(this.mock.connect(this.voter3).castVote(1, VoteType.For))
.to.emit(this.mock, 'VoteCast')
.withArgs(this.voter3, 1, VoteType.For, ethers.parseEther('5'), '');
await expect(this.mock.connect(this.voter4).castVote(1, VoteType.Abstain))
.to.emit(this.mock, 'VoteCast')
.withArgs(this.voter4, 1, VoteType.Abstain, ethers.parseEther('2'), '');
await this.helper.waitForDeadline();
expect(this.helper.execute())
.to.eventually.emit(this.mock, 'ProposalExecuted')
.withArgs(1)
.emit(this.receiver, 'MockFunctionCalled');
});
});
}
});

@ -33,7 +33,7 @@ describe('GovernorStorage', function () {
const [deployer, owner, proposer, voter1, voter2, voter3, voter4] = await ethers.getSigners();
const receiver = await ethers.deployContract('CallReceiverMock');
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]);
const timelock = await ethers.deployContract('TimelockController', [delay, [], [], deployer]);
const mock = await ethers.deployContract('$GovernorStorageMock', [
name,

@ -40,7 +40,7 @@ describe('GovernorTimelockAccess', function () {
const manager = await ethers.deployContract('$AccessManager', [admin]);
const receiver = await ethers.deployContract('$AccessManagedTarget', [manager]);
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]);
const mock = await ethers.deployContract('$GovernorTimelockAccessMock', [
name,
votingDelay,

@ -28,7 +28,7 @@ describe('GovernorTimelockCompound', function () {
const [deployer, owner, voter1, voter2, voter3, voter4, other] = await ethers.getSigners();
const receiver = await ethers.deployContract('CallReceiverMock');
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]);
const predictGovernor = await deployer
.getNonce()
.then(nonce => ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 1 }));

@ -34,7 +34,7 @@ describe('GovernorTimelockControl', function () {
const [deployer, owner, voter1, voter2, voter3, voter4, other] = await ethers.getSigners();
const receiver = await ethers.deployContract('CallReceiverMock');
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]);
const timelock = await ethers.deployContract('TimelockController', [delay, [], [], deployer]);
const mock = await ethers.deployContract('$GovernorTimelockControlMock', [
name,

@ -29,7 +29,7 @@ describe('GovernorVotesQuorumFraction', function () {
const receiver = await ethers.deployContract('CallReceiverMock');
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]);
const mock = await ethers.deployContract('$GovernorMock', [name, votingDelay, votingPeriod, 0n, token, ratio]);
await owner.sendTransaction({ to: mock, value });

@ -31,7 +31,7 @@ describe('GovernorWithParams', function () {
const [owner, proposer, voter1, voter2, voter3, voter4, other] = await ethers.getSigners();
const receiver = await ethers.deployContract('CallReceiverMock');
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]);
const mock = await ethers.deployContract('$GovernorWithParamsMock', [name, token]);
await owner.sendTransaction({ to: mock, value });

@ -0,0 +1,31 @@
const { ethers } = require('hardhat');
const { setCode } = require('@nomicfoundation/hardhat-network-helpers');
const fs = require('fs');
const path = require('path');
const INSTANCES = {
entrypoint: {
address: '0x0000000071727De22E5E9d8BAf0edAc6f37da032',
abi: JSON.parse(fs.readFileSync(path.resolve(__dirname, '../bin/EntryPoint070.abi'), 'utf-8')),
bytecode: fs.readFileSync(path.resolve(__dirname, '../bin/EntryPoint070.bytecode'), 'hex'),
},
sendercreator: {
address: '0xEFC2c1444eBCC4Db75e7613d20C6a62fF67A167C',
abi: JSON.parse(fs.readFileSync(path.resolve(__dirname, '../bin/SenderCreator070.abi'), 'utf-8')),
bytecode: fs.readFileSync(path.resolve(__dirname, '../bin/SenderCreator070.bytecode'), 'hex'),
},
};
function deployEntrypoint() {
return Promise.all(
Object.entries(INSTANCES).map(([name, { address, abi, bytecode }]) =>
setCode(address, '0x' + bytecode.replace(/0x/, ''))
.then(() => ethers.getContractAt(abi, address))
.then(instance => ({ [name]: instance })),
),
).then(namedInstances => Object.assign(...namedInstances));
}
module.exports = {
deployEntrypoint,
};

@ -35,12 +35,16 @@ class GovernorHelper {
return this;
}
get id() {
get hash() {
return ethers.keccak256(
ethers.AbiCoder.defaultAbiCoder().encode(['address[]', 'uint256[]', 'bytes[]', 'bytes32'], this.shortProposal),
);
}
get id() {
return this.governor.latestProposalId ? this.governor.getProposalId(...this.shortProposal) : this.hash;
}
// used for checking events
get signatures() {
return this.data.map(() => '');
@ -106,10 +110,10 @@ class GovernorHelper {
async vote(vote = {}) {
let method = 'castVote'; // default
let args = [this.id, vote.support]; // base
let args = [await this.id, vote.support]; // base
if (vote.signature) {
const sign = await vote.signature(this.governor, this.forgeMessage(vote));
const sign = await this.forgeMessage(vote).then(msg => vote.signature(this.governor, msg));
if (vote.params || vote.reason) {
method = 'castVoteWithReasonAndParamsBySig';
args.push(vote.voter, vote.reason ?? '', vote.params ?? '0x', sign);
@ -130,14 +134,12 @@ class GovernorHelper {
async overrideVote(vote = {}) {
let method = 'castOverrideVote';
let args = [this.id, vote.support];
let args = [await this.id, vote.support];
vote.reason = vote.reason ?? '';
if (vote.signature) {
let message = this.forgeMessage(vote);
message.reason = message.reason ?? '';
const sign = await vote.signature(this.governor, message);
const sign = await this.forgeMessage(vote).then(msg => vote.signature(this.governor, { reason: '', ...msg }));
method = 'castOverrideVoteBySig';
args.push(vote.voter, vote.reason ?? '', sign);
}
@ -147,23 +149,23 @@ class GovernorHelper {
/// Clock helpers
async waitForSnapshot(offset = 0n) {
const timepoint = await this.governor.proposalSnapshot(this.id);
const timepoint = await this.governor.proposalSnapshot(await this.id);
return time.increaseTo[this.mode](timepoint + offset);
}
async waitForDeadline(offset = 0n) {
const timepoint = await this.governor.proposalDeadline(this.id);
const timepoint = await this.governor.proposalDeadline(await this.id);
return time.increaseTo[this.mode](timepoint + offset);
}
async waitForEta(offset = 0n) {
const timestamp = await this.governor.proposalEta(this.id);
const timestamp = await this.governor.proposalEta(await this.id);
return time.increaseTo.timestamp(timestamp + offset);
}
/// Other helpers
forgeMessage(vote = {}) {
const message = { proposalId: this.id, support: vote.support, voter: vote.voter, nonce: vote.nonce };
async forgeMessage(vote = {}) {
const message = { proposalId: await this.id, support: vote.support, voter: vote.voter, nonce: vote.nonce };
if (vote.params || vote.reason) {
message.reason = vote.reason ?? '';

@ -30,7 +30,12 @@ module.exports = {
// ================================================ Object helpers =================================================
// Create a new object by mapping the values through a function, keeping the keys
// Create a new object by mapping the values through a function, keeping the keys. Second function can be used to pre-filter entries
// Example: mapValues({a:1,b:2,c:3}, x => x**2) → {a:1,b:4,c:9}
mapValues: (obj, fn) => Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fn(v)])),
mapValues: (obj, fn, fn2 = () => true) =>
Object.fromEntries(
Object.entries(obj)
.filter(fn2)
.map(([k, v]) => [k, fn(v)]),
),
};

@ -11,7 +11,7 @@ const { shouldBehaveLikeRegularContext } = require('../utils/Context.behavior');
async function fixture() {
const [sender, other] = await ethers.getSigners();
const forwarder = await ethers.deployContract('ERC2771Forwarder', []);
const forwarder = await ethers.deployContract('ERC2771Forwarder', ['ERC2771Forwarder']);
const forwarderAsSigner = await impersonate(forwarder.target);
const context = await ethers.deployContract('ERC2771ContextMock', [forwarder]);
const domain = await getDomain(forwarder);

@ -3,6 +3,34 @@ const { interfaceId } = require('../../helpers/methods');
const { mapValues } = require('../../helpers/iterate');
const INVALID_ID = '0xffffffff';
const GOVERNOR_INTERFACE = [
'name()',
'version()',
'COUNTING_MODE()',
'hashProposal(address[],uint256[],bytes[],bytes32)',
'state(uint256)',
'proposalThreshold()',
'proposalSnapshot(uint256)',
'proposalDeadline(uint256)',
'proposalProposer(uint256)',
'proposalEta(uint256)',
'proposalNeedsQueuing(uint256)',
'votingDelay()',
'votingPeriod()',
'quorum(uint256)',
'getVotes(address,uint256)',
'getVotesWithParams(address,uint256,bytes)',
'hasVoted(uint256,address)',
'propose(address[],uint256[],bytes[],string)',
'queue(address[],uint256[],bytes[],bytes32)',
'execute(address[],uint256[],bytes[],bytes32)',
'cancel(address[],uint256[],bytes[],bytes32)',
'castVote(uint256,uint8)',
'castVoteWithReason(uint256,uint8,string)',
'castVoteWithReasonAndParams(uint256,uint8,string,bytes)',
'castVoteBySig(uint256,uint8,address,bytes)',
'castVoteWithReasonAndParamsBySig(uint256,uint8,address,string,bytes,bytes)',
];
const SIGNATURES = {
ERC165: ['supportsInterface(bytes4)'],
ERC721: [
@ -59,41 +87,23 @@ const SIGNATURES = {
'acceptDefaultAdminTransfer()',
'cancelDefaultAdminTransfer()',
],
Governor: [
'name()',
'version()',
'COUNTING_MODE()',
'hashProposal(address[],uint256[],bytes[],bytes32)',
'state(uint256)',
'proposalThreshold()',
'proposalSnapshot(uint256)',
'proposalDeadline(uint256)',
'proposalProposer(uint256)',
'proposalEta(uint256)',
'proposalNeedsQueuing(uint256)',
'votingDelay()',
'votingPeriod()',
'quorum(uint256)',
'getVotes(address,uint256)',
'getVotesWithParams(address,uint256,bytes)',
'hasVoted(uint256,address)',
'propose(address[],uint256[],bytes[],string)',
'queue(address[],uint256[],bytes[],bytes32)',
'execute(address[],uint256[],bytes[],bytes32)',
'cancel(address[],uint256[],bytes[],bytes32)',
'castVote(uint256,uint8)',
'castVoteWithReason(uint256,uint8,string)',
'castVoteWithReasonAndParams(uint256,uint8,string,bytes)',
'castVoteBySig(uint256,uint8,address,bytes)',
'castVoteWithReasonAndParamsBySig(uint256,uint8,address,string,bytes,bytes)',
],
Governor: GOVERNOR_INTERFACE,
Governor_5_3: GOVERNOR_INTERFACE.concat('getProposalId(address[],uint256[],bytes[],bytes32)'),
ERC2981: ['royaltyInfo(uint256,uint256)'],
};
const INTERFACE_IDS = mapValues(SIGNATURES, interfaceId);
function shouldSupportInterfaces(interfaces = []) {
function shouldSupportInterfaces(interfaces = [], signatures = SIGNATURES) {
// case where only signatures are provided
if (!Array.isArray(interfaces)) {
signatures = interfaces;
interfaces = Object.keys(interfaces);
}
interfaces.unshift('ERC165');
signatures.ERC165 = SIGNATURES.ERC165;
const interfaceIds = mapValues(signatures, interfaceId, ([name]) => interfaces.includes(name));
describe('ERC165', function () {
beforeEach(function () {
@ -103,14 +113,14 @@ function shouldSupportInterfaces(interfaces = []) {
describe('when the interfaceId is supported', function () {
it('uses less than 30k gas', async function () {
for (const k of interfaces) {
const interfaceId = INTERFACE_IDS[k] ?? k;
const interfaceId = interfaceIds[k] ?? k;
expect(await this.contractUnderTest.supportsInterface.estimateGas(interfaceId)).to.lte(30_000n);
}
});
it('returns true', async function () {
for (const k of interfaces) {
const interfaceId = INTERFACE_IDS[k] ?? k;
const interfaceId = interfaceIds[k] ?? k;
expect(await this.contractUnderTest.supportsInterface(interfaceId), `does not support ${k}`).to.be.true;
}
});
@ -129,10 +139,10 @@ function shouldSupportInterfaces(interfaces = []) {
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 (SIGNATURES[k] === undefined) continue;
if (signatures[k] === undefined) continue;
// Check the presence of each function in the contract's interface
for (const fnSig of SIGNATURES[k]) {
for (const fnSig of signatures[k]) {
expect(this.contractUnderTest.interface.hasFunction(fnSig), `did not find ${fnSig}`).to.be.true;
}
}
@ -141,5 +151,7 @@ function shouldSupportInterfaces(interfaces = []) {
}
module.exports = {
SIGNATURES,
INTERFACE_IDS,
shouldSupportInterfaces,
};

Loading…
Cancel
Save