merge 'master' into 'lpcf/add-saturating-math-operators'

pull/5029/head
Lohann Paterno Coutinho Ferreira 7 months ago
commit 79be2062a7
No known key found for this signature in database
GPG Key ID: 2F8CDF07D94CAE05
  1. 5
      .changeset/chilly-humans-warn.md
  2. 5
      .changeset/curvy-crabs-repeat.md
  3. 5
      .changeset/eight-eyes-burn.md
  4. 5
      .changeset/forty-dodos-visit.md
  5. 2
      .changeset/heavy-baboons-give.md
  6. 5
      .changeset/light-news-listen.md
  7. 5
      .changeset/nervous-eyes-teach.md
  8. 5
      .changeset/odd-lobsters-wash.md
  9. 3
      .codecov.yml
  10. 1
      .github/actions/gas-compare/action.yml
  11. 1
      .github/actions/setup/action.yml
  12. 1
      .github/actions/storage-layout/action.yml
  13. 2
      .github/workflows/checks.yml
  14. 22
      .github/workflows/formal-verification.yml
  15. 3
      .gitmodules
  16. 4
      CHANGELOG.md
  17. 2
      CODE_OF_CONDUCT.md
  18. 2
      LICENSE
  19. 4
      certora/diff/access_manager_AccessManager.sol.patch
  20. 19
      contracts/access/manager/AccessManager.sol
  21. 7
      contracts/access/manager/IAccessManager.sol
  22. 14
      contracts/governance/Governor.sol
  23. 7
      contracts/governance/IGovernor.sol
  24. 4
      contracts/governance/README.adoc
  25. 193
      contracts/governance/extensions/GovernorCountingFractional.sol
  26. 12
      contracts/governance/extensions/GovernorCountingSimple.sol
  27. 2
      contracts/metatx/ERC2771Forwarder.sol
  28. 21
      contracts/mocks/AccessManagerMock.sol
  29. 34
      contracts/mocks/ConstructorMock.sol
  30. 2
      contracts/mocks/Stateless.sol
  31. 14
      contracts/mocks/governance/GovernorFractionalMock.sol
  32. 2
      contracts/mocks/governance/GovernorWithParamsMock.sol
  33. 22
      contracts/proxy/Clones.sol
  34. 8
      contracts/proxy/transparent/ProxyAdmin.sol
  35. 11
      contracts/token/ERC1155/extensions/ERC1155Supply.sol
  36. 11
      contracts/token/ERC20/extensions/ERC1363.sol
  37. 40
      contracts/token/ERC20/utils/SafeERC20.sol
  38. 5
      contracts/token/ERC721/ERC721.sol
  39. 6
      contracts/utils/Create2.sol
  40. 5
      contracts/utils/Errors.sol
  41. 1015
      contracts/utils/Packing.sol
  42. 4
      contracts/utils/README.adoc
  43. 24
      contracts/utils/Strings.sol
  44. 316
      contracts/utils/cryptography/P256.sol
  45. 145
      contracts/utils/cryptography/RSA.sol
  46. 21
      contracts/utils/math/Math.sol
  47. 40
      contracts/utils/structs/MerkleTree.sol
  48. 2
      docs/modules/ROOT/pages/governance.adoc
  49. 130
      docs/modules/ROOT/pages/utilities.adoc
  50. 2
      foundry.toml
  51. 4
      fv-requirements.txt
  52. 40
      hardhat.config.js
  53. 1
      lib/halmos-cheatcodes
  54. 400
      package-lock.json
  55. 4
      package.json
  56. 1
      requirements.txt
  57. 2
      scripts/checks/compare-layout.js
  58. 18
      scripts/checks/coverage.sh
  59. 8
      scripts/generate/run.js
  60. 149
      scripts/generate/templates/Arrays.js
  61. 47
      scripts/generate/templates/Checkpoints.js
  62. 48
      scripts/generate/templates/Checkpoints.t.js
  63. 30
      scripts/generate/templates/EnumerableMap.js
  64. 9
      scripts/generate/templates/EnumerableSet.js
  65. 86
      scripts/generate/templates/Packing.js
  66. 3
      scripts/generate/templates/Packing.opts.js
  67. 48
      scripts/generate/templates/Packing.t.js
  68. 65
      scripts/generate/templates/SafeCast.js
  69. 68
      scripts/generate/templates/SlotDerivation.js
  70. 124
      scripts/generate/templates/SlotDerivation.t.js
  71. 50
      scripts/generate/templates/StorageSlot.js
  72. 21
      scripts/generate/templates/StorageSlotMock.js
  73. 56
      test/access/manager/AccessManager.behavior.js
  74. 95
      test/access/manager/AccessManager.test.js
  75. 4
      test/governance/Governor.t.sol
  76. 248
      test/governance/extensions/GovernorCountingFractional.test.js
  77. 4
      test/governance/extensions/GovernorVotesQuorumFraction.test.js
  78. 14
      test/helpers/deploy.js
  79. 2
      test/helpers/enums.js
  80. 44
      test/proxy/Clones.t.sol
  81. 6
      test/token/ERC20/utils/SafeERC20.test.js
  82. 18
      test/utils/Arrays.t.sol
  83. 2
      test/utils/Create2.t.sol
  84. 58
      test/utils/Create2.test.js
  85. 678
      test/utils/Packing.t.sol
  86. 69
      test/utils/Packing.test.js
  87. 72
      test/utils/ShortStrings.t.sol
  88. 45
      test/utils/SlotDerivation.t.sol
  89. 41
      test/utils/Strings.test.js
  90. 135
      test/utils/cryptography/P256.t.sol
  91. 156
      test/utils/cryptography/P256.test.js
  92. 17
      test/utils/cryptography/RSA.helper.js
  93. 97
      test/utils/cryptography/RSA.test.js
  94. 3850
      test/utils/cryptography/SigVer15_186-3.rsp
  95. 3719
      test/utils/cryptography/ecdsa_secp256r1_sha256_p1363_test.json
  96. 4
      test/utils/math/Math.t.sol
  97. 10
      test/utils/math/SignedMath.t.sol

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': patch
---
`ProxyAdmin`: Fixed documentation for `UPGRADE_INTERFACE_VERSION` getter.

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`RSA`: Library to verify signatures according to RFC 8017 Signature Verification Operation

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`GovernorCountingFractional`: Add a governor counting module that allows distributing voting power amongst 3 options (For, Against, Abstain).

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`Strings`: Added a utility function for converting an address to checksummed string.

@ -2,4 +2,4 @@
'openzeppelin-solidity': minor
---
`Packing`: Added a new utility for packing and unpacking multiple values into a single bytes32. Includes initial support for packing two `uint128` in an `Uint128x2` type.
`Packing`: Added a new utility for packing, extracting and replacing bytesXX values.

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`AccessManager`: Allow the `onlyAuthorized` modifier to restrict functions added to the manager.

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`Create2`: Bubbles up returndata from a deployed contract that reverted during construction.

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`P256`: Library for verification and public key recovery of P256 (aka secp256r1) signatures.

@ -10,3 +10,6 @@ coverage:
project:
default:
threshold: 1%
ignore:
- "test"
- "contracts/mocks"

@ -1,4 +1,5 @@
name: Compare gas costs
description: Compare gas costs between branches
inputs:
token:
description: github token

@ -1,4 +1,5 @@
name: Setup
description: Common environment setup
runs:
using: composite

@ -1,4 +1,5 @@
name: Compare storage layouts
description: Compare storage layouts between branches
inputs:
token:
description: github token

@ -115,7 +115,7 @@ jobs:
- name: Set up environment
uses: ./.github/actions/setup
- run: rm foundry.toml
- uses: crytic/slither-action@v0.3.2
- uses: crytic/slither-action@v0.4.0
with:
node-version: 18.15
slither-version: 0.10.1

@ -44,12 +44,13 @@ jobs:
fi
echo "result=$RESULT" >> "$GITHUB_OUTPUT"
- name: Install python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ env.PIP_VERSION }}
cache: 'pip'
cache-dependency-path: 'fv-requirements.txt'
- name: Install python packages
run: pip install -r requirements.txt
run: pip install -r fv-requirements.txt
- name: Install java
uses: actions/setup-java@v3
with:
@ -66,3 +67,20 @@ jobs:
node certora/run.js ${{ steps.arguments.outputs.result }} >> "$GITHUB_STEP_SUMMARY"
env:
CERTORAKEY: ${{ secrets.CERTORAKEY }}
halmos:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up environment
uses: ./.github/actions/setup
- name: Install python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PIP_VERSION }}
cache: 'pip'
cache-dependency-path: 'fv-requirements.txt'
- name: Install python packages
run: pip install -r fv-requirements.txt
- name: Run Halmos
run: halmos --match-test '^symbolic|^testSymbolic' -vv

3
.gitmodules vendored

@ -5,3 +5,6 @@
[submodule "lib/erc4626-tests"]
path = lib/erc4626-tests
url = https://github.com/a16z/erc4626-tests.git
[submodule "lib/halmos-cheatcodes"]
path = lib/halmos-cheatcodes
url = https://github.com/a16z/halmos-cheatcodes

@ -3,6 +3,7 @@
### Breaking changes
- `ERC1967Utils`: Removed duplicate declaration of the `Upgraded`, `AdminChanged` and `BeaconUpgraded` events. These events are still available through the `IERC1967` interface located under the `contracts/interfaces/` directory. Minimum pragma version is now 0.8.21.
- `Governor`, `GovernorCountingSimple`: The `_countVotes` virtual function now returns an `uint256` with the total votes casted. This change allows for more flexibility for partial and fractional voting. Upgrading users may get a compilation error that can be fixed by adding a return statement to the `_countVotes` function.
### Custom error changes
@ -13,6 +14,9 @@ This version comes with changes to the custom error identifiers. Contracts previ
- Replace `Clones.Create2InsufficientBalance` with `Errors.InsufficientBalance`
- Replace `Clones.ERC1167FailedCreateClone` with `Errors.FailedDeployment`
- Replace `Clones.Create2FailedDeployment` with `Errors.FailedDeployment`
- `SafeERC20`: Replace `Address.AddressEmptyCode` with `SafeERC20FailedOperation` if there is no code at the token's address.
- `SafeERC20`: Replace generic `Error(string)` with `SafeERC20FailedOperation` if the returned data can't be decoded as `bool`.
- `SafeERC20`: Replace generic `SafeERC20FailedOperation` with the revert message from the contract call if it fails.
## 5.0.2 (2024-02-29)

@ -6,7 +6,7 @@ In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
level of experience, education, socioeconomic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2016-2024 Zeppelin Group Ltd and contributors
Copyright (c) 2016-2024 Zeppelin Group Ltd
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

@ -64,8 +64,8 @@
*/
function _getAdminRestrictions(
bytes calldata data
- ) private view returns (bool restricted, uint64 roleAdminId, uint32 executionDelay) {
+ ) internal view returns (bool restricted, uint64 roleAdminId, uint32 executionDelay) { // private → internal for FV
- ) private view returns (bool adminRestricted, uint64 roleAdminId, uint32 executionDelay) {
+ ) internal view returns (bool adminRestricted, uint64 roleAdminId, uint32 executionDelay) { // private → internal for FV
if (data.length < 4) {
return (false, 0, 0);
}

@ -412,9 +412,6 @@ contract AccessManager is Context, Multicall, IAccessManager {
* Emits a {TargetClosed} event.
*/
function _setTargetClosed(address target, bool closed) internal virtual {
if (target == address(this)) {
revert AccessManagerLockedAccount(target);
}
_targets[target].closed = closed;
emit TargetClosed(target, closed);
}
@ -586,7 +583,9 @@ contract AccessManager is Context, Multicall, IAccessManager {
// ================================================= ADMIN LOGIC ==================================================
/**
* @dev Check if the current call is authorized according to admin logic.
* @dev Check if the current call is authorized according to admin and roles logic.
*
* WARNING: Carefully review the considerations of {AccessManaged-restricted} since they apply to this modifier.
*/
function _checkAuthorized() private {
address caller = _msgSender();
@ -611,7 +610,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
*/
function _getAdminRestrictions(
bytes calldata data
) private view returns (bool restricted, uint64 roleAdminId, uint32 executionDelay) {
) private view returns (bool adminRestricted, uint64 roleAdminId, uint32 executionDelay) {
if (data.length < 4) {
return (false, 0, 0);
}
@ -648,7 +647,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
return (true, getRoleAdmin(roleId), 0);
}
return (false, 0, 0);
return (false, getTargetFunctionRole(address(this), selector), 0);
}
// =================================================== HELPERS ====================================================
@ -673,7 +672,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
}
/**
* @dev A version of {canCall} that checks for admin restrictions in this contract.
* @dev A version of {canCall} that checks for restrictions in this contract.
*/
function _canCallSelf(address caller, bytes calldata data) private view returns (bool immediate, uint32 delay) {
if (data.length < 4) {
@ -686,8 +685,10 @@ contract AccessManager is Context, Multicall, IAccessManager {
return (_isExecuting(address(this), _checkSelector(data)), 0);
}
(bool enabled, uint64 roleId, uint32 operationDelay) = _getAdminRestrictions(data);
if (!enabled) {
(bool adminRestricted, uint64 roleId, uint32 operationDelay) = _getAdminRestrictions(data);
// isTragetClosed apply to non-admin-restricted function
if (!adminRestricted && isTargetClosed(address(this))) {
return (false, 0);
}

@ -82,7 +82,6 @@ interface IAccessManager {
error AccessManagerNotScheduled(bytes32 operationId);
error AccessManagerNotReady(bytes32 operationId);
error AccessManagerExpired(bytes32 operationId);
error AccessManagerLockedAccount(address account);
error AccessManagerLockedRole(uint64 roleId);
error AccessManagerBadConfirmation();
error AccessManagerUnauthorizedAccount(address msgsender, uint64 roleId);
@ -108,7 +107,7 @@ interface IAccessManager {
* is backward compatible. Some contracts may thus ignore the second return argument. In that case they will fail
* to identify the indirect workflow, and will consider calls that require a delay to be forbidden.
*
* NOTE: This function does not report the permissions of this manager itself. These are defined by the
* NOTE: This function does not report the permissions of the admin functions in the manager itself. These are defined by the
* {AccessManager} documentation.
*/
function canCall(
@ -134,6 +133,8 @@ interface IAccessManager {
/**
* @dev Get whether the contract is closed disabling any access. Otherwise role permissions are applied.
*
* NOTE: When the manager itself is closed, admin functions are still accessible to avoid locking the contract.
*/
function isTargetClosed(address target) external view returns (bool);
@ -308,6 +309,8 @@ interface IAccessManager {
/**
* @dev Set the closed flag for a contract.
*
* Closing the manager itself won't disable access to admin methods to avoid locking the contract.
*
* Requirements:
*
* - the caller must be a global admin

@ -255,9 +255,9 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
uint256 proposalId,
address account,
uint8 support,
uint256 weight,
uint256 totalWeight,
bytes memory params
) internal virtual;
) internal virtual returns (uint256);
/**
* @dev Default additional encoded parameters used by castVote methods that don't include them
@ -639,16 +639,16 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
) internal virtual returns (uint256) {
_validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Active));
uint256 weight = _getVotes(account, proposalSnapshot(proposalId), params);
_countVote(proposalId, account, support, weight, params);
uint256 totalWeight = _getVotes(account, proposalSnapshot(proposalId), params);
uint256 votedWeight = _countVote(proposalId, account, support, totalWeight, params);
if (params.length == 0) {
emit VoteCast(account, proposalId, support, weight, reason);
emit VoteCast(account, proposalId, support, votedWeight, reason);
} else {
emit VoteCastWithParams(account, proposalId, support, weight, reason, params);
emit VoteCastWithParams(account, proposalId, support, votedWeight, reason, params);
}
return weight;
return votedWeight;
}
/**

@ -83,6 +83,11 @@ interface IGovernor is IERC165, IERC6372 {
*/
error GovernorInvalidVoteType();
/**
* @dev The provided params buffer is not supported by the counting module.
*/
error GovernorInvalidVoteParams();
/**
* @dev Queue operation is not implemented for this governor. Execute should be called directly.
*/
@ -145,7 +150,7 @@ interface IGovernor is IERC165, IERC6372 {
* @dev Emitted when a vote is cast with params.
*
* Note: `support` values should be seen as buckets. Their interpretation depends on the voting module used.
* `params` are additional encoded parameters. Their interpepretation also depends on the voting module used.
* `params` are additional encoded parameters. Their interpretation also depends on the voting module used.
*/
event VoteCastWithParams(
address indexed voter,

@ -28,6 +28,8 @@ Counting modules determine valid voting options.
* {GovernorCountingSimple}: Simple voting mechanism with 3 voting options: Against, For and Abstain.
* {GovernorCountingFractional}: A more modular voting system that allows a user to vote with only part of its voting power, and to split that weight arbitrarily between the 3 different options (Against, For and Abstain).
Timelock extensions add a delay for governance decisions to be executed. The workflow is extended to require a `queue` step before execution. With these modules, proposals are executed by the external timelock contract, thus it is the timelock that has to hold the assets that are being governed.
* {GovernorTimelockAccess}: Connects with an instance of an {AccessManager}. This allows restrictions (and delays) enforced by the manager to be considered by the Governor and integrated into the AccessManager's "schedule + execute" workflow.
@ -62,6 +64,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you
{{GovernorCountingSimple}}
{{GovernorCountingFractional}}
{{GovernorVotes}}
{{GovernorVotesQuorumFraction}}

@ -0,0 +1,193 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Governor} from "../Governor.sol";
import {GovernorCountingSimple} from "./GovernorCountingSimple.sol";
import {Math} from "../../utils/math/Math.sol";
/**
* @dev Extension of {Governor} for fractional voting.
*
* Similar to {GovernorCountingSimple}, this contract is a votes counting module for {Governor} that supports 3 options:
* Against, For, Abstain. Additionally, it includes a fourth option: Fractional, which allows voters to split their voting
* power amongst the other 3 options.
*
* Votes cast with the Fractional support must be accompanied by a `params` argument that is three packed `uint128` values
* representing the weight the delegate assigns to Against, For, and Abstain respectively. For those votes cast for the other
* 3 options, the `params` argument must be empty.
*
* This is mostly useful when the delegate is a contract that implements its own rules for voting. These delegate-contracts
* can cast fractional votes according to the preferences of multiple entities delegating their voting power.
*
* Some example use cases include:
*
* * Voting from tokens that are held by a DeFi pool
* * Voting from an L2 with tokens held by a bridge
* * Voting privately from a shielded pool using zero knowledge proofs.
*
* Based on ScopeLift's GovernorCountingFractional[https://github.com/ScopeLift/flexible-voting/blob/e5de2efd1368387b840931f19f3c184c85842761/src/GovernorCountingFractional.sol]
*/
abstract contract GovernorCountingFractional is Governor {
using Math for *;
uint8 internal constant VOTE_TYPE_FRACTIONAL = 255;
struct ProposalVote {
uint256 againstVotes;
uint256 forVotes;
uint256 abstainVotes;
mapping(address voter => uint256) usedVotes;
}
/**
* @dev Mapping from proposal ID to vote tallies for that proposal.
*/
mapping(uint256 => ProposalVote) private _proposalVotes;
/**
* @dev A fractional vote params uses more votes than are available for that user.
*/
error GovernorExceedRemainingWeight(address voter, uint256 usedVotes, uint256 remainingWeight);
/**
* @dev See {IGovernor-COUNTING_MODE}.
*/
// solhint-disable-next-line func-name-mixedcase
function COUNTING_MODE() public pure virtual override returns (string memory) {
return "support=bravo,fractional&quorum=for,abstain&params=fractional";
}
/**
* @dev See {IGovernor-hasVoted}.
*/
function hasVoted(uint256 proposalId, address account) public view virtual override returns (bool) {
return usedVotes(proposalId, account) > 0;
}
/**
* @dev Get the number of votes already cast by `account` for a proposal with `proposalId`. Useful for
* integrations that allow delegates to cast rolling, partial votes.
*/
function usedVotes(uint256 proposalId, address account) public view virtual returns (uint256) {
return _proposalVotes[proposalId].usedVotes[account];
}
/**
* @dev Get current distribution of votes for a given proposal.
*/
function proposalVotes(
uint256 proposalId
) public view virtual returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) {
ProposalVote storage proposalVote = _proposalVotes[proposalId];
return (proposalVote.againstVotes, proposalVote.forVotes, proposalVote.abstainVotes);
}
/**
* @dev See {Governor-_quorumReached}.
*/
function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) {
ProposalVote storage proposalVote = _proposalVotes[proposalId];
return quorum(proposalSnapshot(proposalId)) <= proposalVote.forVotes + proposalVote.abstainVotes;
}
/**
* @dev See {Governor-_voteSucceeded}. In this module, forVotes must be > againstVotes.
*/
function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) {
ProposalVote storage proposalVote = _proposalVotes[proposalId];
return proposalVote.forVotes > proposalVote.againstVotes;
}
/**
* @dev See {Governor-_countVote}. Function that records the delegate's votes.
*
* Executing this function consumes (part of) the delegate's weight on the proposal. This weight can be
* distributed amongst the 3 options (Against, For, Abstain) by specifying a fractional `support`.
*
* This counting module supports two vote casting modes: nominal and fractional.
*
* - Nominal: A nominal vote is cast by setting `support` to one of the 3 bravo options (Against, For, Abstain).
* - Fractional: A fractional vote is cast by setting `support` to `type(uint8).max` (255).
*
* Casting a nominal vote requires `params` to be empty and consumes the delegate's full remaining weight on the
* proposal for the specified `support` option. This is similar to the {GovernorCountingSimple} module and follows
* the `VoteType` enum from Governor Bravo. As a consequence, no vote weight remains unspent so no further voting
* is possible (for this `proposalId` and this `account`).
*
* Casting a fractional vote consumes a fraction of the delegate's remaining weight on the proposal according to the
* weights the delegate assigns to each support option (Against, For, Abstain respectively). The sum total of the
* three decoded vote weights _must_ be less than or equal to the delegate's remaining weight on the proposal (i.e.
* their checkpointed total weight minus votes already cast on the proposal). This format can be produced using:
*
* `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))`
*
* NOTE: Consider that fractional voting restricts the number of casted vote (in each category) to 128 bits.
* Depending on how many decimals the underlying token has, a single voter may require to split their vote into
* multiple vote operations. For precision higher than ~30 decimals, large token holders may require an
* potentially large number of calls to cast all their votes. The voter has the possibility to cast all the
* remaining votes in a single operation using the traditional "bravo" vote.
*/
// slither-disable-next-line cyclomatic-complexity
function _countVote(
uint256 proposalId,
address account,
uint8 support,
uint256 totalWeight,
bytes memory params
) internal virtual override returns (uint256) {
// Compute number of remaining votes. Returns 0 on overflow.
(, uint256 remainingWeight) = totalWeight.trySub(usedVotes(proposalId, account));
if (remainingWeight == 0) {
revert GovernorAlreadyCastVote(account);
}
uint256 againstVotes = 0;
uint256 forVotes = 0;
uint256 abstainVotes = 0;
uint256 usedWeight;
// For clarity of event indexing, fractional voting must be clearly advertised in the "support" field.
//
// Supported `support` value must be:
// - "Full" voting: `support = 0` (Against), `1` (For) or `2` (Abstain), with empty params.
// - "Fractional" voting: `support = 255`, with 48 bytes params.
if (support == uint8(GovernorCountingSimple.VoteType.Against)) {
if (params.length != 0) revert GovernorInvalidVoteParams();
usedWeight = againstVotes = remainingWeight;
} else if (support == uint8(GovernorCountingSimple.VoteType.For)) {
if (params.length != 0) revert GovernorInvalidVoteParams();
usedWeight = forVotes = remainingWeight;
} else if (support == uint8(GovernorCountingSimple.VoteType.Abstain)) {
if (params.length != 0) revert GovernorInvalidVoteParams();
usedWeight = abstainVotes = remainingWeight;
} else if (support == VOTE_TYPE_FRACTIONAL) {
// The `params` argument is expected to be three packed `uint128`:
// `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))`
if (params.length != 0x30) revert GovernorInvalidVoteParams();
assembly ("memory-safe") {
againstVotes := shr(128, mload(add(params, 0x20)))
forVotes := shr(128, mload(add(params, 0x30)))
abstainVotes := shr(128, mload(add(params, 0x40)))
usedWeight := add(add(againstVotes, forVotes), abstainVotes) // inputs are uint128: cannot overflow
}
// check parsed arguments are valid
if (usedWeight > remainingWeight) {
revert GovernorExceedRemainingWeight(account, usedWeight, remainingWeight);
}
} else {
revert GovernorInvalidVoteType();
}
// update votes tracking
ProposalVote storage details = _proposalVotes[proposalId];
if (againstVotes > 0) details.againstVotes += againstVotes;
if (forVotes > 0) details.forVotes += forVotes;
if (abstainVotes > 0) details.abstainVotes += abstainVotes;
details.usedVotes[account] += usedWeight;
return usedWeight;
}
}

@ -77,9 +77,9 @@ abstract contract GovernorCountingSimple is Governor {
uint256 proposalId,
address account,
uint8 support,
uint256 weight,
uint256 totalWeight,
bytes memory // params
) internal virtual override {
) internal virtual override returns (uint256) {
ProposalVote storage proposalVote = _proposalVotes[proposalId];
if (proposalVote.hasVoted[account]) {
@ -88,13 +88,15 @@ abstract contract GovernorCountingSimple is Governor {
proposalVote.hasVoted[account] = true;
if (support == uint8(VoteType.Against)) {
proposalVote.againstVotes += weight;
proposalVote.againstVotes += totalWeight;
} else if (support == uint8(VoteType.For)) {
proposalVote.forVotes += weight;
proposalVote.forVotes += totalWeight;
} else if (support == uint8(VoteType.Abstain)) {
proposalVote.abstainVotes += weight;
proposalVote.abstainVotes += totalWeight;
} else {
revert GovernorInvalidVoteType();
}
return totalWeight;
}
}

@ -347,7 +347,7 @@ contract ERC2771Forwarder is EIP712, Nonces {
// We can't know X after CALL dynamic costs, but we want it to be such that X * 63 / 64 >= req.gas.
// Let Y be the gas used in the subcall. gasleft() measured immediately after the subcall will be gasleft() = X - Y.
// If the subcall ran out of gas, then Y = X * 63 / 64 and gasleft() = X - Y = X / 64.
// Under this assumption req.gas / 63 > gasleft() is true is true if and only if
// Under this assumption req.gas / 63 > gasleft() is true if and only if
// req.gas / 63 > X / 64, or equivalently req.gas > X * 63 / 64.
// This means that if the subcall runs out of gas we are able to detect that insufficient gas was passed.
//

@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {AccessManager} from "../access/manager/AccessManager.sol";
import {StorageSlot} from "../utils/StorageSlot.sol";
contract AccessManagerMock is AccessManager {
event CalledRestricted(address caller);
event CalledUnrestricted(address caller);
constructor(address initialAdmin) AccessManager(initialAdmin) {}
function fnRestricted() public onlyAuthorized {
emit CalledRestricted(msg.sender);
}
function fnUnrestricted() public {
emit CalledUnrestricted(msg.sender);
}
}

@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract ConstructorMock {
bool foo;
enum RevertType {
None,
RevertWithoutMessage,
RevertWithMessage,
RevertWithCustomError,
Panic
}
error CustomError();
constructor(RevertType error) {
// After transpilation to upgradeable contract, the constructor will become an initializer
// To silence the `... can be restricted to view` warning, we write to state
foo = true;
if (error == RevertType.RevertWithoutMessage) {
revert();
} else if (error == RevertType.RevertWithMessage) {
revert("ConstructorMock: reverting");
} else if (error == RevertType.RevertWithCustomError) {
revert CustomError();
} else if (error == RevertType.Panic) {
uint256 a = uint256(0) / uint256(0);
a;
}
}
}

@ -25,8 +25,10 @@ import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol";
import {Math} from "../utils/math/Math.sol";
import {MerkleProof} from "../utils/cryptography/MerkleProof.sol";
import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol";
import {P256} from "../utils/cryptography/P256.sol";
import {Panic} from "../utils/Panic.sol";
import {Packing} from "../utils/Packing.sol";
import {RSA} from "../utils/cryptography/RSA.sol";
import {SafeCast} from "../utils/math/SafeCast.sol";
import {SafeERC20} from "../token/ERC20/utils/SafeERC20.sol";
import {ShortStrings} from "../utils/ShortStrings.sol";

@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Governor} from "../../governance/Governor.sol";
import {GovernorSettings} from "../../governance/extensions/GovernorSettings.sol";
import {GovernorCountingFractional} from "../../governance/extensions/GovernorCountingFractional.sol";
import {GovernorVotesQuorumFraction} from "../../governance/extensions/GovernorVotesQuorumFraction.sol";
abstract contract GovernorFractionalMock is GovernorSettings, GovernorVotesQuorumFraction, GovernorCountingFractional {
function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) {
return super.proposalThreshold();
}
}

@ -41,7 +41,7 @@ abstract contract GovernorWithParamsMock is GovernorVotes, GovernorCountingSimpl
uint8 support,
uint256 weight,
bytes memory params
) internal override(Governor, GovernorCountingSimple) {
) internal override(Governor, GovernorCountingSimple) returns (uint256) {
if (params.length > 0) {
(uint256 _uintParam, string memory _strParam) = abi.decode(params, (uint256, string));
emit CountParams(_uintParam, _strParam);

@ -39,12 +39,11 @@ library Clones {
}
/// @solidity memory-safe-assembly
assembly {
// Stores the bytecode after address
mstore(0x20, 0x5af43d82803e903d91602b57fd5bf3)
// implementation address
mstore(0x11, implementation)
// Packs the first 3 bytes of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0x88, implementation), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create(value, 0x09, 0x37)
}
if (instance == address(0)) {
@ -80,12 +79,11 @@ library Clones {
}
/// @solidity memory-safe-assembly
assembly {
// Stores the bytecode after address
mstore(0x20, 0x5af43d82803e903d91602b57fd5bf3)
// implementation address
mstore(0x11, implementation)
// Packs the first 3 bytes of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0x88, implementation), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create2(value, 0x09, 0x37, salt)
}
if (instance == address(0)) {

@ -12,10 +12,10 @@ import {Ownable} from "../../access/Ownable.sol";
*/
contract ProxyAdmin is Ownable {
/**
* @dev The version of the upgrade interface of the contract. If this getter is missing, both `upgrade(address)`
* and `upgradeAndCall(address,bytes)` are present, and `upgradeTo` must be used if no function should be called,
* while `upgradeAndCall` will invoke the `receive` function if the second argument is the empty byte string.
* If the getter returns `"5.0.0"`, only `upgradeAndCall(address,bytes)` is present, and the second argument must
* @dev The version of the upgrade interface of the contract. If this getter is missing, both `upgrade(address,address)`
* and `upgradeAndCall(address,address,bytes)` are present, and `upgrade` must be used if no function should be called,
* while `upgradeAndCall` will invoke the `receive` function if the third argument is the empty byte string.
* If the getter returns `"5.0.0"`, only `upgradeAndCall(address,address,bytes)` is present, and the third argument must
* be the empty byte string if no function should be called, making it impossible to invoke the `receive` function
* during an upgrade.
*/

@ -4,6 +4,7 @@
pragma solidity ^0.8.20;
import {ERC1155} from "../ERC1155.sol";
import {Arrays} from "../../../utils/Arrays.sol";
/**
* @dev Extension of ERC-1155 that adds tracking of total supply per id.
@ -19,6 +20,8 @@ import {ERC1155} from "../ERC1155.sol";
* CAUTION: This extension should not be added in an upgrade to an already deployed contract.
*/
abstract contract ERC1155Supply is ERC1155 {
using Arrays for uint256[];
mapping(uint256 id => uint256) private _totalSupply;
uint256 private _totalSupplyAll;
@ -57,9 +60,9 @@ abstract contract ERC1155Supply is ERC1155 {
if (from == address(0)) {
uint256 totalMintValue = 0;
for (uint256 i = 0; i < ids.length; ++i) {
uint256 value = values[i];
uint256 value = values.unsafeMemoryAccess(i);
// Overflow check required: The rest of the code assumes that totalSupply never overflows
_totalSupply[ids[i]] += value;
_totalSupply[ids.unsafeMemoryAccess(i)] += value;
totalMintValue += value;
}
// Overflow check required: The rest of the code assumes that totalSupplyAll never overflows
@ -69,11 +72,11 @@ abstract contract ERC1155Supply is ERC1155 {
if (to == address(0)) {
uint256 totalBurnValue = 0;
for (uint256 i = 0; i < ids.length; ++i) {
uint256 value = values[i];
uint256 value = values.unsafeMemoryAccess(i);
unchecked {
// Overflow not possible: values[i] <= balanceOf(from, ids[i]) <= totalSupply(ids[i])
_totalSupply[ids[i]] -= value;
_totalSupply[ids.unsafeMemoryAccess(i)] -= value;
// Overflow not possible: sum_i(values[i]) <= sum_i(totalSupply(ids[i])) <= totalSupplyAll
totalBurnValue += value;
}

@ -30,16 +30,23 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 {
/**
* @dev Indicates a failure within the {transfer} part of a transferAndCall operation.
* @param receiver Address to which tokens are being transferred.
* @param value Amount of tokens to be transferred.
*/
error ERC1363TransferFailed(address to, uint256 value);
error ERC1363TransferFailed(address receiver, uint256 value);
/**
* @dev Indicates a failure within the {transferFrom} part of a transferFromAndCall operation.
* @param sender Address from which to send tokens.
* @param receiver Address to which tokens are being transferred.
* @param value Amount of tokens to be transferred.
*/
error ERC1363TransferFromFailed(address from, address to, uint256 value);
error ERC1363TransferFromFailed(address sender, address receiver, uint256 value);
/**
* @dev Indicates a failure within the {approve} part of a approveAndCall operation.
* @param spender Address which will spend the funds.
* @param value Amount of tokens to be spent.
*/
error ERC1363ApproveFailed(address spender, uint256 value);

@ -17,8 +17,6 @@ import {Address} from "../../../utils/Address.sol";
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
*/
library SafeERC20 {
using Address for address;
/**
* @dev An operation with an ERC-20 token failed.
*/
@ -142,14 +140,25 @@ library SafeERC20 {
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that
// the target address contains contract code and also asserts for success in the low-level call.
uint256 returnSize;
uint256 returnValue;
assembly ("memory-safe") {
let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
// bubble errors
if iszero(success) {
let ptr := mload(0x40)
returndatacopy(ptr, 0, returndatasize())
revert(ptr, returndatasize())
}
returnSize := returndatasize()
returnValue := mload(0)
}
bytes memory returndata = address(token).functionCall(data);
if (returndata.length != 0 && !abi.decode(returndata, (bool))) {
if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
revert SafeERC20FailedOperation(address(token));
}
}
@ -160,14 +169,17 @@ library SafeERC20 {
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead.
* This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.
*/
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false
// and not revert is the subcall reverts.
(bool success, bytes memory returndata) = address(token).call(data);
return success && (returndata.length == 0 || abi.decode(returndata, (bool))) && address(token).code.length > 0;
bool success;
uint256 returnSize;
uint256 returnValue;
assembly ("memory-safe") {
success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
returnSize := returndatasize()
returnValue := mload(0)
}
return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);
}
}

@ -195,8 +195,9 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
/**
* @dev Checks if `spender` can operate on `tokenId`, assuming the provided `owner` is the actual owner.
* Reverts if `spender` does not have approval from the provided `owner` for the given token or for all its assets
* the `spender` for the specific `tokenId`.
* Reverts if:
* - `spender` does not have approval from `owner` for `tokenId`.
* - `spender` does not have approval to manage all of `owner`'s assets.
*
* WARNING: This function assumes that `owner` is the actual owner of `tokenId` and does not verify this
* assumption.

@ -44,6 +44,12 @@ library Create2 {
/// @solidity memory-safe-assembly
assembly {
addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)
// if no address was created, and returndata is not empty, bubble revert
if and(iszero(addr), not(iszero(returndatasize()))) {
let p := mload(0x40)
returndatacopy(p, 0, returndatasize())
revert(p, returndatasize())
}
}
if (addr == address(0)) {
revert Errors.FailedDeployment();

@ -23,4 +23,9 @@ library Errors {
* @dev The deployment failed.
*/
error FailedDeployment();
/**
* @dev A necessary precompile is missing.
*/
error MissingPrecompile(address);
}

File diff suppressed because it is too large Load Diff

@ -8,6 +8,8 @@ Miscellaneous contracts and libraries containing utility functions you can use t
* {Math}, {SignedMath}: Implementation of various arithmetic functions.
* {SafeCast}: Checked downcasting functions to avoid silent truncation.
* {ECDSA}, {MessageHashUtils}: Libraries for interacting with ECDSA signatures.
* {P256}: Library for verifying and recovering public keys from secp256r1 signatures.
* {RSA}: Library with RSA PKCS#1 v1.5 signature verification utilities.
* {SignatureChecker}: A library helper to support regular ECDSA from EOAs as well as ERC-1271 signatures for smart contracts.
* {Hashes}: Commonly used hash functions.
* {MerkleProof}: Functions for verifying https://en.wikipedia.org/wiki/Merkle_tree[Merkle Tree] proofs.
@ -16,7 +18,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
* {ReentrancyGuardTransient}: Variant of {ReentrancyGuard} that uses transient storage (https://eips.ethereum.org/EIPS/eip-1153[EIP-1153]).
* {Pausable}: A common emergency response mechanism that can pause functionality while a remediation is pending.
* {Nonces}: Utility for tracking and verifying address nonces that only increment.
* {ERC165, ERC165Checker}: Utilities for inspecting interfaces supported by contracts.
* {ERC165}, {ERC165Checker}: Utilities for inspecting interfaces supported by contracts.
* {BitMaps}: A simple library to manage boolean value mapped to a numerical index in an efficient way.
* {EnumerableMap}: A type like Solidity's https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`], but with key-value _enumeration_: this will let you know how many entries a mapping has, and iterate over them (which is not possible with `mapping`).
* {EnumerableSet}: Like {EnumerableMap}, but for https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets]. Can be used to store privileged accounts, issued IDs, etc.

@ -85,6 +85,30 @@ library Strings {
return toHexString(uint256(uint160(addr)), ADDRESS_LENGTH);
}
/**
* @dev Converts an `address` with fixed length of 20 bytes to its checksummed ASCII `string` hexadecimal
* representation, according to EIP-55.
*/
function toChecksumHexString(address addr) internal pure returns (string memory) {
bytes memory buffer = bytes(toHexString(addr));
// hash the hex part of buffer (skip length + 2 bytes, length 40)
uint256 hashValue;
assembly ("memory-safe") {
hashValue := shr(96, keccak256(add(buffer, 0x22), 40))
}
for (uint256 i = 41; i > 1; --i) {
// possible values for buffer[i] are 48 (0) to 57 (9) and 97 (a) to 102 (f)
if (hashValue & 0xf > 7 && uint8(buffer[i]) > 96) {
// case shift by xoring with 0x20
buffer[i] ^= 0x20;
}
hashValue >>= 4;
}
return string(buffer);
}
/**
* @dev Returns true if the two strings are equal.
*/

@ -0,0 +1,316 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Math} from "../math/Math.sol";
import {Errors} from "../Errors.sol";
/**
* @dev Implementation of secp256r1 verification and recovery functions.
*
* The secp256r1 curve (also known as P256) is a NIST standard curve with wide support in modern devices
* and cryptographic standards. Some notable examples include Apple's Secure Enclave and Android's Keystore
* as well as authentication protocols like FIDO2.
*
* Based on the original https://github.com/itsobvioustech/aa-passkeys-wallet/blob/main/src/Secp256r1.sol[implementation of itsobvioustech].
* Heavily inspired in https://github.com/maxrobot/elliptic-solidity/blob/master/contracts/Secp256r1.sol[maxrobot] and
* https://github.com/tdrerup/elliptic-curve-solidity/blob/master/contracts/curves/EllipticCurve.sol[tdrerup] implementations.
*/
library P256 {
struct JPoint {
uint256 x;
uint256 y;
uint256 z;
}
/// @dev Generator (x component)
uint256 internal constant GX = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296;
/// @dev Generator (y component)
uint256 internal constant GY = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5;
/// @dev P (size of the field)
uint256 internal constant P = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF;
/// @dev N (order of G)
uint256 internal constant N = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551;
/// @dev A parameter of the weierstrass equation
uint256 internal constant A = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC;
/// @dev B parameter of the weierstrass equation
uint256 internal constant B = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B;
/// @dev (P + 1) / 4. Useful to compute sqrt
uint256 private constant P1DIV4 = 0x3fffffffc0000000400000000000000000000000400000000000000000000000;
/// @dev N/2 for excluding higher order `s` values
uint256 private constant HALF_N = 0x7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a8;
/**
* @dev Verifies a secp256r1 signature using the RIP-7212 precompile and falls back to the Solidity implementation
* if the precompile is not available. This version should work on all chains, but requires the deployment of more
* bytecode.
*
* @param h - hashed message
* @param r - signature half R
* @param s - signature half S
* @param qx - public key coordinate X
* @param qy - public key coordinate Y
*
* IMPORTANT: This function disallows signatures where the `s` value is above `N/2` to prevent malleability.
* To flip the `s` value, compute `s = N - s`.
*/
function verify(bytes32 h, bytes32 r, bytes32 s, bytes32 qx, bytes32 qy) internal view returns (bool) {
(bool valid, bool supported) = _tryVerifyNative(h, r, s, qx, qy);
return supported ? valid : verifySolidity(h, r, s, qx, qy);
}
/**
* @dev Same as {verify}, but it will revert if the required precompile is not available.
*
* Make sure any logic (code or precompile) deployed at that address is the expected one,
* otherwise the returned value may be misinterpreted as a positive boolean.
*/
function verifyNative(bytes32 h, bytes32 r, bytes32 s, bytes32 qx, bytes32 qy) internal view returns (bool) {
(bool valid, bool supported) = _tryVerifyNative(h, r, s, qx, qy);
if (supported) {
return valid;
} else {
revert Errors.MissingPrecompile(address(0x100));
}
}
/**
* @dev Same as {verify}, but it will return false if the required precompile is not available.
*/
function _tryVerifyNative(
bytes32 h,
bytes32 r,
bytes32 s,
bytes32 qx,
bytes32 qy
) private view returns (bool valid, bool supported) {
if (!_isProperSignature(r, s) || !isValidPublicKey(qx, qy)) {
return (false, true); // signature is invalid, and its not because the precompile is missing
}
(bool success, bytes memory returndata) = address(0x100).staticcall(abi.encode(h, r, s, qx, qy));
return (success && returndata.length == 0x20) ? (abi.decode(returndata, (bool)), true) : (false, false);
}
/**
* @dev Same as {verify}, but only the Solidity implementation is used.
*/
function verifySolidity(bytes32 h, bytes32 r, bytes32 s, bytes32 qx, bytes32 qy) internal view returns (bool) {
if (!_isProperSignature(r, s) || !isValidPublicKey(qx, qy)) {
return false;
}
JPoint[16] memory points = _preComputeJacobianPoints(uint256(qx), uint256(qy));
uint256 w = Math.invModPrime(uint256(s), N);
uint256 u1 = mulmod(uint256(h), w, N);
uint256 u2 = mulmod(uint256(r), w, N);
(uint256 x, ) = _jMultShamir(points, u1, u2);
return ((x % N) == uint256(r));
}
/**
* @dev Public key recovery
*
* @param h - hashed message
* @param v - signature recovery param
* @param r - signature half R
* @param s - signature half S
*
* IMPORTANT: This function disallows signatures where the `s` value is above `N/2` to prevent malleability.
* To flip the `s` value, compute `s = N - s` and `v = 1 - v` if (`v = 0 | 1`).
*/
function recovery(bytes32 h, uint8 v, bytes32 r, bytes32 s) internal view returns (bytes32, bytes32) {
if (!_isProperSignature(r, s) || v > 1) {
return (0, 0);
}
uint256 rx = uint256(r);
uint256 ry2 = addmod(mulmod(addmod(mulmod(rx, rx, P), A, P), rx, P), B, P); // weierstrass equation y² = x³ + a.x + b
uint256 ry = Math.modExp(ry2, P1DIV4, P); // This formula for sqrt work because P 3 (mod 4)
if (mulmod(ry, ry, P) != ry2) return (0, 0); // Sanity check
if (ry % 2 != v % 2) ry = P - ry;
JPoint[16] memory points = _preComputeJacobianPoints(rx, ry);
uint256 w = Math.invModPrime(uint256(r), N);
uint256 u1 = mulmod(N - (uint256(h) % N), w, N);
uint256 u2 = mulmod(uint256(s), w, N);
(uint256 x, uint256 y) = _jMultShamir(points, u1, u2);
return (bytes32(x), bytes32(y));
}
/**
* @dev Checks if (x, y) are valid coordinates of a point on the curve.
* In particular this function checks that x <= P and y <= P.
*/
function isValidPublicKey(bytes32 x, bytes32 y) internal pure returns (bool result) {
assembly ("memory-safe") {
let p := P
let lhs := mulmod(y, y, p) // y^2
let rhs := addmod(mulmod(addmod(mulmod(x, x, p), A, p), x, p), B, p) // ((x^2 + a) * x) + b = x^3 + ax + b
result := and(and(lt(x, p), lt(y, p)), eq(lhs, rhs)) // Should conform with the Weierstrass equation
}
}
/**
* @dev Checks if (r, s) is a proper signature.
* In particular, this checks that `s` is in the "lower-range", making the signature non-malleable.
*/
function _isProperSignature(bytes32 r, bytes32 s) private pure returns (bool) {
return uint256(r) > 0 && uint256(r) < N && uint256(s) > 0 && uint256(s) <= HALF_N;
}
/**
* @dev Reduce from jacobian to affine coordinates
* @param jx - jacobian coordinate x
* @param jy - jacobian coordinate y
* @param jz - jacobian coordinate z
* @return ax - affine coordinate x
* @return ay - affine coordinate y
*/
function _affineFromJacobian(uint256 jx, uint256 jy, uint256 jz) private view returns (uint256 ax, uint256 ay) {
if (jz == 0) return (0, 0);
uint256 zinv = Math.invModPrime(jz, P);
uint256 zzinv = mulmod(zinv, zinv, P);
uint256 zzzinv = mulmod(zzinv, zinv, P);
ax = mulmod(jx, zzinv, P);
ay = mulmod(jy, zzzinv, P);
}
/**
* @dev Point addition on the jacobian coordinates
* Reference: https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-add-1998-cmo-2
*/
function _jAdd(
JPoint memory p1,
uint256 x2,
uint256 y2,
uint256 z2
) private pure returns (uint256 rx, uint256 ry, uint256 rz) {
assembly ("memory-safe") {
let p := P
let z1 := mload(add(p1, 0x40))
let s1 := mulmod(mload(add(p1, 0x20)), mulmod(mulmod(z2, z2, p), z2, p), p) // s1 = y1*z2³
let s2 := mulmod(y2, mulmod(mulmod(z1, z1, p), z1, p), p) // s2 = y2*z1³
let r := addmod(s2, sub(p, s1), p) // r = s2-s1
let u1 := mulmod(mload(p1), mulmod(z2, z2, p), p) // u1 = x1*z2²
let u2 := mulmod(x2, mulmod(z1, z1, p), p) // u2 = x2*z1²
let h := addmod(u2, sub(p, u1), p) // h = u2-u1
let hh := mulmod(h, h, p) // h²
// x' = r²-h³-2*u1*h²
rx := addmod(
addmod(mulmod(r, r, p), sub(p, mulmod(h, hh, p)), p),
sub(p, mulmod(2, mulmod(u1, hh, p), p)),
p
)
// y' = r*(u1*h²-x')-s1*h³
ry := addmod(
mulmod(r, addmod(mulmod(u1, hh, p), sub(p, rx), p), p),
sub(p, mulmod(s1, mulmod(h, hh, p), p)),
p
)
// z' = h*z1*z2
rz := mulmod(h, mulmod(z1, z2, p), p)
}
}
/**
* @dev Point doubling on the jacobian coordinates
* Reference: https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-1998-cmo-2
*/
function _jDouble(uint256 x, uint256 y, uint256 z) private pure returns (uint256 rx, uint256 ry, uint256 rz) {
assembly ("memory-safe") {
let p := P
let yy := mulmod(y, y, p)
let zz := mulmod(z, z, p)
let s := mulmod(4, mulmod(x, yy, p), p) // s = 4*x*y²
let m := addmod(mulmod(3, mulmod(x, x, p), p), mulmod(A, mulmod(zz, zz, p), p), p) // m = 3*x²+a*z
let t := addmod(mulmod(m, m, p), sub(p, mulmod(2, s, p)), p) // t = m²-2*s
// x' = t
rx := t
// y' = m*(s-t)-8*y⁴
ry := addmod(mulmod(m, addmod(s, sub(p, t), p), p), sub(p, mulmod(8, mulmod(yy, yy, p), p)), p)
// z' = 2*y*z
rz := mulmod(2, mulmod(y, z, p), p)
}
}
/**
* @dev Compute P·u1 + Q·u2 using the precomputed points for P and Q (see {_preComputeJacobianPoints}).
*
* Uses Strauss Shamir trick for EC multiplication
* https://stackoverflow.com/questions/50993471/ec-scalar-multiplication-with-strauss-shamir-method
* we optimise on this a bit to do with 2 bits at a time rather than a single bit
* the individual points for a single pass are precomputed
* overall this reduces the number of additions while keeping the same number of doublings
*/
function _jMultShamir(JPoint[16] memory points, uint256 u1, uint256 u2) private view returns (uint256, uint256) {
uint256 x = 0;
uint256 y = 0;
uint256 z = 0;
unchecked {
for (uint256 i = 0; i < 128; ++i) {
if (z > 0) {
(x, y, z) = _jDouble(x, y, z);
(x, y, z) = _jDouble(x, y, z);
}
// Read 2 bits of u1, and 2 bits of u2. Combining the two give a lookup index in the table.
uint256 pos = ((u1 >> 252) & 0xc) | ((u2 >> 254) & 0x3);
if (pos > 0) {
if (z == 0) {
(x, y, z) = (points[pos].x, points[pos].y, points[pos].z);
} else {
(x, y, z) = _jAdd(points[pos], x, y, z);
}
}
u1 <<= 2;
u2 <<= 2;
}
}
return _affineFromJacobian(x, y, z);
}
/**
* @dev Precompute a matrice of useful jacobian points associated with a given P. This can be seen as a 4x4 matrix
* that contains combination of P and G (generator) up to 3 times each. See the table below:
*
*
* i 0 1 2 3
*
* 0 0 p 2p 3p
* 4 g g+p g+2p g+3p
* 8 2g 2g+p 2g+2p 2g+3p
* 12 3g 3g+p 3g+2p 3g+3p
*
*/
function _preComputeJacobianPoints(uint256 px, uint256 py) private pure returns (JPoint[16] memory points) {
points[0x00] = JPoint(0, 0, 0); // 0,0
points[0x01] = JPoint(px, py, 1); // 1,0 (p)
points[0x04] = JPoint(GX, GY, 1); // 0,1 (g)
points[0x02] = _jDoublePoint(points[0x01]); // 2,0 (2p)
points[0x08] = _jDoublePoint(points[0x04]); // 0,2 (2g)
points[0x03] = _jAddPoint(points[0x01], points[0x02]); // 3,0 (3p)
points[0x05] = _jAddPoint(points[0x01], points[0x04]); // 1,1 (p+g)
points[0x06] = _jAddPoint(points[0x02], points[0x04]); // 2,1 (2p+g)
points[0x07] = _jAddPoint(points[0x03], points[0x04]); // 3,1 (3p+g)
points[0x09] = _jAddPoint(points[0x01], points[0x08]); // 1,2 (p+2g)
points[0x0a] = _jAddPoint(points[0x02], points[0x08]); // 2,2 (2p+2g)
points[0x0b] = _jAddPoint(points[0x03], points[0x08]); // 3,2 (3p+2g)
points[0x0c] = _jAddPoint(points[0x04], points[0x08]); // 0,3 (g+2g)
points[0x0d] = _jAddPoint(points[0x01], points[0x0c]); // 1,3 (p+3g)
points[0x0e] = _jAddPoint(points[0x02], points[0x0c]); // 2,3 (2p+3g)
points[0x0f] = _jAddPoint(points[0x03], points[0x0C]); // 3,3 (3p+3g)
}
function _jAddPoint(JPoint memory p1, JPoint memory p2) private pure returns (JPoint memory) {
(uint256 x, uint256 y, uint256 z) = _jAdd(p1, p2.x, p2.y, p2.z);
return JPoint(x, y, z);
}
function _jDoublePoint(JPoint memory p) private pure returns (JPoint memory) {
(uint256 x, uint256 y, uint256 z) = _jDouble(p.x, p.y, p.z);
return JPoint(x, y, z);
}
}

@ -0,0 +1,145 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Math} from "../math/Math.sol";
/**
* @dev RSA PKCS#1 v1.5 signature verification implementation according to https://datatracker.ietf.org/doc/html/rfc8017[RFC8017].
*
* This library supports PKCS#1 v1.5 padding to avoid malleability via chosen plaintext attacks in practical implementations.
* The padding follows the EMSA-PKCS1-v1_5-ENCODE encoding definition as per section 9.2 of the RFC. This padding makes
* RSA semanticaly secure for signing messages.
*
* Inspired by https://github.com/adria0/SolRsaVerify[Adrià Massanet's work]
*/
library RSA {
/**
* @dev Same as {pkcs1} but using SHA256 to calculate the digest of `data`.
*/
function pkcs1Sha256(
bytes memory data,
bytes memory s,
bytes memory e,
bytes memory n
) internal view returns (bool) {
return pkcs1(sha256(data), s, e, n);
}
/**
* @dev Verifies a PKCSv1.5 signature given a digest according the verification
* method described in https://datatracker.ietf.org/doc/html/rfc8017#section-8.2.2[section 8.2.2 of RFC8017].
*
* IMPORTANT: Although this function allows for it, using n of length 1024 bits is considered unsafe.
* Consider using at least 2048 bits.
*
* WARNING: PKCS#1 v1.5 allows for replayability given the message may contain arbitrary optional parameters in the
* DigestInfo. Consider using an onchain nonce or unique identifier to include in the message to prevent replay attacks.
*
* @param digest the digest to verify
* @param s is a buffer containing the signature
* @param e is the exponent of the public key
* @param n is the modulus of the public key
*/
function pkcs1(bytes32 digest, bytes memory s, bytes memory e, bytes memory n) internal view returns (bool) {
unchecked {
// cache and check length
uint256 length = n.length;
if (
length < 0x40 || // PKCS#1 padding is slightly less than 0x40 bytes at the bare minimum
length != s.length // signature must have the same length as the finite field
) {
return false;
}
// Verify that s < n to ensure there's only one valid signature for a given message
for (uint256 i = 0; i < length; i += 0x20) {
uint256 p = Math.min(i, length - 0x20);
bytes32 sp = _unsafeReadBytes32(s, p);
bytes32 np = _unsafeReadBytes32(n, p);
if (sp < np) {
// s < n in the upper bits (everything before is equal) s < n globally: ok
break;
} else if (sp > np || p == length - 0x20) {
// s > n in the upper bits (everything before is equal) s > n globally: fail
// or
// s = n and we are looking at the lower bits s = n globally: fail
return false;
}
}
// RSAVP1 https://datatracker.ietf.org/doc/html/rfc8017#section-5.2.2
// The previous check guarantees that n > 0. Therefore modExp cannot revert.
bytes memory buffer = Math.modExp(s, e, n);
// Check that buffer is well encoded:
// buffer ::= 0x00 | 0x01 | PS | 0x00 | DigestInfo
//
// With
// - PS is padding filled with 0xFF
// - DigestInfo ::= SEQUENCE {
// digestAlgorithm AlgorithmIdentifier,
// [optional algorithm parameters]
// digest OCTET STRING
// }
// Get AlgorithmIdentifier from the DigestInfo, and set the config accordingly
// - params: includes 00 + first part of DigestInfo
// - mask: filter to check the params
// - offset: length of the suffix (including digest)
bytes32 params; // 0x00 | DigestInfo
bytes32 mask;
uint256 offset;
// Digest is expected at the end of the buffer. Therefore if NULL param is present,
// it should be at 32 (digest) + 2 bytes from the end. To those 34 bytes, we add the
// OID (9 bytes) and its length (2 bytes) to get the position of the DigestInfo sequence,
// which is expected to have a length of 0x31 when the NULL param is present or 0x2f if not.
if (bytes1(_unsafeReadBytes32(buffer, length - 50)) == 0x31) {
offset = 0x34;
// 00 (1 byte) | SEQUENCE length (0x31) = 3031 (2 bytes) | SEQUENCE length (0x0d) = 300d (2 bytes) | OBJECT_IDENTIFIER length (0x09) = 0609 (2 bytes)
// SHA256 OID = 608648016503040201 (9 bytes) | NULL = 0500 (2 bytes) (explicit) | OCTET_STRING length (0x20) = 0420 (2 bytes)
params = 0x003031300d060960864801650304020105000420000000000000000000000000;
mask = 0xffffffffffffffffffffffffffffffffffffffff000000000000000000000000; // (20 bytes)
} else if (bytes1(_unsafeReadBytes32(buffer, length - 48)) == 0x2F) {
offset = 0x32;
// 00 (1 byte) | SEQUENCE length (0x2f) = 302f (2 bytes) | SEQUENCE length (0x0b) = 300b (2 bytes) | OBJECT_IDENTIFIER length (0x09) = 0609 (2 bytes)
// SHA256 OID = 608648016503040201 (9 bytes) | NULL = <implicit> | OCTET_STRING length (0x20) = 0420 (2 bytes)
params = 0x00302f300b060960864801650304020104200000000000000000000000000000;
mask = 0xffffffffffffffffffffffffffffffffffff0000000000000000000000000000; // (18 bytes)
} else {
// unknown
return false;
}
// Length is at least 0x40 and offset is at most 0x34, so this is safe. There is always some padding.
uint256 paddingEnd = length - offset;
// The padding has variable (arbitrary) length, so we check it byte per byte in a loop.
// This is required to ensure non-malleability. Not checking would allow an attacker to
// use the padding to manipulate the message in order to create a valid signature out of
// multiple valid signatures.
for (uint256 i = 2; i < paddingEnd; ++i) {
if (bytes1(_unsafeReadBytes32(buffer, i)) != 0xFF) {
return false;
}
}
// All the other parameters are small enough to fit in a bytes32, so we can check them directly.
return
bytes2(0x0001) == bytes2(_unsafeReadBytes32(buffer, 0x00)) && // 00 | 01
// PS was checked in the loop
params == _unsafeReadBytes32(buffer, paddingEnd) & mask && // DigestInfo
// Optional parameters are not checked
digest == _unsafeReadBytes32(buffer, length - 0x20); // Digest
}
}
/// @dev Reads a bytes32 from a bytes array without bounds checking.
function _unsafeReadBytes32(bytes memory array, uint256 offset) private pure returns (bytes32 result) {
// Memory safetiness is guaranteed as long as the provided `array` is a Solidity-allocated bytes array
// and `offset` is within bounds. This is the case for all calls to this private function from {pkcs1}.
assembly ("memory-safe") {
result := mload(add(add(array, 0x20), offset))
}
}
}

@ -283,8 +283,8 @@ library Math {
*
* If the input value is not inversible, 0 is returned.
*
* NOTE: If you know for sure that n is (big) a prime, it may be cheaper to use Ferma's little theorem and get the
* inverse using `Math.modExp(a, n - 2, n)`.
* NOTE: If you know for sure that n is (big) a prime, it may be cheaper to use Fermat's little theorem and get the
* inverse using `Math.modExp(a, n - 2, n)`. See {invModPrime}.
*/
function invMod(uint256 a, uint256 n) internal pure returns (uint256) {
unchecked {
@ -334,6 +334,21 @@ library Math {
}
}
/**
* @dev Variant of {invMod}. More efficient, but only works if `p` is known to be a prime greater than `2`.
*
* From https://en.wikipedia.org/wiki/Fermat%27s_little_theorem[Fermat's little theorem], we know that if p is
* prime, then `a**(p-1) 1 mod p`. As a consequence, we have `a * a**(p-2) 1 mod p`, which means that
* `a**(p-2)` is the modular multiplicative inverse of a in Fp.
*
* NOTE: this function does NOT check that `p` is a prime greater than `2`.
*/
function invModPrime(uint256 a, uint256 p) internal view returns (uint256) {
unchecked {
return Math.modExp(a, p - 2, p);
}
}
/**
* @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m)
*
@ -357,7 +372,7 @@ library Math {
/**
* @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m).
* It includes a success flag indicating if the operation succeeded. Operation will be marked has failed if trying
* It includes a success flag indicating if the operation succeeded. Operation will be marked as failed if trying
* to operate modulo 0 or if the underlying precompile reverted.
*
* IMPORTANT: The result is only valid if the success flag is true. When using this function, make sure the chain

@ -17,7 +17,7 @@ import {Panic} from "../Panic.sol";
*
* * Depth: The number of levels in the tree, it also defines the maximum number of leaves as 2**depth.
* * Zero value: The value that represents an empty leaf. Used to avoid regular zero values to be part of the tree.
* * Hashing function: A cryptographic hash function used to produce internal nodes.
* * Hashing function: A cryptographic hash function used to produce internal nodes. Defaults to {Hashes-commutativeKeccak256}.
*
* _Available since v5.1._
*/
@ -27,9 +27,6 @@ library MerkleTree {
*
* The `sides` and `zero` arrays are set to have a length equal to the depth of the tree during setup.
*
* The hashing function used during initialization to compute the `zeros` values (value of a node at a given depth
* for which the subtree is full of zero leaves). This function is kept in the structure for handling insertions.
*
* Struct members have an underscore prefix indicating that they are "private" and should not be read or written to
* directly. Use the functions provided below instead. Modifying the struct manually may violate assumptions and
* lead to unexpected behavior.
@ -44,7 +41,6 @@ library MerkleTree {
uint256 _nextLeafIndex;
bytes32[] _sides;
bytes32[] _zeros;
function(bytes32, bytes32) view returns (bytes32) _fnHash;
}
/**
@ -53,6 +49,9 @@ library MerkleTree {
*
* Calling this function on MerkleTree that was already setup and used will reset it to a blank state.
*
* Once a tree is setup, any push to it must use the same hashing function. This means that values
* should be pushed to it using the default {xref-MerkleTree-push-struct-MerkleTree-Bytes32PushTree-bytes32-}[push] function.
*
* IMPORTANT: The zero value should be carefully chosen since it will be stored in the tree representing
* empty leaves. It should be a value that is not expected to be part of the tree.
*/
@ -61,7 +60,10 @@ library MerkleTree {
}
/**
* @dev Same as {setup}, but allows to specify a custom hashing function.
* @dev Same as {xref-MerkleTree-setup-struct-MerkleTree-Bytes32PushTree-uint8-bytes32-}[setup], but allows to specify a custom hashing function.
*
* Once a tree is setup, any push to it must use the same hashing function. This means that values
* should be pushed to it using the custom push function, which should be the same one as used during the setup.
*
* IMPORTANT: Providing a custom hashing function is a security-sensitive operation since it may
* compromise the soundness of the tree. Consider using functions from {Hashes}.
@ -85,7 +87,6 @@ library MerkleTree {
// Set the first root
self._nextLeafIndex = 0;
self._fnHash = fnHash;
return currentZero;
}
@ -96,11 +97,32 @@ library MerkleTree {
*
* Hashing the leaf before calling this function is recommended as a protection against
* second pre-image attacks.
*
* This variant uses {Hashes-commutativeKeccak256} to hash internal nodes. It should only be used on merkle trees
* that were setup using the same (default) hashing function (i.e. by calling
* {xref-MerkleTree-setup-struct-MerkleTree-Bytes32PushTree-uint8-bytes32-}[the default setup] function).
*/
function push(Bytes32PushTree storage self, bytes32 leaf) internal returns (uint256 index, bytes32 newRoot) {
return push(self, leaf, Hashes.commutativeKeccak256);
}
/**
* @dev Insert a new leaf in the tree, and compute the new root. Returns the position of the inserted leaf in the
* tree, and the resulting root.
*
* Hashing the leaf before calling this function is recommended as a protection against
* second pre-image attacks.
*
* This variant uses a custom hashing function to hash internal nodes. It should only be called with the same
* function as the one used during the initial setup of the merkle tree.
*/
function push(
Bytes32PushTree storage self,
bytes32 leaf,
function(bytes32, bytes32) view returns (bytes32) fnHash
) internal returns (uint256 index, bytes32 newRoot) {
// Cache read
uint256 levels = self._zeros.length;
function(bytes32, bytes32) view returns (bytes32) fnHash = self._fnHash;
// Get leaf index
index = self._nextLeafIndex++;
@ -123,7 +145,7 @@ library MerkleTree {
}
// Compute the current node hash by using the hash function
// with either the its sibling (side) or the zero value for that level.
// with either its sibling (side) or the zero value for that level.
currentLevelHash = fnHash(
isLeft ? currentLevelHash : Arrays.unsafeAccess(self._sides, i).value,
isLeft ? Arrays.unsafeAccess(self._zeros, i).value : currentLevelHash

@ -20,7 +20,7 @@ The ERC-20 extension to keep track of votes and vote delegation is one such case
=== Governor & GovernorStorage
An OpenZeppelin Governor contract is not interface-compatible with Compound's GovernorAlpha or Bravo. Even though events are fully compatible, proposal lifecycle functions (creation, execution, etc.) have different signatures that are meant to optimize storage use. Other functions from GovernorAlpha are Bravo are likewise not available. It’s possible to opt in some Bravo-like behavior by inheriting from the GovernorStorage module. This module provides proposal enumerability and alternate versions of the `queue`, `execute` and `cancel` function that only take the proposal id. This module reduces the calldata needed by some operations in exchange for an increased the storage footprint. This might be a good trade-off for some L2 chains. It also provides primitives for indexer-free frontends.
An OpenZeppelin Governor contract is not interface-compatible with Compound's GovernorAlpha or Bravo. Even though events are fully compatible, proposal lifecycle functions (creation, execution, etc.) have different signatures that are meant to optimize storage use. Other functions from GovernorAlpha and Bravo are likewise not available. It’s possible to opt in some Bravo-like behavior by inheriting from the GovernorStorage module. This module provides proposal enumerability and alternate versions of the `queue`, `execute` and `cancel` function that only take the proposal id. This module reduces the calldata needed by some operations in exchange for an increased the storage footprint. This might be a good trade-off for some L2 chains. It also provides primitives for indexer-free frontends.
Note that even with the use of this module, one important difference with Compound's GovernorBravo is the way that `proposalId`s are calculated. Governor uses the hash of the proposal parameters with the purpose of keeping its data off-chain by event indexing, while the original Bravo implementation uses sequential `proposalId`s.

@ -8,6 +8,10 @@ Here are some of the more popular ones.
=== Checking Signatures On-Chain
At a high level, signatures are a set of cryptographic algorithms that allow for a _signer_ to prove himself owner of a _private key_ used to authorize an piece of information (generally a transaction or `UserOperation`). Natively, the EVM supports the Elliptic Curve Digital Signature Algorithm (https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm[ECDSA]) using the secp256k1 curve, however other signature algorithms such as P256 and RSA are supported.
==== Ethereum Signatures (secp256k1)
xref:api:utils.adoc#ECDSA[`ECDSA`] provides functions for recovering and managing Ethereum account ECDSA signatures. These are often generated via https://web3js.readthedocs.io/en/v1.7.3/web3-eth.html#sign[`web3.eth.sign`], and are a 65 byte array (of type `bytes` in Solidity) arranged the following way: `[[v (1)], [r (32)], [s (32)]]`.
The data signer can be recovered with xref:api:utils.adoc#ECDSA-recover-bytes32-bytes-[`ECDSA.recover`], and its address compared to verify the signature. Most wallets will hash the data to sign and add the prefix `\x19Ethereum Signed Message:\n`, so when attempting to recover the signer of an Ethereum signed message hash, you'll want to use xref:api:utils.adoc#MessageHashUtils-toEthSignedMessageHash-bytes32-[`toEthSignedMessageHash`].
@ -26,6 +30,73 @@ function _verify(bytes32 data, bytes memory signature, address account) internal
WARNING: Getting signature verification right is not trivial: make sure you fully read and understand xref:api:utils.adoc#MessageHashUtils[`MessageHashUtils`]'s and xref:api:utils.adoc#ECDSA[`ECDSA`]'s documentation.
==== P256 Signatures (secp256r1)
P256, also known as secp256r1, is one of the most used signature schemes. P256 signatures are standardized by the National Institute of Standards and Technology (NIST) and it's widely available in consumer hardware and software.
These signatures are different to regular Ethereum Signatures (secp256k1) in that they use a different elliptic curve to perform operations but have similar security guarantees.
[source,solidity]
----
using P256 for bytes32;
function _verify(
bytes32 data,
bytes32 r,
bytes32 s,
bytes32 qx,
bytes32 qy
) internal pure returns (bool) {
return data.verify(data, r, s, qx, qy);
}
----
By default, the `verify` function will try calling the (https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md)[RIP-7212] precompile at address `0x100` and will fallback to an implementation in Solidity if not available. We encourage you to use `verifyNative` if you know the precompile is available on the chain you're working on and on any other chain on which you intend to use the same bytecode in the future. In case of any doubts regarding the implementation roadmap of the native precompile `P256` of potential future target chains, please consider using `verifySolidity`.
[source,solidity]
----
using P256 for bytes32;
function _verify(
bytes32 data,
bytes32 r,
bytes32 s,
bytes32 qx,
bytes32 qy
) internal pure returns (bool) {
// Will only call the precompile at address(0x100)
return data.verifyNative(data, r, s, qx, qy);
}
----
IMPORTANT: The P256 library only allows for `s` values in the lower order of the curve (i.e. `s <= N/2`) to prevent malleability. In case your tooling produces signatures in both sides of the curve, consider flipping the `s` value to keep compatibility.
==== RSA
RSA a public-key cryptosystem that was popularized by corporate and governmental public key infrastructures (https://en.wikipedia.org/wiki/Public_key_infrastructure[PKIs]) and https://en.wikipedia.org/wiki/Domain_Name_System_Security_Extensions[DNSSEC].
This cryptosystem consists of using a private key that's the product of 2 large prime numbers. The message is signed by applying a modular exponentiation to its hash (commonly SHA256), where both the exponent and modulus compose the public key of the signer.
RSA signatures are known for being less efficient than elliptic curve signatures given the size of the keys, which are big compared to ECDSA keys with the same security level. Using plain RSA is considered unsafe, this is why the implementation uses the `EMSA-PKCS1-v1_5` encoding method from https://datatracker.ietf.org/doc/html/rfc8017[RFC8017] to include padding to the signature.
To verify a signature using RSA, you can leverage the xref:api:utils.adoc#RSA[`RSA`] library that exposes a method for verifying RSA with the PKCS 1.5 standard:
[source,solidity]
----
using RSA for bytes32;
function _verify(
bytes32 data,
bytes memory signature,
bytes memory e,
bytes memory n
) internal pure returns (bool) {
return data.pkcs1(signature, e, n);
}
----
IMPORTANT: Always use keys of at least 2048 bits. Additionally, be aware that PKCS#1 v1.5 allows for replayability due to the possibility of arbitrary optional parameters. To prevent replay attacks, consider including an onchain nonce or unique identifier in the message.
=== Verifying Merkle Proofs
Developers can build a Merkle Tree off-chain, which allows for verifying that an element (leaf) is part of a set by using a Merkle Proof. This technique is widely used for creating whitelists (e.g. for airdrops) and other advanced use cases.
@ -127,10 +198,10 @@ Building an on-chain Merkle Tree allow developers to keep track of the history o
The Merkle Tree does not keep track of the roots purposely, so that developers can choose their tracking mechanism. Setting up and using an Merkle Tree in Solidity is as simple as follows:
NOTE: Functions are exposed without access control for demonstration purposes
[source,solidity]
----
// NOTE: Functions are exposed without access control for demonstration purposes
using MerkleTree for MerkleTree.Bytes32PushTree;
MerkleTree.Bytes32PushTree private _tree;
@ -144,9 +215,64 @@ function push(bytes32 leaf) public /* onlyOwner */ {
}
----
The library also supports custom hashing functions, which can be passed as an extra parameter to the xref:api:utils.adoc#MerkleTree-push-struct-MerkleTree-Bytes32PushTree-bytes32-[`push`] and xref:api:utils.adoc#MerkleTree-setup-struct-MerkleTree-Bytes32PushTree-uint8-bytes32-[`setup`] functions.
Using custom hashing functions is a sensitive operation. After setup, it requires to keep using the same hashing function for every new valued pushed to the tree to avoid corrupting the tree. For this reason, it's a good practice to keep your hashing function static in your implementation contract as follows:
[source,solidity]
----
using MerkleTree for MerkleTree.Bytes32PushTree;
MerkleTree.Bytes32PushTree private _tree;
function setup(uint8 _depth, bytes32 _zero) public /* onlyOwner */ {
root = _tree.setup(_depth, _zero, _hashFn);
}
function push(bytes32 leaf) public /* onlyOwner */ {
(uint256 leafIndex, bytes32 currentRoot) = _tree.push(leaf, _hashFn);
// Store the new root.
}
function _hashFn(bytes32 a, bytes32 b) internal view returns(bytes32) {
// Custom hash function implementation
// Kept as an internal implementation detail to
// guarantee the same function is always used
}
----
[[misc]]
== Misc
=== Packing
The storage in the EVM is shaped in chunks of 32 bytes, each of this chunks is known as _slot_, and can hold multiple values together as long as these values don't exceed its size. These properties of the storage allows for a technique known as _packing_, that consists of placing values together on a single storage slot to reduce the costs associated to reading and writing to multiple slots instead of just one.
Commonly, developers pack values using structs that place values together so they fit better in storage. However, this approach requires to load such struct from either calldata or memory. Although sometimes necessary, it may be useful to pack values in a single slot and treat it as a packed value without involving calldata or memory.
The xref:api:utils.adoc#Packing[`Packing`] library is a set of utilities for packing values that fit in 32 bytes. The library includes 3 main functionalities:
* Packing 2 `bytesXX` values
* Extracting a packed `bytesXX` value from a `bytesYY`
* Replacing a packed `bytesXX` value from a `bytesYY`
With these primitives, one can build custom functions to create custom packed types. For example, suppose you need to pack an `address` of 20 bytes with a `bytes4` selector and an `uint64` time period:
[source,solidity]
----
function _pack(address account, bytes4 selector, uint64 period) external pure returns (bytes32) {
bytes12 subpack = Packing.pack_4_8(selector, bytes8(period));
return Packing.pack_20_12(bytes20(account), subpack);
}
function _unpack(bytes32 pack) external pure returns (address, bytes4, uint64) {
return (
address(Packing.extract_32_20(pack, 0)),
Packing.extract_32_4(pack, 20),
uint64(Packing.extract_32_8(pack, 24))
);
}
----
=== Storage Slots
Solidity allocates a storage pointer for each variable declared in a contract. However, there are cases when it's required to access storage pointers that can't be derived by using regular Solidity.

@ -1,6 +1,8 @@
[profile.default]
solc_version = '0.8.24'
evm_version = 'cancun'
optimizer = true
optimizer-runs = 200
src = 'contracts'
out = 'out'
libs = ['node_modules', 'lib']

@ -0,0 +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.1.13

@ -1,12 +1,12 @@
/// ENVVAR
// - COMPILE_VERSION: compiler version (default: 0.8.20)
// - SRC: contracts folder to compile (default: contracts)
// - COMPILE_MODE: production modes enables optimizations (default: development)
// - IR: enable IR compilation (default: false)
// - COVERAGE: enable coverage report
// - ENABLE_GAS_REPORT: enable gas report
// - COINMARKETCAP: coinmarkercat api key for USD value in gas report
// - CI: output gas report to file instead of stdout
// - COMPILER: compiler version (default: 0.8.24)
// - SRC: contracts folder to compile (default: contracts)
// - RUNS: number of optimization runs (default: 200)
// - IR: enable IR compilation (default: false)
// - COVERAGE: enable coverage report (default: false)
// - GAS: enable gas report (default: false)
// - COINMARKETCAP: coinmarketcap api key for USD value in gas report
// - CI: output gas report to file instead of stdout
const fs = require('fs');
const path = require('path');
@ -25,11 +25,10 @@ const { argv } = require('yargs/yargs')()
type: 'string',
default: 'contracts',
},
mode: {
alias: 'compileMode',
type: 'string',
choices: ['production', 'development'],
default: 'development',
runs: {
alias: 'optimizationRuns',
type: 'number',
default: 200,
},
ir: {
alias: 'enableIR',
@ -69,9 +68,6 @@ for (const f of fs.readdirSync(path.join(__dirname, 'hardhat'))) {
require(path.join(__dirname, 'hardhat', f));
}
const withOptimizations = argv.gas || argv.coverage || argv.compileMode === 'production';
const allowUnlimitedContractSize = argv.gas || argv.coverage || argv.compileMode === 'development';
/**
* @type import('hardhat/config').HardhatUserConfig
*/
@ -80,11 +76,11 @@ module.exports = {
version: argv.compiler,
settings: {
optimizer: {
enabled: withOptimizations,
runs: 200,
enabled: true,
runs: argv.runs,
},
evmVersion: argv.evm,
viaIR: withOptimizations && argv.ir,
viaIR: argv.ir,
outputSelection: { '*': { '*': ['storageLayout'] } },
},
},
@ -94,7 +90,7 @@ module.exports = {
'initcode-size': 'off',
},
'*': {
'code-size': withOptimizations,
'code-size': true,
'unused-param': !argv.coverage, // coverage causes unused-param warnings
'transient-storage': false,
default: 'error',
@ -103,7 +99,9 @@ module.exports = {
networks: {
hardhat: {
hardfork: argv.evm,
allowUnlimitedContractSize,
// Exposed contracts often exceed the maximum contract size. For normal contract,
// we rely on the `code-size` compiler warning, that will cause a compilation error.
allowUnlimitedContractSize: true,
initialBaseFeePerGas: argv.coverage ? 0 : undefined,
},
},

@ -0,0 +1 @@
Subproject commit c0d865508c0fee0a11b97732c5e90f9cad6b65a5

400
package-lock.json generated

@ -37,7 +37,7 @@
"prettier-plugin-solidity": "^1.1.0",
"rimraf": "^5.0.1",
"semver": "^7.3.5",
"solhint": "^4.0.0",
"solhint": "^5.0.0",
"solhint-plugin-openzeppelin": "file:scripts/solhint-custom",
"solidity-ast": "^0.4.50",
"solidity-coverage": "^0.8.5",
@ -2431,6 +2431,12 @@
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@solidity-parser/parser": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.18.0.tgz",
"integrity": "sha512-yfORGUIPgLck41qyN7nbwJRAx17/jAIXCTanHOJZhB6PJ1iAk/84b/xlsVKFSyNyLXIj0dhppoE0+CRws7wlzA==",
"dev": true
},
"node_modules/@szmarczak/http-timer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
@ -2718,15 +2724,6 @@
"string-width": "^4.1.0"
}
},
"node_modules/ansi-align/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-align/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@ -2750,18 +2747,6 @@
"node": ">=8"
}
},
"node_modules/ansi-align/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@ -2798,6 +2783,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@ -3073,15 +3067,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/boxen/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/boxen/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -3163,18 +3148,6 @@
"node": ">=8"
}
},
"node_modules/boxen/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/boxen/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -3198,12 +3171,12 @@
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@ -3566,15 +3539,6 @@
"node": ">=12"
}
},
"node_modules/cliui/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@ -3598,18 +3562,6 @@
"node": ">=8"
}
},
"node_modules/cliui/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
@ -4134,27 +4086,6 @@
"node": ">=8.6"
}
},
"node_modules/enquirer/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/enquirer/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
@ -4494,15 +4425,6 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/eslint/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -4649,18 +4571,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/eslint/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -5038,9 +4948,9 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
@ -5784,21 +5694,6 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/hardhat-gas-reporter/node_modules/@solidity-parser/parser": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.18.0.tgz",
"integrity": "sha512-yfORGUIPgLck41qyN7nbwJRAx17/jAIXCTanHOJZhB6PJ1iAk/84b/xlsVKFSyNyLXIj0dhppoE0+CRws7wlzA==",
"dev": true
},
"node_modules/hardhat-gas-reporter/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/hardhat-gas-reporter/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -5920,18 +5815,6 @@
"node": ">=8"
}
},
"node_modules/hardhat-gas-reporter/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/hardhat-gas-reporter/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -7518,15 +7401,6 @@
"node": ">=6"
}
},
"node_modules/mocha/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/mocha/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@ -7705,18 +7579,6 @@
"node": ">=8"
}
},
"node_modules/mocha/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mocha/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
@ -9341,15 +9203,6 @@
"node": ">=6"
}
},
"node_modules/smartwrap/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/smartwrap/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -9417,18 +9270,6 @@
"node": ">=8"
}
},
"node_modules/smartwrap/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/smartwrap/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@ -9472,9 +9313,9 @@
}
},
"node_modules/solhint": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/solhint/-/solhint-4.5.2.tgz",
"integrity": "sha512-o7MNYS5QPgE6l+PTGOTAUtCzo0ZLnffQsv586hntSHBe2JbSDfkoxfhAOcjZjN4OesTgaX4UEEjCjH9y/4BP5w==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/solhint/-/solhint-5.0.0.tgz",
"integrity": "sha512-pSRKkzRsruia6/xa9L5VSyd7dMZkiiTi/aYZcvUQo7KK+S16ojPwIbt2jfjbH5WEJ83grzIIE4WrYQfAxGWh/A==",
"dev": true,
"dependencies": {
"@solidity-parser/parser": "^0.18.0",
@ -9507,21 +9348,6 @@
"resolved": "scripts/solhint-custom",
"link": true
},
"node_modules/solhint/node_modules/@solidity-parser/parser": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.18.0.tgz",
"integrity": "sha512-yfORGUIPgLck41qyN7nbwJRAx17/jAIXCTanHOJZhB6PJ1iAk/84b/xlsVKFSyNyLXIj0dhppoE0+CRws7wlzA==",
"dev": true
},
"node_modules/solhint/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/solhint/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -9654,18 +9480,6 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/solhint/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/solhint/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -10186,15 +10000,6 @@
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@ -10204,18 +10009,6 @@
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string.prototype.trim": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz",
@ -10261,8 +10054,7 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
@ -10274,11 +10066,15 @@
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
@ -10361,15 +10157,6 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/table/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/table/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@ -10408,18 +10195,6 @@
"node": ">=8"
}
},
"node_modules/table/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/term-size": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
@ -10575,15 +10350,6 @@
"node": ">=8.0.0"
}
},
"node_modules/tty-table/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/tty-table/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -10642,18 +10408,6 @@
"node": ">=8"
}
},
"node_modules/tty-table/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/tty-table/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -11153,15 +10907,6 @@
"node": ">=8"
}
},
"node_modules/widest-line/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/widest-line/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@ -11185,18 +10930,6 @@
"node": ">=8"
}
},
"node_modules/widest-line/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@ -11253,15 +10986,6 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -11318,27 +11042,6 @@
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -11395,18 +11098,6 @@
"node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@ -11519,15 +11210,6 @@
"node": ">=8"
}
},
"node_modules/yargs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@ -11551,18 +11233,6 @@
"node": ">=8"
}
},
"node_modules/yargs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",

@ -10,7 +10,7 @@
"scripts": {
"compile": "hardhat compile",
"compile:harnesses": "env SRC=./certora/harnesses hardhat compile",
"coverage": "env COVERAGE=true hardhat coverage",
"coverage": "scripts/checks/coverage.sh",
"docs": "npm run prepare-docs && oz-docs",
"docs:watch": "oz-docs watch contracts docs/templates docs/config.js",
"prepare": "git config --local core.hooksPath .githooks",
@ -78,7 +78,7 @@
"prettier-plugin-solidity": "^1.1.0",
"rimraf": "^5.0.1",
"semver": "^7.3.5",
"solhint": "^4.0.0",
"solhint": "^5.0.0",
"solhint-plugin-openzeppelin": "file:scripts/solhint-custom",
"solidity-ast": "^0.4.50",
"solidity-coverage": "^0.8.5",

@ -1 +0,0 @@
certora-cli==4.13.1

@ -10,7 +10,7 @@ for (const name in oldLayout) {
if (name in newLayout) {
const report = getStorageUpgradeReport(oldLayout[name], newLayout[name], {});
if (!report.ok) {
console.log(`Storage layout incompatilibity found in ${name}:`);
console.log(`Storage layout incompatibility found in ${name}:`);
console.log(report.explain());
process.exitCode = 1;
}

@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
export COVERAGE=true
export FOUNDRY_FUZZ_RUNS=10
# Hardhat coverage
hardhat coverage
if [ "${CI:-"false"}" == "true" ]; then
# Foundry coverage
forge coverage --report lcov --ir-minimum
# Remove zero hits
sed -i '/,0/d' lcov.info
fi
# Reports are then uploaded to Codecov automatically by workflow, and merged.

@ -1,6 +1,6 @@
#!/usr/bin/env node
const cp = require('child_process');
// const cp = require('child_process');
const fs = require('fs');
const path = require('path');
const format = require('./format-lines');
@ -23,11 +23,11 @@ function generateFromTemplate(file, template, outputPrefix = '') {
...(version ? [version + ` (${file})`] : []),
`// This file was procedurally generated from ${input}.`,
'',
require(template),
require(template).trimEnd(),
);
fs.writeFileSync(output, content);
cp.execFileSync('prettier', ['--write', output]);
// cp.execFileSync('prettier', ['--write', output]);
}
// Contracts
@ -39,6 +39,7 @@ for (const [file, template] of Object.entries({
'utils/SlotDerivation.sol': './templates/SlotDerivation.js',
'utils/StorageSlot.sol': './templates/StorageSlot.js',
'utils/Arrays.sol': './templates/Arrays.js',
'utils/Packing.sol': './templates/Packing.js',
'mocks/StorageSlotMock.sol': './templates/StorageSlotMock.js',
})) {
generateFromTemplate(file, template, './contracts/');
@ -47,6 +48,7 @@ for (const [file, template] of Object.entries({
// Tests
for (const [file, template] of Object.entries({
'utils/structs/Checkpoints.t.sol': './templates/Checkpoints.t.js',
'utils/Packing.t.sol': './templates/Packing.t.js',
'utils/SlotDerivation.t.sol': './templates/SlotDerivation.t.js',
})) {
generateFromTemplate(file, template, './test/');

@ -15,39 +15,39 @@ import {Math} from "./math/Math.sol";
`;
const sort = type => `\
/**
* @dev Sort an array of ${type} (in memory) following the provided comparator function.
*
* This function does the sorting "in place", meaning that it overrides the input. The object is returned for
* convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.
*
* NOTE: this function's cost is \`O(n · log(n))\` in average and \`O(n²)\` in the worst case, with n the length of the
* array. Using it in view functions that are executed through \`eth_call\` is safe, but one should be very careful
* when executing this as part of a transaction. If the array being sorted is too large, the sort operation may
* consume more gas than is available in a block, leading to potential DoS.
*/
function sort(
${type}[] memory array,
function(${type}, ${type}) pure returns (bool) comp
) internal pure returns (${type}[] memory) {
${
type === 'bytes32'
? '_quickSort(_begin(array), _end(array), comp);'
: 'sort(_castToBytes32Array(array), _castToBytes32Comp(comp));'
}
return array;
/**
* @dev Sort an array of ${type} (in memory) following the provided comparator function.
*
* This function does the sorting "in place", meaning that it overrides the input. The object is returned for
* convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.
*
* NOTE: this function's cost is \`O(n · log(n))\` in average and \`O(n²)\` in the worst case, with n the length of the
* array. Using it in view functions that are executed through \`eth_call\` is safe, but one should be very careful
* when executing this as part of a transaction. If the array being sorted is too large, the sort operation may
* consume more gas than is available in a block, leading to potential DoS.
*/
function sort(
${type}[] memory array,
function(${type}, ${type}) pure returns (bool) comp
) internal pure returns (${type}[] memory) {
${
type === 'bytes32'
? '_quickSort(_begin(array), _end(array), comp);'
: 'sort(_castToBytes32Array(array), _castToBytes32Comp(comp));'
}
return array;
}
/**
* @dev Variant of {sort} that sorts an array of ${type} in increasing order.
*/
function sort(${type}[] memory array) internal pure returns (${type}[] memory) {
${type === 'bytes32' ? 'sort(array, _defaultComp);' : 'sort(_castToBytes32Array(array), _defaultComp);'}
return array;
}
/**
* @dev Variant of {sort} that sorts an array of ${type} in increasing order.
*/
function sort(${type}[] memory array) internal pure returns (${type}[] memory) {
${type === 'bytes32' ? 'sort(array, _defaultComp);' : 'sort(_castToBytes32Array(array), _defaultComp);'}
return array;
}
`;
const quickSort = `
const quickSort = `\
/**
* @dev Performs a quick sort of a segment of memory. The segment sorted starts at \`begin\` (inclusive), and stops
* at end (exclusive). Sorting follows the \`comp\` comparator.
@ -123,34 +123,34 @@ function _swap(uint256 ptr1, uint256 ptr2) private pure {
}
`;
const defaultComparator = `
/// @dev Comparator for sorting arrays in increasing order.
function _defaultComp(bytes32 a, bytes32 b) private pure returns (bool) {
return a < b;
}
const defaultComparator = `\
/// @dev Comparator for sorting arrays in increasing order.
function _defaultComp(bytes32 a, bytes32 b) private pure returns (bool) {
return a < b;
}
`;
const castArray = type => `\
/// @dev Helper: low level cast ${type} memory array to uint256 memory array
function _castToBytes32Array(${type}[] memory input) private pure returns (bytes32[] memory output) {
assembly {
output := input
}
/// @dev Helper: low level cast ${type} memory array to uint256 memory array
function _castToBytes32Array(${type}[] memory input) private pure returns (bytes32[] memory output) {
assembly {
output := input
}
}
`;
const castComparator = type => `\
/// @dev Helper: low level cast ${type} comp function to bytes32 comp function
function _castToBytes32Comp(
function(${type}, ${type}) pure returns (bool) input
) private pure returns (function(bytes32, bytes32) pure returns (bool) output) {
assembly {
output := input
}
/// @dev Helper: low level cast ${type} comp function to bytes32 comp function
function _castToBytes32Comp(
function(${type}, ${type}) pure returns (bool) input
) private pure returns (function(bytes32, bytes32) pure returns (bool) output) {
assembly {
output := input
}
}
`;
const search = `
const search = `\
/**
* @dev Searches a sorted \`array\` and returns the first index that contains
* a value greater or equal to \`element\`. If no such index exists (i.e. all
@ -319,12 +319,12 @@ function upperBoundMemory(uint256[] memory array, uint256 element) internal pure
}
`;
const unsafeAccessStorage = type => `
const unsafeAccessStorage = type => `\
/**
* @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check.
*
* WARNING: Only use if you are certain \`pos\` is lower than the array length.
*/
* @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check.
*
* WARNING: Only use if you are certain \`pos\` is lower than the array length.
*/
function unsafeAccess(${type}[] storage arr, uint256 pos) internal pure returns (StorageSlot.${capitalize(
type,
)}Slot storage) {
@ -334,9 +334,10 @@ function unsafeAccess(${type}[] storage arr, uint256 pos) internal pure returns
slot := arr.slot
}
return slot.deriveArray().offset(pos).get${capitalize(type)}Slot();
}`;
}
`;
const unsafeAccessMemory = type => `
const unsafeAccessMemory = type => `\
/**
* @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check.
*
@ -349,7 +350,7 @@ function unsafeMemoryAccess(${type}[] memory arr, uint256 pos) internal pure ret
}
`;
const unsafeSetLength = type => `
const unsafeSetLength = type => `\
/**
* @dev Helper to set the length of an dynamic array. Directly writing to \`.length\` is forbidden.
*
@ -360,26 +361,32 @@ function unsafeSetLength(${type}[] storage array, uint256 len) internal {
assembly {
sstore(array.slot, len)
}
}`;
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'library Arrays {',
'using SlotDerivation for bytes32;',
'using StorageSlot for bytes32;',
// sorting, comparator, helpers and internal
sort('bytes32'),
TYPES.filter(type => type !== 'bytes32').map(sort),
quickSort,
defaultComparator,
TYPES.filter(type => type !== 'bytes32').map(castArray),
TYPES.filter(type => type !== 'bytes32').map(castComparator),
// lookup
search,
// unsafe (direct) storage and memory access
TYPES.map(unsafeAccessStorage),
TYPES.map(unsafeAccessMemory),
TYPES.map(unsafeSetLength),
format(
[].concat(
'using SlotDerivation for bytes32;',
'using StorageSlot for bytes32;',
'',
// sorting, comparator, helpers and internal
sort('bytes32'),
TYPES.filter(type => type !== 'bytes32').map(sort),
quickSort,
defaultComparator,
TYPES.filter(type => type !== 'bytes32').map(castArray),
TYPES.filter(type => type !== 'bytes32').map(castComparator),
// lookup
search,
// unsafe (direct) storage and memory access
TYPES.map(unsafeAccessStorage),
TYPES.map(unsafeAccessMemory),
TYPES.map(unsafeSetLength),
),
).trimEnd(),
'}',
);

@ -17,10 +17,10 @@ import {Math} from "../math/Math.sol";
`;
const errors = `\
/**
* @dev A value was attempted to be inserted on a past checkpoint.
*/
error CheckpointUnorderedInsertion();
/**
* @dev A value was attempted to be inserted on a past checkpoint.
*/
error CheckpointUnorderedInsertion();
`;
const template = opts => `\
@ -41,11 +41,7 @@ struct ${opts.checkpointTypeName} {
* IMPORTANT: Never accept \`key\` as a user input, since an arbitrary \`type(${opts.keyTypeName}).max\` key set will disable the
* library.
*/
function push(
${opts.historyTypeName} storage self,
${opts.keyTypeName} key,
${opts.valueTypeName} value
) internal returns (${opts.valueTypeName}, ${opts.valueTypeName}) {
function push(${opts.historyTypeName} storage self, ${opts.keyTypeName} key, ${opts.valueTypeName} value) internal returns (${opts.valueTypeName}, ${opts.valueTypeName}) {
return _insert(self.${opts.checkpointFieldName}, key, value);
}
@ -108,15 +104,7 @@ function latest(${opts.historyTypeName} storage self) internal view returns (${o
* @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value
* in the most recent checkpoint.
*/
function latestCheckpoint(${opts.historyTypeName} storage self)
internal
view
returns (
bool exists,
${opts.keyTypeName} ${opts.keyFieldName},
${opts.valueTypeName} ${opts.valueFieldName}
)
{
function latestCheckpoint(${opts.historyTypeName} storage self) internal view returns (bool exists, ${opts.keyTypeName} ${opts.keyFieldName}, ${opts.valueTypeName} ${opts.valueFieldName}) {
uint256 pos = self.${opts.checkpointFieldName}.length;
if (pos == 0) {
return (false, 0, 0);
@ -144,11 +132,7 @@ function at(${opts.historyTypeName} storage self, uint32 pos) internal view retu
* @dev Pushes a (\`key\`, \`value\`) pair into an ordered list of checkpoints, either by inserting a new checkpoint,
* or by updating the last one.
*/
function _insert(
${opts.checkpointTypeName}[] storage self,
${opts.keyTypeName} key,
${opts.valueTypeName} value
) private returns (${opts.valueTypeName}, ${opts.valueTypeName}) {
function _insert(${opts.checkpointTypeName}[] storage self, ${opts.keyTypeName} key, ${opts.valueTypeName} value) private returns (${opts.valueTypeName}, ${opts.valueTypeName}) {
uint256 pos = self.length;
if (pos > 0) {
@ -225,11 +209,10 @@ function _lowerBinaryLookup(
/**
* @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds.
*/
function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos)
private
pure
returns (${opts.checkpointTypeName} storage result)
{
function _unsafeAccess(
${opts.checkpointTypeName}[] storage self,
uint256 pos
) private pure returns (${opts.checkpointTypeName} storage result) {
assembly {
mstore(0, self.slot)
result.slot := add(keccak256(0, 0x20), pos)
@ -242,7 +225,11 @@ function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos)
module.exports = format(
header.trimEnd(),
'library Checkpoints {',
errors,
OPTS.flatMap(opts => template(opts)),
format(
[].concat(
errors,
OPTS.map(opts => template(opts)),
),
).trimEnd(),
'}',
);

@ -22,18 +22,13 @@ uint8 internal constant _KEY_MAX_GAP = 64;
Checkpoints.${opts.historyTypeName} internal _ckpts;
// helpers
function _bound${capitalize(opts.keyTypeName)}(
${opts.keyTypeName} x,
${opts.keyTypeName} min,
${opts.keyTypeName} max
) internal pure returns (${opts.keyTypeName}) {
function _bound${capitalize(opts.keyTypeName)}(${opts.keyTypeName} x, ${opts.keyTypeName} min, ${
opts.keyTypeName
} max) internal pure returns (${opts.keyTypeName}) {
return SafeCast.to${capitalize(opts.keyTypeName)}(bound(uint256(x), uint256(min), uint256(max)));
}
function _prepareKeys(
${opts.keyTypeName}[] memory keys,
${opts.keyTypeName} maxSpread
) internal pure {
function _prepareKeys(${opts.keyTypeName}[] memory keys, ${opts.keyTypeName} maxSpread) internal pure {
${opts.keyTypeName} lastKey = 0;
for (uint256 i = 0; i < keys.length; ++i) {
${opts.keyTypeName} key = _bound${capitalize(opts.keyTypeName)}(keys[i], lastKey, lastKey + maxSpread);
@ -42,11 +37,7 @@ function _prepareKeys(
}
}
function _assertLatestCheckpoint(
bool exist,
${opts.keyTypeName} key,
${opts.valueTypeName} value
) internal {
function _assertLatestCheckpoint(bool exist, ${opts.keyTypeName} key, ${opts.valueTypeName} value) internal {
(bool _exist, ${opts.keyTypeName} _key, ${opts.valueTypeName} _value) = _ckpts.latestCheckpoint();
assertEq(_exist, exist);
assertEq(_key, key);
@ -54,11 +45,9 @@ function _assertLatestCheckpoint(
}
// tests
function testPush(
${opts.keyTypeName}[] memory keys,
${opts.valueTypeName}[] memory values,
${opts.keyTypeName} pastKey
) public {
function testPush(${opts.keyTypeName}[] memory keys, ${opts.valueTypeName}[] memory values, ${
opts.keyTypeName
} pastKey) public {
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
@ -71,7 +60,7 @@ function testPush(
for (uint256 i = 0; i < keys.length; ++i) {
${opts.keyTypeName} key = keys[i];
${opts.valueTypeName} value = values[i % values.length];
if (i > 0 && key == keys[i-1]) ++duplicates;
if (i > 0 && key == keys[i - 1]) ++duplicates;
// push
_ckpts.push(key, value);
@ -95,14 +84,12 @@ function testPush(
// used to test reverts
function push(${opts.keyTypeName} key, ${opts.valueTypeName} value) external {
_ckpts.push(key, value);
_ckpts.push(key, value);
}
function testLookup(
${opts.keyTypeName}[] memory keys,
${opts.valueTypeName}[] memory values,
${opts.keyTypeName} lookup
) public {
function testLookup(${opts.keyTypeName}[] memory keys, ${opts.valueTypeName}[] memory values, ${
opts.keyTypeName
} lookup) public {
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
@ -124,7 +111,7 @@ function testLookup(
upper = value;
}
// find the first key that is not smaller than the lookup key
if (key >= lookup && (i == 0 || keys[i-1] < lookup)) {
if (key >= lookup && (i == 0 || keys[i - 1] < lookup)) {
lowerKey = key;
}
if (key == lowerKey) {
@ -142,5 +129,10 @@ function testLookup(
// GENERATE
module.exports = format(
header,
...OPTS.flatMap(opts => [`contract Checkpoints${opts.historyTypeName}Test is Test {`, [template(opts)], '}']),
...OPTS.flatMap(opts => [
`contract Checkpoints${opts.historyTypeName}Test is Test {`,
[template(opts).trimEnd()],
'}',
'',
]),
);

@ -54,7 +54,7 @@ import {EnumerableSet} from "./EnumerableSet.sol";
`;
/* eslint-enable max-len */
const defaultMap = () => `\
const defaultMap = `\
// To implement this library for multiple types with as little code repetition as possible, we write it in
// terms of a generic Map type with bytes32 keys and values. The Map implementation uses private functions,
// and user-facing implementations such as \`UintToAddressMap\` are just wrappers around the underlying Map.
@ -78,11 +78,7 @@ struct Bytes32ToBytes32Map {
* Returns true if the key was added to the map, that is if it was not
* already present.
*/
function set(
Bytes32ToBytes32Map storage map,
bytes32 key,
bytes32 value
) internal returns (bool) {
function set(Bytes32ToBytes32Map storage map, bytes32 key, bytes32 value) internal returns (bool) {
map._values[key] = value;
return map._keys.add(key);
}
@ -148,7 +144,7 @@ function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view retu
*/
function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) {
bytes32 value = map._values[key];
if(value == 0 && !contains(map, key)) {
if (value == 0 && !contains(map, key)) {
revert EnumerableMapNonexistentKey(key);
}
return value;
@ -181,11 +177,7 @@ struct ${name} {
* Returns true if the key was added to the map, that is if it was not
* already present.
*/
function set(
${name} storage map,
${keyType} key,
${valueType} value
) internal returns (bool) {
function set(${name} storage map, ${keyType} key, ${valueType} value) internal returns (bool) {
return set(map._inner, ${toBytes32(keyType, 'key')}, ${toBytes32(valueType, 'value')});
}
@ -271,11 +263,13 @@ function keys(${name} storage map) internal view returns (${keyType}[] memory) {
module.exports = format(
header.trimEnd(),
'library EnumerableMap {',
[
'using EnumerableSet for EnumerableSet.Bytes32Set;',
'',
defaultMap(),
TYPES.map(details => customMap(details).trimEnd()).join('\n\n'),
],
format(
[].concat(
'using EnumerableSet for EnumerableSet.Bytes32Set;',
'',
defaultMap,
TYPES.map(details => customMap(details)),
),
).trimEnd(),
'}',
);

@ -43,7 +43,7 @@ pragma solidity ^0.8.20;
`;
/* eslint-enable max-len */
const defaultSet = () => `\
const defaultSet = `\
// To implement this library for multiple types with as little code
// repetition as possible, we write it in terms of a generic Set type with
// bytes32 values.
@ -240,6 +240,11 @@ function values(${name} storage set) internal view returns (${type}[] memory) {
module.exports = format(
header.trimEnd(),
'library EnumerableSet {',
[defaultSet(), TYPES.map(details => customSet(details).trimEnd()).join('\n\n')],
format(
[].concat(
defaultSet,
TYPES.map(details => customSet(details)),
),
).trimEnd(),
'}',
);

@ -0,0 +1,86 @@
const format = require('../format-lines');
const { product } = require('../../helpers');
const { SIZES } = require('./Packing.opts');
// TEMPLATE
const header = `\
pragma solidity ^0.8.20;
/**
* @dev Helper library packing and unpacking multiple values into bytesXX.
*
* Example usage:
*
* \`\`\`solidity
* library MyPacker {
* type MyType is bytes32;
*
* function _pack(address account, bytes4 selector, uint64 period) external pure returns (MyType) {
* bytes12 subpack = Packing.pack_4_8(selector, bytes8(period));
* bytes32 pack = Packing.pack_20_12(bytes20(account), subpack);
* return MyType.wrap(pack);
* }
*
* function _unpack(MyType self) external pure returns (address, bytes4, uint64) {
* bytes32 pack = MyType.unwrap(self);
* return (
* address(Packing.extract_32_20(pack, 0)),
* Packing.extract_32_4(pack, 20),
* uint64(Packing.extract_32_8(pack, 24))
* );
* }
* }
* \`\`\`
*/
// solhint-disable func-name-mixedcase
`;
const errors = `\
error OutOfRangeAccess();
`;
const pack = (left, right) => `\
function pack_${left}_${right}(bytes${left} left, bytes${right} right) internal pure returns (bytes${
left + right
} result) {
assembly ("memory-safe") {
result := or(left, shr(${8 * left}, right))
}
}
`;
const extract = (outer, inner) => `\
function extract_${outer}_${inner}(bytes${outer} self, uint8 offset) internal pure returns (bytes${inner} result) {
if (offset > ${outer - inner}) revert OutOfRangeAccess();
assembly ("memory-safe") {
result := and(shl(mul(8, offset), self), shl(${256 - 8 * inner}, not(0)))
}
}
`;
const replace = (outer, inner) => `\
function replace_${outer}_${inner}(bytes${outer} self, bytes${inner} value, uint8 offset) internal pure returns (bytes${outer} result) {
bytes${inner} oldValue = extract_${outer}_${inner}(self, offset);
assembly ("memory-safe") {
result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
}
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'library Packing {',
format(
[].concat(
errors,
product(SIZES, SIZES)
.filter(([left, right]) => SIZES.includes(left + right))
.map(([left, right]) => pack(left, right)),
product(SIZES, SIZES)
.filter(([outer, inner]) => outer > inner)
.flatMap(([outer, inner]) => [extract(outer, inner), replace(outer, inner)]),
),
).trimEnd(),
'}',
);

@ -0,0 +1,3 @@
module.exports = {
SIZES: [1, 2, 4, 6, 8, 12, 16, 20, 24, 28, 32],
};

@ -0,0 +1,48 @@
const format = require('../format-lines');
const { product } = require('../../helpers');
const { SIZES } = require('./Packing.opts');
// TEMPLATE
const header = `\
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {Packing} from "@openzeppelin/contracts/utils/Packing.sol";
`;
const testPack = (left, right) => `\
function testPack(bytes${left} left, bytes${right} right) external {
assertEq(left, Packing.pack_${left}_${right}(left, right).extract_${left + right}_${left}(0));
assertEq(right, Packing.pack_${left}_${right}(left, right).extract_${left + right}_${right}(${left}));
}
`;
const testReplace = (outer, inner) => `\
function testReplace(bytes${outer} container, bytes${inner} newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, ${outer - inner}));
bytes${inner} oldValue = container.extract_${outer}_${inner}(offset);
assertEq(newValue, container.replace_${outer}_${inner}(newValue, offset).extract_${outer}_${inner}(offset));
assertEq(container, container.replace_${outer}_${inner}(newValue, offset).replace_${outer}_${inner}(oldValue, offset));
}
`;
// GENERATE
module.exports = format(
header,
'contract PackingTest is Test {',
format(
[].concat(
'using Packing for *;',
'',
product(SIZES, SIZES)
.filter(([left, right]) => SIZES.includes(left + right))
.map(([left, right]) => testPack(left, right)),
product(SIZES, SIZES)
.filter(([outer, inner]) => outer > inner)
.map(([outer, inner]) => testReplace(outer, inner)),
),
).trimEnd(),
'}',
);

@ -21,25 +21,25 @@ pragma solidity ^0.8.20;
`;
const errors = `\
/**
* @dev Value doesn't fit in an uint of \`bits\` size.
*/
error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value);
/**
* @dev Value doesn't fit in an uint of \`bits\` size.
*/
error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value);
/**
* @dev An int value doesn't fit in an uint of \`bits\` size.
*/
error SafeCastOverflowedIntToUint(int256 value);
/**
* @dev An int value doesn't fit in an uint of \`bits\` size.
*/
error SafeCastOverflowedIntToUint(int256 value);
/**
* @dev Value doesn't fit in an int of \`bits\` size.
*/
error SafeCastOverflowedIntDowncast(uint8 bits, int256 value);
/**
* @dev Value doesn't fit in an int of \`bits\` size.
*/
error SafeCastOverflowedIntDowncast(uint8 bits, int256 value);
/**
* @dev An uint value doesn't fit in an int of \`bits\` size.
*/
error SafeCastOverflowedUintToInt(uint256 value);
/**
* @dev An uint value doesn't fit in an int of \`bits\` size.
*/
error SafeCastOverflowedUintToInt(uint256 value);
`;
const toUintDownCast = length => `\
@ -55,7 +55,7 @@ const toUintDownCast = length => `\
*/
function toUint${length}(uint256 value) internal pure returns (uint${length}) {
if (value > type(uint${length}).max) {
revert SafeCastOverflowedUintDowncast(${length}, value);
revert SafeCastOverflowedUintDowncast(${length}, value);
}
return uint${length}(value);
}
@ -77,7 +77,7 @@ const toIntDownCast = length => `\
function toInt${length}(int256 value) internal pure returns (int${length} downcasted) {
downcasted = int${length}(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(${length}, value);
revert SafeCastOverflowedIntDowncast(${length}, value);
}
}
`;
@ -94,7 +94,7 @@ const toInt = length => `\
function toInt${length}(uint${length} value) internal pure returns (int${length}) {
// Note: Unsafe cast below is okay because \`type(int${length}).max\` is guaranteed to be positive
if (value > uint${length}(type(int${length}).max)) {
revert SafeCastOverflowedUintToInt(value);
revert SafeCastOverflowedUintToInt(value);
}
return int${length}(value);
}
@ -110,29 +110,30 @@ const toUint = length => `\
*/
function toUint${length}(int${length} value) internal pure returns (uint${length}) {
if (value < 0) {
revert SafeCastOverflowedIntToUint(value);
revert SafeCastOverflowedIntToUint(value);
}
return uint${length}(value);
}
`;
const boolToUint = `
/**
* @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump.
*/
function toUint(bool b) internal pure returns (uint256 u) {
/// @solidity memory-safe-assembly
assembly {
u := iszero(iszero(b))
}
}
const boolToUint = `\
/**
* @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump.
*/
function toUint(bool b) internal pure returns (uint256 u) {
/// @solidity memory-safe-assembly
assembly {
u := iszero(iszero(b))
}
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'library SafeCast {',
errors,
[...LENGTHS.map(toUintDownCast), toUint(256), ...LENGTHS.map(toIntDownCast), toInt(256), boolToUint],
format(
[].concat(errors, LENGTHS.map(toUintDownCast), toUint(256), LENGTHS.map(toIntDownCast), toInt(256), boolToUint),
).trimEnd(),
'}',
);

@ -43,11 +43,11 @@ const namespace = `\
* @dev Derive an ERC-7201 slot from a string (namespace).
*/
function erc7201Slot(string memory namespace) internal pure returns (bytes32 slot) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, sub(keccak256(add(namespace, 0x20), mload(namespace)), 1))
slot := and(keccak256(0x00, 0x20), not(0xff))
}
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, sub(keccak256(add(namespace, 0x20), mload(namespace)), 1))
slot := and(keccak256(0x00, 0x20), not(0xff))
}
}
`;
@ -56,20 +56,20 @@ const array = `\
* @dev Add an offset to a slot to get the n-th element of a structure or an array.
*/
function offset(bytes32 slot, uint256 pos) internal pure returns (bytes32 result) {
unchecked {
return bytes32(uint256(slot) + pos);
}
unchecked {
return bytes32(uint256(slot) + pos);
}
}
/**
* @dev Derive the location of the first element in an array from the slot where the length is stored.
*/
function deriveArray(bytes32 slot) internal pure returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, slot)
result := keccak256(0x00, 0x20)
}
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, slot)
result := keccak256(0x00, 0x20)
}
}
`;
@ -78,12 +78,12 @@ const mapping = ({ type }) => `\
* @dev Derive the location of a mapping element from the key.
*/
function deriveMapping(bytes32 slot, ${type} key) internal pure returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, key)
mstore(0x20, slot)
result := keccak256(0x00, 0x40)
}
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, key)
mstore(0x20, slot)
result := keccak256(0x00, 0x40)
}
}
`;
@ -92,16 +92,16 @@ const mapping2 = ({ type }) => `\
* @dev Derive the location of a mapping element from the key.
*/
function deriveMapping(bytes32 slot, ${type} memory key) internal pure returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
let length := mload(key)
let begin := add(key, 0x20)
let end := add(begin, length)
let cache := mload(end)
mstore(end, slot)
result := keccak256(begin, add(length, 0x20))
mstore(end, cache)
}
/// @solidity memory-safe-assembly
assembly {
let length := mload(key)
let begin := add(key, 0x20)
let end := add(begin, length)
let cache := mload(end)
mstore(end, slot)
result := keccak256(begin, add(length, 0x20))
mstore(end, cache)
}
}
`;
@ -109,8 +109,12 @@ function deriveMapping(bytes32 slot, ${type} memory key) internal pure returns (
module.exports = format(
header.trimEnd(),
'library SlotDerivation {',
namespace,
array,
TYPES.map(type => (type.isValueType ? mapping(type) : mapping2(type))),
format(
[].concat(
namespace,
array,
TYPES.map(type => (type.isValueType ? mapping(type) : mapping2(type))),
),
).trimEnd(),
'}',
);

@ -6,68 +6,108 @@ const header = `\
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {SymTest} from "halmos-cheatcodes/SymTest.sol";
import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol";
`;
const array = `\
bytes[] private _array;
function symbolicDeriveArray(uint256 length, uint256 offset) public {
vm.assume(length > 0);
vm.assume(offset < length);
_assertDeriveArray(length, offset);
}
function testDeriveArray(uint256 length, uint256 offset) public {
length = bound(length, 1, type(uint256).max);
offset = bound(offset, 0, length - 1);
bytes32 baseSlot;
assembly {
baseSlot := _array.slot
sstore(baseSlot, length) // store length so solidity access does not revert
}
bytes storage derived = _array[offset];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveArray().offset(offset), derivedSlot);
length = bound(length, 1, type(uint256).max);
offset = bound(offset, 0, length - 1);
_assertDeriveArray(length, offset);
}
function _assertDeriveArray(uint256 length, uint256 offset) public {
bytes32 baseSlot;
assembly {
baseSlot := _array.slot
sstore(baseSlot, length) // store length so solidity access does not revert
}
bytes storage derived = _array[offset];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveArray().offset(offset), derivedSlot);
}
`;
const mapping = ({ type, name }) => `\
mapping(${type} => bytes) private _${type}Mapping;
function testSymbolicDeriveMapping${name}(${type} key) public {
bytes32 baseSlot;
assembly {
baseSlot := _${type}Mapping.slot
}
bytes storage derived = _${type}Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
`;
const mapping = ({ type, name, isValueType }) => `\
const boundedMapping = ({ type, name }) => `\
mapping(${type} => bytes) private _${type}Mapping;
function testDeriveMapping${name}(${type} ${isValueType ? '' : 'memory'} key) public {
bytes32 baseSlot;
assembly {
baseSlot := _${type}Mapping.slot
}
function testDeriveMapping${name}(${type} memory key) public {
_assertDeriveMapping${name}(key);
}
function symbolicDeriveMapping${name}() public {
_assertDeriveMapping${name}(svm.create${name}(256, "DeriveMapping${name}Input"));
}
function _assertDeriveMapping${name}(${type} memory key) internal {
bytes32 baseSlot;
assembly {
baseSlot := _${type}Mapping.slot
}
bytes storage derived = _${type}Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
bytes storage derived = _${type}Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
'contract SlotDerivationTest is Test {',
'using SlotDerivation for bytes32;',
'',
array,
TYPES.flatMap(type =>
header,
'contract SlotDerivationTest is Test, SymTest {',
format(
[].concat(
type,
(type.variants ?? []).map(variant => ({
type: variant,
name: capitalize(variant),
isValueType: type.isValueType,
})),
'using SlotDerivation for bytes32;',
'',
array,
TYPES.flatMap(type =>
[].concat(
type,
(type.variants ?? []).map(variant => ({
type: variant,
name: capitalize(variant),
isValueType: type.isValueType,
})),
),
).map(type => (type.isValueType ? mapping(type) : boundedMapping(type))),
),
).map(type => mapping(type)),
).trimEnd(),
'}',
);

@ -53,7 +53,7 @@ pragma solidity ^0.8.24;
const struct = ({ type, name }) => `\
struct ${name}Slot {
${type} value;
${type} value;
}
`;
@ -62,10 +62,10 @@ const get = ({ name }) => `\
* @dev Returns an \`${name}Slot\` with member \`value\` located at \`slot\`.
*/
function get${name}Slot(bytes32 slot) internal pure returns (${name}Slot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
/// @solidity memory-safe-assembly
assembly {
r.slot := slot
}
}
`;
@ -74,10 +74,10 @@ const getStorage = ({ type, name }) => `\
* @dev Returns an \`${name}Slot\` representation of the ${type} storage pointer \`store\`.
*/
function get${name}Slot(${type} storage store) internal pure returns (${name}Slot storage r) {
/// @solidity memory-safe-assembly
assembly {
r.slot := store.slot
}
/// @solidity memory-safe-assembly
assembly {
r.slot := store.slot
}
}
`;
@ -86,11 +86,12 @@ const udvt = ({ type, name }) => `\
* @dev UDVT that represent a slot holding a ${type}.
*/
type ${name}SlotType is bytes32;
/**
* @dev Cast an arbitrary slot to a ${name}SlotType.
*/
function as${name}(bytes32 slot) internal pure returns (${name}SlotType) {
return ${name}SlotType.wrap(slot);
return ${name}SlotType.wrap(slot);
}
`;
@ -99,19 +100,20 @@ const transient = ({ type, name }) => `\
* @dev Load the value held at location \`slot\` in transient storage.
*/
function tload(${name}SlotType slot) internal view returns (${type} value) {
/// @solidity memory-safe-assembly
assembly {
value := tload(slot)
}
/// @solidity memory-safe-assembly
assembly {
value := tload(slot)
}
}
/**
* @dev Store \`value\` at location \`slot\` in transient storage.
*/
function tstore(${name}SlotType slot, ${type} value) internal {
/// @solidity memory-safe-assembly
assembly {
tstore(slot, value)
}
/// @solidity memory-safe-assembly
assembly {
tstore(slot, value)
}
}
`;
@ -119,9 +121,13 @@ function tstore(${name}SlotType slot, ${type} value) internal {
module.exports = format(
header.trimEnd(),
'library StorageSlot {',
TYPES.map(type => struct(type)),
TYPES.flatMap(type => [get(type), type.isValueType ? '' : getStorage(type)]),
TYPES.filter(type => type.isValueType).map(type => udvt(type)),
TYPES.filter(type => type.isValueType).map(type => transient(type)),
format(
[].concat(
TYPES.map(type => struct(type)),
TYPES.flatMap(type => [get(type), !type.isValueType && getStorage(type)].filter(Boolean)),
TYPES.filter(type => type.isValueType).map(type => udvt(type)),
TYPES.filter(type => type.isValueType).map(type => transient(type)),
),
).trimEnd(),
'}',
);

@ -44,22 +44,27 @@ const transient = ({ type, name }) => `\
event ${name}Value(bytes32 slot, ${type} value);
function tload${name}(bytes32 slot) public {
emit ${name}Value(slot, slot.as${name}().tload());
emit ${name}Value(slot, slot.as${name}().tload());
}
function tstore(bytes32 slot, ${type} value) public {
slot.as${name}().tstore(value);
slot.as${name}().tstore(value);
}
`;
// GENERATE
module.exports = format(
header.trimEnd(),
header,
'contract StorageSlotMock is Multicall {',
'using StorageSlot for *;',
TYPES.filter(type => type.isValueType).map(type => storageSetValueType(type)),
TYPES.filter(type => type.isValueType).map(type => storageGetValueType(type)),
TYPES.filter(type => !type.isValueType).map(type => storageSetNonValueType(type)),
TYPES.filter(type => type.isValueType).map(type => transient(type)),
format(
[].concat(
'using StorageSlot for *;',
'',
TYPES.filter(type => type.isValueType).map(type => storageSetValueType(type)),
TYPES.filter(type => type.isValueType).map(type => storageGetValueType(type)),
TYPES.filter(type => !type.isValueType).map(type => storageSetNonValueType(type)),
TYPES.filter(type => type.isValueType).map(type => transient(type)),
),
).trimEnd(),
'}',
);

@ -193,9 +193,65 @@ function shouldBehaveLikeAManagedRestrictedOperation() {
});
}
/**
* @requires this.{target,manager,roles,calldata,role}
*/
function shouldBehaveLikeASelfRestrictedOperation() {
function revertUnauthorized() {
it('reverts as AccessManagerUnauthorizedAccount', async function () {
await expect(this.caller.sendTransaction({ to: this.target, data: this.calldata }))
.to.be.revertedWithCustomError(this.manager, 'AccessManagerUnauthorizedAccount')
.withArgs(this.caller, this.role?.id ?? 0n);
});
}
const getAccessPath = LIKE_COMMON_GET_ACCESS;
function testScheduleOperation(mineDelay) {
return function self() {
self.mineDelay = mineDelay;
beforeEach('sets execution delay', async function () {
this.scheduleIn = this.executionDelay; // For testAsSchedulableOperation
});
testAsSchedulableOperation(LIKE_COMMON_SCHEDULABLE);
};
}
getAccessPath.requiredRoleIsGranted.roleGrantingIsDelayed.callerHasAnExecutionDelay.afterGrantDelay =
testScheduleOperation(true);
getAccessPath.requiredRoleIsGranted.roleGrantingIsNotDelayed.callerHasAnExecutionDelay = testScheduleOperation(false);
beforeEach('set target as manager', function () {
this.target = this.manager;
});
const isExecutingPath = LIKE_COMMON_IS_EXECUTING;
isExecutingPath.notExecuting = revertUnauthorized;
testAsCanCall({
closed: revertUnauthorized,
open: {
callerIsTheManager: isExecutingPath,
callerIsNotTheManager: {
publicRoleIsRequired() {
it('succeeds called directly', async function () {
await this.caller.sendTransaction({ to: this.target, data: this.calldata });
});
it('succeeds via execute', async function () {
await this.manager.connect(this.caller).execute(this.target, this.calldata);
});
},
specificRoleIsRequired: getAccessPath,
},
},
});
}
module.exports = {
shouldBehaveLikeDelayedAdminOperation,
shouldBehaveLikeNotDelayedAdminOperation,
shouldBehaveLikeRoleAdminOperation,
shouldBehaveLikeAManagedRestrictedOperation,
shouldBehaveLikeASelfRestrictedOperation,
};

@ -23,6 +23,7 @@ const {
shouldBehaveLikeNotDelayedAdminOperation,
shouldBehaveLikeRoleAdminOperation,
shouldBehaveLikeAManagedRestrictedOperation,
shouldBehaveLikeASelfRestrictedOperation,
} = require('./AccessManager.behavior');
const {
@ -48,7 +49,7 @@ async function fixture() {
roles.SOME.members = [member];
roles.PUBLIC.members = [admin, roleAdmin, roleGuardian, member, user, other];
const manager = await ethers.deployContract('$AccessManager', [admin]);
const manager = await ethers.deployContract('$AccessManagerMock', [admin]);
const target = await ethers.deployContract('$AccessManagedTarget', [manager]);
for (const { id: roleId, admin, guardian, members } of Object.values(roles)) {
@ -1105,10 +1106,18 @@ describe('AccessManager', function () {
expect(await this.manager.isTargetClosed(this.target)).to.be.false;
});
it('reverts if closing the manager', async function () {
await expect(this.manager.connect(this.admin).setTargetClosed(this.manager, true))
.to.be.revertedWithCustomError(this.manager, 'AccessManagerLockedAccount')
.withArgs(this.manager);
describe('when the target is the manager', async function () {
it('closes and opens the manager', async function () {
await expect(this.manager.connect(this.admin).setTargetClosed(this.manager, true))
.to.emit(this.manager, 'TargetClosed')
.withArgs(this.manager, true);
expect(await this.manager.isTargetClosed(this.manager)).to.be.true;
await expect(this.manager.connect(this.admin).setTargetClosed(this.manager, false))
.to.emit(this.manager, 'TargetClosed')
.withArgs(this.manager, false);
expect(await this.manager.isTargetClosed(this.manager)).to.be.false;
});
});
});
@ -1670,18 +1679,74 @@ describe('AccessManager', function () {
});
});
describe('access managed self operations', function () {
describe('when calling a restricted target function', function () {
const method = 'fnRestricted()';
beforeEach('set required role', async function () {
this.role = { id: 785913n };
await this.manager.$_setTargetFunctionRole(
this.manager,
this.manager[method].getFragment().selector,
this.role.id,
);
});
describe('restrictions', function () {
beforeEach('set method and args', function () {
this.caller = this.user;
this.calldata = this.manager.interface.encodeFunctionData(method, []);
});
shouldBehaveLikeASelfRestrictedOperation();
});
it('succeeds called by a role member', async function () {
await this.manager.$_grantRole(this.role.id, this.user, 0, 0);
await expect(this.manager.connect(this.user)[method]())
.to.emit(this.manager, 'CalledRestricted')
.withArgs(this.user);
});
});
describe('when calling a non-restricted target function', function () {
const method = 'fnUnrestricted()';
beforeEach('set required role', async function () {
this.role = { id: 879435n };
await this.manager.$_setTargetFunctionRole(
this.manager,
this.manager[method].getFragment().selector,
this.role.id,
);
});
it('succeeds called by anyone', async function () {
await expect(this.manager.connect(this.user)[method]())
.to.emit(this.manager, 'CalledUnrestricted')
.withArgs(this.user);
});
});
});
describe('access managed target operations', function () {
describe('when calling a restricted target function', function () {
beforeEach('set required role', function () {
this.method = this.target.fnRestricted.getFragment();
const method = 'fnRestricted()';
beforeEach('set required role', async function () {
this.role = { id: 3597243n };
this.manager.$_setTargetFunctionRole(this.target, this.method.selector, this.role.id);
await this.manager.$_setTargetFunctionRole(
this.target,
this.target[method].getFragment().selector,
this.role.id,
);
});
describe('restrictions', function () {
beforeEach('set method and args', function () {
this.calldata = this.target.interface.encodeFunctionData(this.method, []);
this.caller = this.user;
this.calldata = this.target.interface.encodeFunctionData(method, []);
});
shouldBehaveLikeAManagedRestrictedOperation();
@ -1690,11 +1755,7 @@ describe('AccessManager', function () {
it('succeeds called by a role member', async function () {
await this.manager.$_grantRole(this.role.id, this.user, 0, 0);
await expect(
this.target.connect(this.user)[this.method.selector]({
data: this.calldata,
}),
)
await expect(this.target.connect(this.user)[method]())
.to.emit(this.target, 'CalledRestricted')
.withArgs(this.user);
});
@ -1713,11 +1774,7 @@ describe('AccessManager', function () {
});
it('succeeds called by anyone', async function () {
await expect(
this.target.connect(this.user)[method]({
data: this.calldata,
}),
)
await expect(this.target.connect(this.user)[method]())
.to.emit(this.target, 'CalledUnrestricted')
.withArgs(this.user);
});

@ -26,7 +26,7 @@ contract GovernorInternalTest is Test, Governor {
assertFalse(_isValidDescriptionForProposer(actualProposer, description));
}
// We don't need to truly implement implement the missing functions because we are just testing
// We don't need to truly implement the missing functions because we are just testing
// internal helpers.
function clock() public pure override returns (uint48) {}
@ -51,5 +51,5 @@ contract GovernorInternalTest is Test, Governor {
function _getVotes(address, uint256, bytes memory) internal pure virtual override returns (uint256) {}
function _countVote(uint256, address, uint8, uint256, bytes memory) internal virtual override {}
function _countVote(uint256, address, uint8, uint256, bytes memory) internal virtual override returns (uint256) {}
}

@ -0,0 +1,248 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { GovernorHelper } = require('../../helpers/governance');
const { VoteType } = require('../../helpers/enums');
const { zip } = require('../../helpers/iterate');
const { sum } = require('../../helpers/math');
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');
describe('GovernorCountingFractional', function () {
for (const { Token, mode } of TOKENS) {
const fixture = async () => {
const [owner, proposer, voter1, voter2, voter3, voter4, other] = await ethers.getSigners();
const receiver = await ethers.deployContract('CallReceiverMock');
const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]);
const mock = await ethers.deployContract('$GovernorFractionalMock', [
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, to: voter1, value: ethers.parseEther('10') });
await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') });
await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') });
await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') });
return { owner, proposer, voter1, voter2, voter3, voter4, other, receiver, token, mock, helper };
};
describe(`using ${Token}`, function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
// default proposal
this.proposal = this.helper.setProposal(
[
{
target: this.receiver.target,
value,
data: this.receiver.interface.encodeFunctionData('mockFunction'),
},
],
'<proposal description>',
);
});
it('deployment check', async function () {
expect(await this.mock.name()).to.equal(name);
expect(await this.mock.token()).to.equal(this.token);
expect(await this.mock.votingDelay()).to.equal(votingDelay);
expect(await this.mock.votingPeriod()).to.equal(votingPeriod);
expect(await this.mock.COUNTING_MODE()).to.equal(
'support=bravo,fractional&quorum=for,abstain&params=fractional',
);
});
it('nominal is unaffected', async function () {
await this.helper.connect(this.proposer).propose();
await this.helper.waitForSnapshot();
await this.helper.connect(this.voter1).vote({ support: VoteType.For, reason: 'This is nice' });
await this.helper.connect(this.voter2).vote({ support: VoteType.For });
await this.helper.connect(this.voter3).vote({ support: VoteType.Against });
await this.helper.connect(this.voter4).vote({ support: VoteType.Abstain });
await this.helper.waitForDeadline();
await this.helper.execute();
expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false;
expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.true;
expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.true;
expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
expect(await ethers.provider.getBalance(this.receiver)).to.equal(value);
});
describe('voting with a fraction of the weight', function () {
it('twice', async function () {
await this.helper.connect(this.proposer).propose();
await this.helper.waitForSnapshot();
expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, 0n, 0n]);
expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(false);
expect(await this.mock.usedVotes(this.proposal.id, this.voter2)).to.equal(0n);
const steps = [
['0', '2', '1'],
['1', '0', '1'],
].map(votes => votes.map(vote => ethers.parseEther(vote)));
for (const votes of steps) {
const params = ethers.solidityPacked(['uint128', 'uint128', 'uint128'], votes);
await expect(
this.helper.connect(this.voter2).vote({
support: VoteType.Parameters,
reason: 'no particular reason',
params,
}),
)
.to.emit(this.mock, 'VoteCastWithParams')
.withArgs(
this.voter2,
this.proposal.id,
VoteType.Parameters,
sum(...votes),
'no particular reason',
params,
);
}
expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal(zip(...steps).map(v => sum(...v)));
expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(true);
expect(await this.mock.usedVotes(this.proposal.id, this.voter2)).to.equal(sum(...[].concat(...steps)));
});
it('fractional then nominal', async function () {
await this.helper.connect(this.proposer).propose();
await this.helper.waitForSnapshot();
expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([0n, 0n, 0n]);
expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(false);
expect(await this.mock.usedVotes(this.proposal.id, this.voter2)).to.equal(0n);
const weight = ethers.parseEther('7');
const fractional = ['1', '2', '1'].map(ethers.parseEther);
const params = ethers.solidityPacked(['uint128', 'uint128', 'uint128'], fractional);
await expect(
this.helper.connect(this.voter2).vote({
support: VoteType.Parameters,
reason: 'no particular reason',
params,
}),
)
.to.emit(this.mock, 'VoteCastWithParams')
.withArgs(
this.voter2,
this.proposal.id,
VoteType.Parameters,
sum(...fractional),
'no particular reason',
params,
);
await expect(this.helper.connect(this.voter2).vote({ support: VoteType.Against }))
.to.emit(this.mock, 'VoteCast')
.withArgs(this.voter2, this.proposal.id, VoteType.Against, weight - sum(...fractional), '');
expect(await this.mock.proposalVotes(this.proposal.id)).to.deep.equal([
weight - sum(...fractional.slice(1)),
...fractional.slice(1),
]);
expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.equal(true);
expect(await this.mock.usedVotes(this.proposal.id, this.voter2)).to.equal(weight);
});
it('revert if params spend more than available', async function () {
await this.helper.connect(this.proposer).propose();
await this.helper.waitForSnapshot();
const weight = ethers.parseEther('7');
const fractional = ['0', '1000', '0'].map(ethers.parseEther);
await expect(
this.helper.connect(this.voter2).vote({
support: VoteType.Parameters,
reason: 'no particular reason',
params: ethers.solidityPacked(['uint128', 'uint128', 'uint128'], fractional),
}),
)
.to.be.revertedWithCustomError(this.mock, 'GovernorExceedRemainingWeight')
.withArgs(this.voter2, sum(...fractional), weight);
});
it('revert if no weight remaining', async function () {
await this.helper.connect(this.proposer).propose();
await this.helper.waitForSnapshot();
await this.helper.connect(this.voter2).vote({ support: VoteType.For });
await expect(
this.helper.connect(this.voter2).vote({
support: VoteType.Parameters,
reason: 'no particular reason',
params: ethers.solidityPacked(['uint128', 'uint128', 'uint128'], [0n, 1n, 0n]),
}),
)
.to.be.revertedWithCustomError(this.mock, 'GovernorAlreadyCastVote')
.withArgs(this.voter2);
});
it('revert if params are not properly formatted #1', async function () {
await this.helper.connect(this.proposer).propose();
await this.helper.waitForSnapshot();
await expect(
this.helper.connect(this.voter2).vote({
support: VoteType.Parameters,
reason: 'no particular reason',
params: ethers.solidityPacked(['uint128', 'uint128'], [0n, 1n]),
}),
).to.be.revertedWithCustomError(this.mock, 'GovernorInvalidVoteParams');
});
it('revert if params are not properly formatted #2', async function () {
await this.helper.connect(this.proposer).propose();
await this.helper.waitForSnapshot();
await expect(
this.helper.connect(this.voter2).vote({
support: VoteType.Against,
reason: 'no particular reason',
params: ethers.solidityPacked(['uint128', 'uint128', 'uint128'], [0n, 1n, 0n]),
}),
).to.be.revertedWithCustomError(this.mock, 'GovernorInvalidVoteParams');
});
it('revert if vote type is invalid', async function () {
await this.helper.connect(this.proposer).propose();
await this.helper.waitForSnapshot();
await expect(this.helper.connect(this.voter2).vote({ support: 128n })).to.be.revertedWithCustomError(
this.mock,
'GovernorInvalidVoteType',
);
});
});
});
}
});

@ -74,7 +74,7 @@ describe('GovernorVotesQuorumFraction', function () {
);
});
it('quroum reached', async function () {
it('quorum reached', async function () {
await this.helper.propose();
await this.helper.waitForSnapshot();
await this.helper.connect(this.voter1).vote({ support: VoteType.For });
@ -82,7 +82,7 @@ describe('GovernorVotesQuorumFraction', function () {
await this.helper.execute();
});
it('quroum not reached', async function () {
it('quorum not reached', async function () {
await this.helper.propose();
await this.helper.waitForSnapshot();
await this.helper.connect(this.voter2).vote({ support: VoteType.For });

@ -0,0 +1,14 @@
const { artifacts, ethers } = require('hardhat');
const { setCode } = require('@nomicfoundation/hardhat-network-helpers');
const { generators } = require('./random');
const forceDeployCode = (name, address = generators.address(), runner = ethers.provider) =>
artifacts
.readArtifact(name)
.then(({ abi, deployedBytecode }) =>
setCode(address, deployedBytecode).then(() => new ethers.Contract(address, abi, runner)),
);
module.exports = {
forceDeployCode,
};

@ -5,7 +5,7 @@ function Enum(...options) {
module.exports = {
Enum,
ProposalState: Enum('Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'),
VoteType: Enum('Against', 'For', 'Abstain'),
VoteType: Object.assign(Enum('Against', 'For', 'Abstain'), { Parameters: 255n }),
Rounding: Enum('Floor', 'Ceil', 'Trunc', 'Expand'),
OperationState: Enum('Unset', 'Waiting', 'Ready', 'Done'),
RevertType: Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'),

@ -6,7 +6,11 @@ import {Test} from "forge-std/Test.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
contract ClonesTest is Test {
function testPredictDeterministicAddressSpillage(address implementation, bytes32 salt) public {
function getNumber() external pure returns (uint256) {
return 42;
}
function testSymbolicPredictDeterministicAddressSpillage(address implementation, bytes32 salt) public {
address predicted = Clones.predictDeterministicAddress(implementation, salt);
bytes32 spillage;
/// @solidity memory-safe-assembly
@ -15,4 +19,42 @@ contract ClonesTest is Test {
}
assertEq(spillage, bytes32(0));
}
function testCloneDirty() external {
address cloneClean = Clones.clone(address(this));
address cloneDirty = Clones.clone(_dirty(address(this)));
// both clones have the same code
assertEq(keccak256(cloneClean.code), keccak256(cloneDirty.code));
// both clones behave as expected
assertEq(ClonesTest(cloneClean).getNumber(), this.getNumber());
assertEq(ClonesTest(cloneDirty).getNumber(), this.getNumber());
}
function testCloneDeterministicDirty(bytes32 salt) external {
address cloneClean = Clones.cloneDeterministic(address(this), salt);
address cloneDirty = Clones.cloneDeterministic(_dirty(address(this)), ~salt);
// both clones have the same code
assertEq(keccak256(cloneClean.code), keccak256(cloneDirty.code));
// both clones behave as expected
assertEq(ClonesTest(cloneClean).getNumber(), this.getNumber());
assertEq(ClonesTest(cloneDirty).getNumber(), this.getNumber());
}
function testPredictDeterministicAddressDirty(bytes32 salt) external {
address predictClean = Clones.predictDeterministicAddress(address(this), salt);
address predictDirty = Clones.predictDeterministicAddress(_dirty(address(this)), salt);
//prediction should be similar
assertEq(predictClean, predictDirty);
}
function _dirty(address input) private pure returns (address output) {
assembly ("memory-safe") {
output := or(input, shl(160, not(0)))
}
}
}

@ -56,13 +56,13 @@ describe('SafeERC20', function () {
it('reverts on transfer', async function () {
await expect(this.mock.$safeTransfer(this.token, this.receiver, 0n))
.to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode')
.to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
.withArgs(this.token);
});
it('reverts on transferFrom', async function () {
await expect(this.mock.$safeTransferFrom(this.token, this.mock, this.receiver, 0n))
.to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode')
.to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
.withArgs(this.token);
});
@ -78,7 +78,7 @@ describe('SafeERC20', function () {
it('reverts on forceApprove', async function () {
await expect(this.mock.$forceApprove(this.token, this.spender, 0n))
.to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode')
.to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation')
.withArgs(this.token);
});
});

@ -3,11 +3,27 @@
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {SymTest} from "halmos-cheatcodes/SymTest.sol";
import {Arrays} from "@openzeppelin/contracts/utils/Arrays.sol";
contract ArraysTest is Test {
contract ArraysTest is Test, SymTest {
function testSort(uint256[] memory values) public {
Arrays.sort(values);
_assertSort(values);
}
function symbolicSort() public {
uint256[] memory values = new uint256[](3);
for (uint256 i = 0; i < 3; i++) {
values[i] = svm.createUint256("arrayElement");
}
Arrays.sort(values);
_assertSort(values);
}
/// Asserts
function _assertSort(uint256[] memory values) internal {
for (uint256 i = 1; i < values.length; ++i) {
assertLe(values[i - 1], values[i]);
}

@ -6,7 +6,7 @@ import {Test} from "forge-std/Test.sol";
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
contract Create2Test is Test {
function testComputeAddressSpillage(bytes32 salt, bytes32 bytecodeHash, address deployer) public {
function testSymbolicComputeAddressSpillage(bytes32 salt, bytes32 bytecodeHash, address deployer) public {
address predicted = Create2.computeAddress(salt, bytecodeHash, deployer);
bytes32 spillage;
/// @solidity memory-safe-assembly

@ -1,6 +1,9 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
const { RevertType } = require('../helpers/enums');
async function fixture() {
const [deployer, other] = await ethers.getSigners();
@ -19,7 +22,9 @@ async function fixture() {
.getContractFactory('$Create2')
.then(({ bytecode, interface }) => ethers.concat([bytecode, interface.encodeDeploy([])]));
return { deployer, other, factory, constructorByteCode, constructorLessBytecode };
const mockFactory = await ethers.getContractFactory('ConstructorMock');
return { deployer, other, factory, constructorByteCode, constructorLessBytecode, mockFactory };
}
describe('Create2', function () {
@ -130,5 +135,56 @@ describe('Create2', function () {
.to.be.revertedWithCustomError(this.factory, 'InsufficientBalance')
.withArgs(0n, 1n);
});
describe('reverts error thrown during contract creation', function () {
it('bubbles up without message', async function () {
await expect(
this.factory.$deploy(
0n,
saltHex,
ethers.concat([
this.mockFactory.bytecode,
this.mockFactory.interface.encodeDeploy([RevertType.RevertWithoutMessage]),
]),
),
).to.be.revertedWithCustomError(this.factory, 'FailedDeployment');
});
it('bubbles up message', async function () {
await expect(
this.factory.$deploy(
0n,
saltHex,
ethers.concat([
this.mockFactory.bytecode,
this.mockFactory.interface.encodeDeploy([RevertType.RevertWithMessage]),
]),
),
).to.be.revertedWith('ConstructorMock: reverting');
});
it('bubbles up custom error', async function () {
await expect(
this.factory.$deploy(
0n,
saltHex,
ethers.concat([
this.mockFactory.bytecode,
this.mockFactory.interface.encodeDeploy([RevertType.RevertWithCustomError]),
]),
),
).to.be.revertedWithCustomError({ interface: this.mockFactory.interface }, 'CustomError');
});
it('bubbles up panic', async function () {
await expect(
this.factory.$deploy(
0n,
saltHex,
ethers.concat([this.mockFactory.bytecode, this.mockFactory.interface.encodeDeploy([RevertType.Panic])]),
),
).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO);
});
});
});
});

@ -1,4 +1,5 @@
// SPDX-License-Identifier: MIT
// This file was procedurally generated from scripts/generate/templates/Packing.t.js.
pragma solidity ^0.8.20;
@ -8,20 +9,673 @@ import {Packing} from "@openzeppelin/contracts/utils/Packing.sol";
contract PackingTest is Test {
using Packing for *;
// Pack a pair of arbitrary uint128, and check that split recovers the correct values
function testUint128x2(uint128 first, uint128 second) external {
Packing.Uint128x2 packed = Packing.pack(first, second);
assertEq(packed.first(), first);
assertEq(packed.second(), second);
function testPack(bytes1 left, bytes1 right) external {
assertEq(left, Packing.pack_1_1(left, right).extract_2_1(0));
assertEq(right, Packing.pack_1_1(left, right).extract_2_1(1));
}
function testPack(bytes2 left, bytes2 right) external {
assertEq(left, Packing.pack_2_2(left, right).extract_4_2(0));
assertEq(right, Packing.pack_2_2(left, right).extract_4_2(2));
}
function testPack(bytes2 left, bytes4 right) external {
assertEq(left, Packing.pack_2_4(left, right).extract_6_2(0));
assertEq(right, Packing.pack_2_4(left, right).extract_6_4(2));
}
function testPack(bytes2 left, bytes6 right) external {
assertEq(left, Packing.pack_2_6(left, right).extract_8_2(0));
assertEq(right, Packing.pack_2_6(left, right).extract_8_6(2));
}
function testPack(bytes4 left, bytes2 right) external {
assertEq(left, Packing.pack_4_2(left, right).extract_6_4(0));
assertEq(right, Packing.pack_4_2(left, right).extract_6_2(4));
}
function testPack(bytes4 left, bytes4 right) external {
assertEq(left, Packing.pack_4_4(left, right).extract_8_4(0));
assertEq(right, Packing.pack_4_4(left, right).extract_8_4(4));
}
function testPack(bytes4 left, bytes8 right) external {
assertEq(left, Packing.pack_4_8(left, right).extract_12_4(0));
assertEq(right, Packing.pack_4_8(left, right).extract_12_8(4));
}
function testPack(bytes4 left, bytes12 right) external {
assertEq(left, Packing.pack_4_12(left, right).extract_16_4(0));
assertEq(right, Packing.pack_4_12(left, right).extract_16_12(4));
}
function testPack(bytes4 left, bytes16 right) external {
assertEq(left, Packing.pack_4_16(left, right).extract_20_4(0));
assertEq(right, Packing.pack_4_16(left, right).extract_20_16(4));
}
function testPack(bytes4 left, bytes20 right) external {
assertEq(left, Packing.pack_4_20(left, right).extract_24_4(0));
assertEq(right, Packing.pack_4_20(left, right).extract_24_20(4));
}
function testPack(bytes4 left, bytes24 right) external {
assertEq(left, Packing.pack_4_24(left, right).extract_28_4(0));
assertEq(right, Packing.pack_4_24(left, right).extract_28_24(4));
}
function testPack(bytes4 left, bytes28 right) external {
assertEq(left, Packing.pack_4_28(left, right).extract_32_4(0));
assertEq(right, Packing.pack_4_28(left, right).extract_32_28(4));
}
function testPack(bytes6 left, bytes2 right) external {
assertEq(left, Packing.pack_6_2(left, right).extract_8_6(0));
assertEq(right, Packing.pack_6_2(left, right).extract_8_2(6));
}
function testPack(bytes6 left, bytes6 right) external {
assertEq(left, Packing.pack_6_6(left, right).extract_12_6(0));
assertEq(right, Packing.pack_6_6(left, right).extract_12_6(6));
}
function testPack(bytes8 left, bytes4 right) external {
assertEq(left, Packing.pack_8_4(left, right).extract_12_8(0));
assertEq(right, Packing.pack_8_4(left, right).extract_12_4(8));
}
function testPack(bytes8 left, bytes8 right) external {
assertEq(left, Packing.pack_8_8(left, right).extract_16_8(0));
assertEq(right, Packing.pack_8_8(left, right).extract_16_8(8));
}
function testPack(bytes8 left, bytes12 right) external {
assertEq(left, Packing.pack_8_12(left, right).extract_20_8(0));
assertEq(right, Packing.pack_8_12(left, right).extract_20_12(8));
}
function testPack(bytes8 left, bytes16 right) external {
assertEq(left, Packing.pack_8_16(left, right).extract_24_8(0));
assertEq(right, Packing.pack_8_16(left, right).extract_24_16(8));
}
function testPack(bytes8 left, bytes20 right) external {
assertEq(left, Packing.pack_8_20(left, right).extract_28_8(0));
assertEq(right, Packing.pack_8_20(left, right).extract_28_20(8));
}
function testPack(bytes8 left, bytes24 right) external {
assertEq(left, Packing.pack_8_24(left, right).extract_32_8(0));
assertEq(right, Packing.pack_8_24(left, right).extract_32_24(8));
}
function testPack(bytes12 left, bytes4 right) external {
assertEq(left, Packing.pack_12_4(left, right).extract_16_12(0));
assertEq(right, Packing.pack_12_4(left, right).extract_16_4(12));
}
function testPack(bytes12 left, bytes8 right) external {
assertEq(left, Packing.pack_12_8(left, right).extract_20_12(0));
assertEq(right, Packing.pack_12_8(left, right).extract_20_8(12));
}
function testPack(bytes12 left, bytes12 right) external {
assertEq(left, Packing.pack_12_12(left, right).extract_24_12(0));
assertEq(right, Packing.pack_12_12(left, right).extract_24_12(12));
}
function testPack(bytes12 left, bytes16 right) external {
assertEq(left, Packing.pack_12_16(left, right).extract_28_12(0));
assertEq(right, Packing.pack_12_16(left, right).extract_28_16(12));
}
function testPack(bytes12 left, bytes20 right) external {
assertEq(left, Packing.pack_12_20(left, right).extract_32_12(0));
assertEq(right, Packing.pack_12_20(left, right).extract_32_20(12));
}
function testPack(bytes16 left, bytes4 right) external {
assertEq(left, Packing.pack_16_4(left, right).extract_20_16(0));
assertEq(right, Packing.pack_16_4(left, right).extract_20_4(16));
}
function testPack(bytes16 left, bytes8 right) external {
assertEq(left, Packing.pack_16_8(left, right).extract_24_16(0));
assertEq(right, Packing.pack_16_8(left, right).extract_24_8(16));
}
function testPack(bytes16 left, bytes12 right) external {
assertEq(left, Packing.pack_16_12(left, right).extract_28_16(0));
assertEq(right, Packing.pack_16_12(left, right).extract_28_12(16));
}
function testPack(bytes16 left, bytes16 right) external {
assertEq(left, Packing.pack_16_16(left, right).extract_32_16(0));
assertEq(right, Packing.pack_16_16(left, right).extract_32_16(16));
}
function testPack(bytes20 left, bytes4 right) external {
assertEq(left, Packing.pack_20_4(left, right).extract_24_20(0));
assertEq(right, Packing.pack_20_4(left, right).extract_24_4(20));
}
function testPack(bytes20 left, bytes8 right) external {
assertEq(left, Packing.pack_20_8(left, right).extract_28_20(0));
assertEq(right, Packing.pack_20_8(left, right).extract_28_8(20));
}
function testPack(bytes20 left, bytes12 right) external {
assertEq(left, Packing.pack_20_12(left, right).extract_32_20(0));
assertEq(right, Packing.pack_20_12(left, right).extract_32_12(20));
}
function testPack(bytes24 left, bytes4 right) external {
assertEq(left, Packing.pack_24_4(left, right).extract_28_24(0));
assertEq(right, Packing.pack_24_4(left, right).extract_28_4(24));
}
function testPack(bytes24 left, bytes8 right) external {
assertEq(left, Packing.pack_24_8(left, right).extract_32_24(0));
assertEq(right, Packing.pack_24_8(left, right).extract_32_8(24));
}
function testPack(bytes28 left, bytes4 right) external {
assertEq(left, Packing.pack_28_4(left, right).extract_32_28(0));
assertEq(right, Packing.pack_28_4(left, right).extract_32_4(28));
}
function testReplace(bytes2 container, bytes1 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 1));
bytes1 oldValue = container.extract_2_1(offset);
assertEq(newValue, container.replace_2_1(newValue, offset).extract_2_1(offset));
assertEq(container, container.replace_2_1(newValue, offset).replace_2_1(oldValue, offset));
}
function testReplace(bytes4 container, bytes1 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 3));
bytes1 oldValue = container.extract_4_1(offset);
assertEq(newValue, container.replace_4_1(newValue, offset).extract_4_1(offset));
assertEq(container, container.replace_4_1(newValue, offset).replace_4_1(oldValue, offset));
}
function testReplace(bytes4 container, bytes2 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 2));
bytes2 oldValue = container.extract_4_2(offset);
assertEq(newValue, container.replace_4_2(newValue, offset).extract_4_2(offset));
assertEq(container, container.replace_4_2(newValue, offset).replace_4_2(oldValue, offset));
}
function testReplace(bytes6 container, bytes1 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 5));
bytes1 oldValue = container.extract_6_1(offset);
assertEq(newValue, container.replace_6_1(newValue, offset).extract_6_1(offset));
assertEq(container, container.replace_6_1(newValue, offset).replace_6_1(oldValue, offset));
}
function testReplace(bytes6 container, bytes2 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 4));
bytes2 oldValue = container.extract_6_2(offset);
assertEq(newValue, container.replace_6_2(newValue, offset).extract_6_2(offset));
assertEq(container, container.replace_6_2(newValue, offset).replace_6_2(oldValue, offset));
}
function testReplace(bytes6 container, bytes4 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 2));
bytes4 oldValue = container.extract_6_4(offset);
assertEq(newValue, container.replace_6_4(newValue, offset).extract_6_4(offset));
assertEq(container, container.replace_6_4(newValue, offset).replace_6_4(oldValue, offset));
}
function testReplace(bytes8 container, bytes1 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 7));
bytes1 oldValue = container.extract_8_1(offset);
assertEq(newValue, container.replace_8_1(newValue, offset).extract_8_1(offset));
assertEq(container, container.replace_8_1(newValue, offset).replace_8_1(oldValue, offset));
}
function testReplace(bytes8 container, bytes2 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 6));
bytes2 oldValue = container.extract_8_2(offset);
assertEq(newValue, container.replace_8_2(newValue, offset).extract_8_2(offset));
assertEq(container, container.replace_8_2(newValue, offset).replace_8_2(oldValue, offset));
}
function testReplace(bytes8 container, bytes4 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 4));
bytes4 oldValue = container.extract_8_4(offset);
assertEq(newValue, container.replace_8_4(newValue, offset).extract_8_4(offset));
assertEq(container, container.replace_8_4(newValue, offset).replace_8_4(oldValue, offset));
}
function testReplace(bytes8 container, bytes6 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 2));
bytes6 oldValue = container.extract_8_6(offset);
assertEq(newValue, container.replace_8_6(newValue, offset).extract_8_6(offset));
assertEq(container, container.replace_8_6(newValue, offset).replace_8_6(oldValue, offset));
}
function testReplace(bytes12 container, bytes1 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 11));
bytes1 oldValue = container.extract_12_1(offset);
assertEq(newValue, container.replace_12_1(newValue, offset).extract_12_1(offset));
assertEq(container, container.replace_12_1(newValue, offset).replace_12_1(oldValue, offset));
}
function testReplace(bytes12 container, bytes2 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 10));
bytes2 oldValue = container.extract_12_2(offset);
assertEq(newValue, container.replace_12_2(newValue, offset).extract_12_2(offset));
assertEq(container, container.replace_12_2(newValue, offset).replace_12_2(oldValue, offset));
}
function testReplace(bytes12 container, bytes4 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 8));
bytes4 oldValue = container.extract_12_4(offset);
assertEq(newValue, container.replace_12_4(newValue, offset).extract_12_4(offset));
assertEq(container, container.replace_12_4(newValue, offset).replace_12_4(oldValue, offset));
}
function testReplace(bytes12 container, bytes6 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 6));
bytes6 oldValue = container.extract_12_6(offset);
assertEq(newValue, container.replace_12_6(newValue, offset).extract_12_6(offset));
assertEq(container, container.replace_12_6(newValue, offset).replace_12_6(oldValue, offset));
}
function testReplace(bytes12 container, bytes8 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 4));
bytes8 oldValue = container.extract_12_8(offset);
assertEq(newValue, container.replace_12_8(newValue, offset).extract_12_8(offset));
assertEq(container, container.replace_12_8(newValue, offset).replace_12_8(oldValue, offset));
}
function testReplace(bytes16 container, bytes1 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 15));
bytes1 oldValue = container.extract_16_1(offset);
assertEq(newValue, container.replace_16_1(newValue, offset).extract_16_1(offset));
assertEq(container, container.replace_16_1(newValue, offset).replace_16_1(oldValue, offset));
}
function testReplace(bytes16 container, bytes2 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 14));
bytes2 oldValue = container.extract_16_2(offset);
assertEq(newValue, container.replace_16_2(newValue, offset).extract_16_2(offset));
assertEq(container, container.replace_16_2(newValue, offset).replace_16_2(oldValue, offset));
}
function testReplace(bytes16 container, bytes4 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 12));
bytes4 oldValue = container.extract_16_4(offset);
assertEq(newValue, container.replace_16_4(newValue, offset).extract_16_4(offset));
assertEq(container, container.replace_16_4(newValue, offset).replace_16_4(oldValue, offset));
}
function testReplace(bytes16 container, bytes6 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 10));
bytes6 oldValue = container.extract_16_6(offset);
(uint128 recoveredFirst, uint128 recoveredSecond) = packed.split();
assertEq(recoveredFirst, first);
assertEq(recoveredSecond, second);
assertEq(newValue, container.replace_16_6(newValue, offset).extract_16_6(offset));
assertEq(container, container.replace_16_6(newValue, offset).replace_16_6(oldValue, offset));
}
// split an arbitrary bytes32 into a pair of uint128, and check that repack matches the input
function testUint128x2(bytes32 input) external {
(uint128 first, uint128 second) = input.asUint128x2().split();
assertEq(Packing.pack(first, second).asBytes32(), input);
function testReplace(bytes16 container, bytes8 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 8));
bytes8 oldValue = container.extract_16_8(offset);
assertEq(newValue, container.replace_16_8(newValue, offset).extract_16_8(offset));
assertEq(container, container.replace_16_8(newValue, offset).replace_16_8(oldValue, offset));
}
function testReplace(bytes16 container, bytes12 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 4));
bytes12 oldValue = container.extract_16_12(offset);
assertEq(newValue, container.replace_16_12(newValue, offset).extract_16_12(offset));
assertEq(container, container.replace_16_12(newValue, offset).replace_16_12(oldValue, offset));
}
function testReplace(bytes20 container, bytes1 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 19));
bytes1 oldValue = container.extract_20_1(offset);
assertEq(newValue, container.replace_20_1(newValue, offset).extract_20_1(offset));
assertEq(container, container.replace_20_1(newValue, offset).replace_20_1(oldValue, offset));
}
function testReplace(bytes20 container, bytes2 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 18));
bytes2 oldValue = container.extract_20_2(offset);
assertEq(newValue, container.replace_20_2(newValue, offset).extract_20_2(offset));
assertEq(container, container.replace_20_2(newValue, offset).replace_20_2(oldValue, offset));
}
function testReplace(bytes20 container, bytes4 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 16));
bytes4 oldValue = container.extract_20_4(offset);
assertEq(newValue, container.replace_20_4(newValue, offset).extract_20_4(offset));
assertEq(container, container.replace_20_4(newValue, offset).replace_20_4(oldValue, offset));
}
function testReplace(bytes20 container, bytes6 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 14));
bytes6 oldValue = container.extract_20_6(offset);
assertEq(newValue, container.replace_20_6(newValue, offset).extract_20_6(offset));
assertEq(container, container.replace_20_6(newValue, offset).replace_20_6(oldValue, offset));
}
function testReplace(bytes20 container, bytes8 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 12));
bytes8 oldValue = container.extract_20_8(offset);
assertEq(newValue, container.replace_20_8(newValue, offset).extract_20_8(offset));
assertEq(container, container.replace_20_8(newValue, offset).replace_20_8(oldValue, offset));
}
function testReplace(bytes20 container, bytes12 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 8));
bytes12 oldValue = container.extract_20_12(offset);
assertEq(newValue, container.replace_20_12(newValue, offset).extract_20_12(offset));
assertEq(container, container.replace_20_12(newValue, offset).replace_20_12(oldValue, offset));
}
function testReplace(bytes20 container, bytes16 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 4));
bytes16 oldValue = container.extract_20_16(offset);
assertEq(newValue, container.replace_20_16(newValue, offset).extract_20_16(offset));
assertEq(container, container.replace_20_16(newValue, offset).replace_20_16(oldValue, offset));
}
function testReplace(bytes24 container, bytes1 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 23));
bytes1 oldValue = container.extract_24_1(offset);
assertEq(newValue, container.replace_24_1(newValue, offset).extract_24_1(offset));
assertEq(container, container.replace_24_1(newValue, offset).replace_24_1(oldValue, offset));
}
function testReplace(bytes24 container, bytes2 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 22));
bytes2 oldValue = container.extract_24_2(offset);
assertEq(newValue, container.replace_24_2(newValue, offset).extract_24_2(offset));
assertEq(container, container.replace_24_2(newValue, offset).replace_24_2(oldValue, offset));
}
function testReplace(bytes24 container, bytes4 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 20));
bytes4 oldValue = container.extract_24_4(offset);
assertEq(newValue, container.replace_24_4(newValue, offset).extract_24_4(offset));
assertEq(container, container.replace_24_4(newValue, offset).replace_24_4(oldValue, offset));
}
function testReplace(bytes24 container, bytes6 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 18));
bytes6 oldValue = container.extract_24_6(offset);
assertEq(newValue, container.replace_24_6(newValue, offset).extract_24_6(offset));
assertEq(container, container.replace_24_6(newValue, offset).replace_24_6(oldValue, offset));
}
function testReplace(bytes24 container, bytes8 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 16));
bytes8 oldValue = container.extract_24_8(offset);
assertEq(newValue, container.replace_24_8(newValue, offset).extract_24_8(offset));
assertEq(container, container.replace_24_8(newValue, offset).replace_24_8(oldValue, offset));
}
function testReplace(bytes24 container, bytes12 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 12));
bytes12 oldValue = container.extract_24_12(offset);
assertEq(newValue, container.replace_24_12(newValue, offset).extract_24_12(offset));
assertEq(container, container.replace_24_12(newValue, offset).replace_24_12(oldValue, offset));
}
function testReplace(bytes24 container, bytes16 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 8));
bytes16 oldValue = container.extract_24_16(offset);
assertEq(newValue, container.replace_24_16(newValue, offset).extract_24_16(offset));
assertEq(container, container.replace_24_16(newValue, offset).replace_24_16(oldValue, offset));
}
function testReplace(bytes24 container, bytes20 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 4));
bytes20 oldValue = container.extract_24_20(offset);
assertEq(newValue, container.replace_24_20(newValue, offset).extract_24_20(offset));
assertEq(container, container.replace_24_20(newValue, offset).replace_24_20(oldValue, offset));
}
function testReplace(bytes28 container, bytes1 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 27));
bytes1 oldValue = container.extract_28_1(offset);
assertEq(newValue, container.replace_28_1(newValue, offset).extract_28_1(offset));
assertEq(container, container.replace_28_1(newValue, offset).replace_28_1(oldValue, offset));
}
function testReplace(bytes28 container, bytes2 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 26));
bytes2 oldValue = container.extract_28_2(offset);
assertEq(newValue, container.replace_28_2(newValue, offset).extract_28_2(offset));
assertEq(container, container.replace_28_2(newValue, offset).replace_28_2(oldValue, offset));
}
function testReplace(bytes28 container, bytes4 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 24));
bytes4 oldValue = container.extract_28_4(offset);
assertEq(newValue, container.replace_28_4(newValue, offset).extract_28_4(offset));
assertEq(container, container.replace_28_4(newValue, offset).replace_28_4(oldValue, offset));
}
function testReplace(bytes28 container, bytes6 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 22));
bytes6 oldValue = container.extract_28_6(offset);
assertEq(newValue, container.replace_28_6(newValue, offset).extract_28_6(offset));
assertEq(container, container.replace_28_6(newValue, offset).replace_28_6(oldValue, offset));
}
function testReplace(bytes28 container, bytes8 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 20));
bytes8 oldValue = container.extract_28_8(offset);
assertEq(newValue, container.replace_28_8(newValue, offset).extract_28_8(offset));
assertEq(container, container.replace_28_8(newValue, offset).replace_28_8(oldValue, offset));
}
function testReplace(bytes28 container, bytes12 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 16));
bytes12 oldValue = container.extract_28_12(offset);
assertEq(newValue, container.replace_28_12(newValue, offset).extract_28_12(offset));
assertEq(container, container.replace_28_12(newValue, offset).replace_28_12(oldValue, offset));
}
function testReplace(bytes28 container, bytes16 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 12));
bytes16 oldValue = container.extract_28_16(offset);
assertEq(newValue, container.replace_28_16(newValue, offset).extract_28_16(offset));
assertEq(container, container.replace_28_16(newValue, offset).replace_28_16(oldValue, offset));
}
function testReplace(bytes28 container, bytes20 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 8));
bytes20 oldValue = container.extract_28_20(offset);
assertEq(newValue, container.replace_28_20(newValue, offset).extract_28_20(offset));
assertEq(container, container.replace_28_20(newValue, offset).replace_28_20(oldValue, offset));
}
function testReplace(bytes28 container, bytes24 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 4));
bytes24 oldValue = container.extract_28_24(offset);
assertEq(newValue, container.replace_28_24(newValue, offset).extract_28_24(offset));
assertEq(container, container.replace_28_24(newValue, offset).replace_28_24(oldValue, offset));
}
function testReplace(bytes32 container, bytes1 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 31));
bytes1 oldValue = container.extract_32_1(offset);
assertEq(newValue, container.replace_32_1(newValue, offset).extract_32_1(offset));
assertEq(container, container.replace_32_1(newValue, offset).replace_32_1(oldValue, offset));
}
function testReplace(bytes32 container, bytes2 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 30));
bytes2 oldValue = container.extract_32_2(offset);
assertEq(newValue, container.replace_32_2(newValue, offset).extract_32_2(offset));
assertEq(container, container.replace_32_2(newValue, offset).replace_32_2(oldValue, offset));
}
function testReplace(bytes32 container, bytes4 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 28));
bytes4 oldValue = container.extract_32_4(offset);
assertEq(newValue, container.replace_32_4(newValue, offset).extract_32_4(offset));
assertEq(container, container.replace_32_4(newValue, offset).replace_32_4(oldValue, offset));
}
function testReplace(bytes32 container, bytes6 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 26));
bytes6 oldValue = container.extract_32_6(offset);
assertEq(newValue, container.replace_32_6(newValue, offset).extract_32_6(offset));
assertEq(container, container.replace_32_6(newValue, offset).replace_32_6(oldValue, offset));
}
function testReplace(bytes32 container, bytes8 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 24));
bytes8 oldValue = container.extract_32_8(offset);
assertEq(newValue, container.replace_32_8(newValue, offset).extract_32_8(offset));
assertEq(container, container.replace_32_8(newValue, offset).replace_32_8(oldValue, offset));
}
function testReplace(bytes32 container, bytes12 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 20));
bytes12 oldValue = container.extract_32_12(offset);
assertEq(newValue, container.replace_32_12(newValue, offset).extract_32_12(offset));
assertEq(container, container.replace_32_12(newValue, offset).replace_32_12(oldValue, offset));
}
function testReplace(bytes32 container, bytes16 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 16));
bytes16 oldValue = container.extract_32_16(offset);
assertEq(newValue, container.replace_32_16(newValue, offset).extract_32_16(offset));
assertEq(container, container.replace_32_16(newValue, offset).replace_32_16(oldValue, offset));
}
function testReplace(bytes32 container, bytes20 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 12));
bytes20 oldValue = container.extract_32_20(offset);
assertEq(newValue, container.replace_32_20(newValue, offset).extract_32_20(offset));
assertEq(container, container.replace_32_20(newValue, offset).replace_32_20(oldValue, offset));
}
function testReplace(bytes32 container, bytes24 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 8));
bytes24 oldValue = container.extract_32_24(offset);
assertEq(newValue, container.replace_32_24(newValue, offset).extract_32_24(offset));
assertEq(container, container.replace_32_24(newValue, offset).replace_32_24(oldValue, offset));
}
function testReplace(bytes32 container, bytes28 newValue, uint8 offset) external {
offset = uint8(bound(offset, 0, 4));
bytes28 oldValue = container.extract_32_28(offset);
assertEq(newValue, container.replace_32_28(newValue, offset).extract_32_28(offset));
assertEq(container, container.replace_32_28(newValue, offset).replace_32_28(oldValue, offset));
}
}

@ -1,10 +1,13 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { generators } = require('../helpers/random');
const { forceDeployCode } = require('../helpers/deploy');
const { product } = require('../helpers/iterate');
const { SIZES } = require('../../scripts/generate/templates/Packing.opts');
async function fixture() {
return { mock: await ethers.deployContract('$Packing') };
return { mock: await forceDeployCode('$Packing') };
}
describe('Packing', function () {
@ -12,16 +15,56 @@ describe('Packing', function () {
Object.assign(this, await loadFixture(fixture));
});
it('Uint128x2', async function () {
const first = generators.uint256() % 2n ** 128n;
const second = generators.uint256() % 2n ** 128n;
const packed = ethers.hexlify(ethers.toBeArray((first << 128n) | second));
expect(await this.mock.$asUint128x2(packed)).to.equal(packed);
expect(await this.mock.$asBytes32(packed)).to.equal(packed);
expect(await this.mock.$pack(first, second)).to.equal(packed);
expect(await this.mock.$split(packed)).to.deep.equal([first, second]);
expect(await this.mock.$first(packed)).to.equal(first);
expect(await this.mock.$second(packed)).to.equal(second);
describe('pack', function () {
for (const [size1, size2] of product(SIZES, SIZES).filter(([size1, size2]) => SIZES.includes(size1 + size2))) {
const value1 = ethers.hexlify(ethers.randomBytes(size1));
const value2 = ethers.hexlify(ethers.randomBytes(size2));
const packed = ethers.concat([value1, value2]);
it(`pack bytes${size1} + bytes${size2} => bytes${size1 + size2}`, async function () {
expect(await this.mock[`$pack_${size1}_${size2}`](value1, value2)).to.equal(packed);
expect(await this.mock[`$extract_${size1 + size2}_${size1}`](packed, 0)).to.equal(value1);
expect(await this.mock[`$extract_${size1 + size2}_${size2}`](packed, size1)).to.equal(value2);
});
}
});
describe('extract / replace', function () {
for (const [size1, size2] of product(SIZES, SIZES).filter(([size1, size2]) => size1 > size2)) {
const MAX_OFFSET = size1 - size2;
const offset = ethers.toNumber(ethers.randomBytes(1)) % (MAX_OFFSET + 1);
const outer = ethers.randomBytes(size1);
const value = ethers.randomBytes(size2);
it(`extract bytes${size2} from bytes${size1}`, async function () {
expect(await this.mock[`$extract_${size1}_${size2}`](outer, offset)).to.equal(
ethers.hexlify(outer.slice(offset, offset + size2)),
);
await expect(this.mock[`$extract_${size1}_${size2}`](outer, MAX_OFFSET)).to.not.be.revertedWithCustomError(
this.mock,
'OutOfRangeAccess',
);
await expect(this.mock[`$extract_${size1}_${size2}`](outer, MAX_OFFSET + 1)).to.be.revertedWithCustomError(
this.mock,
'OutOfRangeAccess',
);
});
it(`replace bytes${size2} from bytes${size1}`, async function () {
expect(await this.mock[`$replace_${size1}_${size2}`](outer, value, offset)).to.equal(
ethers.concat([outer.slice(0, offset), value, outer.slice(offset + size2)]),
);
await expect(
this.mock[`$replace_${size1}_${size2}`](outer, value, MAX_OFFSET),
).to.not.be.revertedWithCustomError(this.mock, 'OutOfRangeAccess');
await expect(
this.mock[`$replace_${size1}_${size2}`](outer, value, MAX_OFFSET + 1),
).to.be.revertedWithCustomError(this.mock, 'OutOfRangeAccess');
});
}
});
});

@ -3,48 +3,102 @@
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {SymTest} from "halmos-cheatcodes/SymTest.sol";
import {ShortStrings, ShortString} from "@openzeppelin/contracts/utils/ShortStrings.sol";
contract ShortStringsTest is Test {
contract ShortStringsTest is Test, SymTest {
string _fallback;
function testRoundtripShort(string memory input) external {
vm.assume(_isShort(input));
_assertRoundtripShort(input);
}
function symbolicRoundtripShort() external {
string memory input = svm.createString(31, "RoundtripShortInput");
_assertRoundtripShort(input);
}
function testRoundtripWithFallback(string memory input, string memory fallbackInitial) external {
_assertRoundtripWithFallback(input, fallbackInitial);
}
function symbolicRoundtripWithFallbackLong() external {
string memory input = svm.createString(256, "RoundtripWithFallbackInput");
string memory fallbackInitial = svm.createString(256, "RoundtripWithFallbackFallbackInitial");
_assertRoundtripWithFallback(input, fallbackInitial);
}
function symbolicRoundtripWithFallbackShort() external {
string memory input = svm.createString(31, "RoundtripWithFallbackInput");
string memory fallbackInitial = svm.createString(31, "RoundtripWithFallbackFallbackInitial");
_assertRoundtripWithFallback(input, fallbackInitial);
}
function testRevertLong(string memory input) external {
vm.assume(!_isShort(input));
_assertRevertLong(input);
}
function testLengthShort(string memory input) external {
vm.assume(_isShort(input));
_assertLengthShort(input);
}
function symbolicLengthShort() external {
string memory input = svm.createString(31, "LengthShortInput");
_assertLengthShort(input);
}
function testLengthWithFallback(string memory input, string memory fallbackInitial) external {
_fallback = fallbackInitial;
_assertLengthWithFallback(input);
}
function symbolicLengthWithFallback() external {
uint256 length = 256;
string memory input = svm.createString(length, "LengthWithFallbackInput");
string memory fallbackInitial = svm.createString(length, "LengthWithFallbackFallbackInitial");
_fallback = fallbackInitial;
_assertLengthWithFallback(input);
}
/// Assertions
function _assertRoundtripShort(string memory input) internal {
ShortString short = ShortStrings.toShortString(input);
string memory output = ShortStrings.toString(short);
assertEq(input, output);
}
function testRoundtripWithFallback(string memory input, string memory fallbackInitial) external {
function _assertRoundtripWithFallback(string memory input, string memory fallbackInitial) internal {
_fallback = fallbackInitial; // Make sure that the initial value has no effect
ShortString short = ShortStrings.toShortStringWithFallback(input, _fallback);
string memory output = ShortStrings.toStringWithFallback(short, _fallback);
assertEq(input, output);
}
function testRevertLong(string memory input) external {
vm.assume(!_isShort(input));
function _assertRevertLong(string memory input) internal {
vm.expectRevert(abi.encodeWithSelector(ShortStrings.StringTooLong.selector, input));
this.toShortString(input);
}
function testLengthShort(string memory input) external {
vm.assume(_isShort(input));
uint256 inputLength = bytes(input).length;
function _assertLengthShort(string memory input) internal {
ShortString short = ShortStrings.toShortString(input);
uint256 shortLength = ShortStrings.byteLength(short);
uint256 inputLength = bytes(input).length;
assertEq(inputLength, shortLength);
}
function testLengthWithFallback(string memory input, string memory fallbackInitial) external {
_fallback = fallbackInitial;
function _assertLengthWithFallback(string memory input) internal {
uint256 inputLength = bytes(input).length;
ShortString short = ShortStrings.toShortStringWithFallback(input, _fallback);
uint256 shortLength = ShortStrings.byteLengthWithFallback(short, _fallback);
assertEq(inputLength, shortLength);
}
/// Helpers
function toShortString(string memory input) external pure returns (ShortString) {
return ShortStrings.toShortString(input);
}

@ -4,18 +4,27 @@
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {SymTest} from "halmos-cheatcodes/SymTest.sol";
import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol";
contract SlotDerivationTest is Test {
contract SlotDerivationTest is Test, SymTest {
using SlotDerivation for bytes32;
bytes[] private _array;
function symbolicDeriveArray(uint256 length, uint256 offset) public {
vm.assume(length > 0);
vm.assume(offset < length);
_assertDeriveArray(length, offset);
}
function testDeriveArray(uint256 length, uint256 offset) public {
length = bound(length, 1, type(uint256).max);
offset = bound(offset, 0, length - 1);
_assertDeriveArray(length, offset);
}
function _assertDeriveArray(uint256 length, uint256 offset) public {
bytes32 baseSlot;
assembly {
baseSlot := _array.slot
@ -33,7 +42,7 @@ contract SlotDerivationTest is Test {
mapping(address => bytes) private _addressMapping;
function testDeriveMappingAddress(address key) public {
function testSymbolicDeriveMappingAddress(address key) public {
bytes32 baseSlot;
assembly {
baseSlot := _addressMapping.slot
@ -50,7 +59,7 @@ contract SlotDerivationTest is Test {
mapping(bool => bytes) private _boolMapping;
function testDeriveMappingBoolean(bool key) public {
function testSymbolicDeriveMappingBoolean(bool key) public {
bytes32 baseSlot;
assembly {
baseSlot := _boolMapping.slot
@ -67,7 +76,7 @@ contract SlotDerivationTest is Test {
mapping(bytes32 => bytes) private _bytes32Mapping;
function testDeriveMappingBytes32(bytes32 key) public {
function testSymbolicDeriveMappingBytes32(bytes32 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _bytes32Mapping.slot
@ -84,7 +93,7 @@ contract SlotDerivationTest is Test {
mapping(bytes4 => bytes) private _bytes4Mapping;
function testDeriveMappingBytes4(bytes4 key) public {
function testSymbolicDeriveMappingBytes4(bytes4 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _bytes4Mapping.slot
@ -101,7 +110,7 @@ contract SlotDerivationTest is Test {
mapping(uint256 => bytes) private _uint256Mapping;
function testDeriveMappingUint256(uint256 key) public {
function testSymbolicDeriveMappingUint256(uint256 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _uint256Mapping.slot
@ -118,7 +127,7 @@ contract SlotDerivationTest is Test {
mapping(uint32 => bytes) private _uint32Mapping;
function testDeriveMappingUint32(uint32 key) public {
function testSymbolicDeriveMappingUint32(uint32 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _uint32Mapping.slot
@ -135,7 +144,7 @@ contract SlotDerivationTest is Test {
mapping(int256 => bytes) private _int256Mapping;
function testDeriveMappingInt256(int256 key) public {
function testSymbolicDeriveMappingInt256(int256 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _int256Mapping.slot
@ -152,7 +161,7 @@ contract SlotDerivationTest is Test {
mapping(int32 => bytes) private _int32Mapping;
function testDeriveMappingInt32(int32 key) public {
function testSymbolicDeriveMappingInt32(int32 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _int32Mapping.slot
@ -170,6 +179,14 @@ contract SlotDerivationTest is Test {
mapping(string => bytes) private _stringMapping;
function testDeriveMappingString(string memory key) public {
_assertDeriveMappingString(key);
}
function symbolicDeriveMappingString() public {
_assertDeriveMappingString(svm.createString(256, "DeriveMappingStringInput"));
}
function _assertDeriveMappingString(string memory key) internal {
bytes32 baseSlot;
assembly {
baseSlot := _stringMapping.slot
@ -187,6 +204,14 @@ contract SlotDerivationTest is Test {
mapping(bytes => bytes) private _bytesMapping;
function testDeriveMappingBytes(bytes memory key) public {
_assertDeriveMappingBytes(key);
}
function symbolicDeriveMappingBytes() public {
_assertDeriveMappingBytes(svm.createBytes(256, "DeriveMappingBytesInput"));
}
function _assertDeriveMappingBytes(bytes memory key) internal {
bytes32 baseSlot;
assembly {
baseSlot := _bytesMapping.slot

@ -108,15 +108,42 @@ describe('Strings', function () {
});
});
describe('toHexString address', function () {
it('converts a random address', async function () {
const addr = '0xa9036907dccae6a1e0033479b12e837e5cf5a02f';
expect(await this.mock.getFunction('$toHexString(address)')(addr)).to.equal(addr);
describe('addresses', function () {
const addresses = [
'0xa9036907dccae6a1e0033479b12e837e5cf5a02f', // Random address
'0x0000e0ca771e21bd00057f54a68c30d400000000', // Leading and trailing zeros
// EIP-55 reference
'0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed',
'0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359',
'0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB',
'0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb',
'0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359',
'0x52908400098527886E0F7030069857D2E4169EE7',
'0x8617E340B3D01FA5F11F306F4090FD50E238070D',
'0xde709f2102306220921060314715629080e2fb77',
'0x27b1fdb04752bbc536007a920d24acb045561c26',
'0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed',
'0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359',
'0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB',
'0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb',
];
describe('toHexString', function () {
for (const addr of addresses) {
it(`converts ${addr}`, async function () {
expect(await this.mock.getFunction('$toHexString(address)')(addr)).to.equal(addr.toLowerCase());
});
}
});
it('converts an address with leading zeros', async function () {
const addr = '0x0000e0ca771e21bd00057f54a68c30d400000000';
expect(await this.mock.getFunction('$toHexString(address)')(addr)).to.equal(addr);
describe('toChecksumHexString', function () {
for (const addr of addresses) {
it(`converts ${addr}`, async function () {
expect(await this.mock.getFunction('$toChecksumHexString(address)')(addr)).to.equal(
ethers.getAddress(addr.toLowerCase()),
);
});
}
});
});

@ -0,0 +1,135 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
contract P256Test is Test {
/// forge-config: default.fuzz.runs = 512
function testVerify(uint256 seed, bytes32 digest) public {
uint256 privateKey = bound(uint256(keccak256(abi.encode(seed))), 1, P256.N - 1);
(bytes32 x, bytes32 y) = P256PublicKey.getPublicKey(privateKey);
(bytes32 r, bytes32 s) = vm.signP256(privateKey, digest);
s = _ensureLowerS(s);
assertTrue(P256.verify(digest, r, s, x, y));
assertTrue(P256.verifySolidity(digest, r, s, x, y));
}
/// forge-config: default.fuzz.runs = 512
function testRecover(uint256 seed, bytes32 digest) public {
uint256 privateKey = bound(uint256(keccak256(abi.encode(seed))), 1, P256.N - 1);
(bytes32 x, bytes32 y) = P256PublicKey.getPublicKey(privateKey);
(bytes32 r, bytes32 s) = vm.signP256(privateKey, digest);
s = _ensureLowerS(s);
(bytes32 qx0, bytes32 qy0) = P256.recovery(digest, 0, r, s);
(bytes32 qx1, bytes32 qy1) = P256.recovery(digest, 1, r, s);
assertTrue((qx0 == x && qy0 == y) || (qx1 == x && qy1 == y));
}
function _ensureLowerS(bytes32 s) private pure returns (bytes32) {
uint256 _s = uint256(s);
unchecked {
return _s > P256.N / 2 ? bytes32(P256.N - _s) : s;
}
}
}
/**
* @dev Library to derive P256 public key from private key
* Should be removed if Foundry adds this functionality
* See https://github.com/foundry-rs/foundry/issues/7908
*/
library P256PublicKey {
function getPublicKey(uint256 privateKey) internal view returns (bytes32, bytes32) {
(uint256 x, uint256 y, uint256 z) = _jMult(P256.GX, P256.GY, 1, privateKey);
return _affineFromJacobian(x, y, z);
}
function _jMult(
uint256 x,
uint256 y,
uint256 z,
uint256 k
) private pure returns (uint256 rx, uint256 ry, uint256 rz) {
unchecked {
for (uint256 i = 0; i < 256; ++i) {
if (rz > 0) {
(rx, ry, rz) = _jDouble(rx, ry, rz);
}
if (k >> 255 > 0) {
if (rz == 0) {
(rx, ry, rz) = (x, y, z);
} else {
(rx, ry, rz) = _jAdd(rx, ry, rz, x, y, z);
}
}
k <<= 1;
}
}
}
/// From P256.sol
function _affineFromJacobian(uint256 jx, uint256 jy, uint256 jz) private view returns (bytes32 ax, bytes32 ay) {
if (jz == 0) return (0, 0);
uint256 zinv = Math.invModPrime(jz, P256.P);
uint256 zzinv = mulmod(zinv, zinv, P256.P);
uint256 zzzinv = mulmod(zzinv, zinv, P256.P);
ax = bytes32(mulmod(jx, zzinv, P256.P));
ay = bytes32(mulmod(jy, zzzinv, P256.P));
}
function _jDouble(uint256 x, uint256 y, uint256 z) private pure returns (uint256 rx, uint256 ry, uint256 rz) {
uint256 p = P256.P;
uint256 a = P256.A;
assembly ("memory-safe") {
let yy := mulmod(y, y, p)
let zz := mulmod(z, z, p)
let s := mulmod(4, mulmod(x, yy, p), p) // s = 4*x*y²
let m := addmod(mulmod(3, mulmod(x, x, p), p), mulmod(a, mulmod(zz, zz, p), p), p) // m = 3*x²+a*z
let t := addmod(mulmod(m, m, p), sub(p, mulmod(2, s, p)), p) // t = m²-2*s
// x' = t
rx := t
// y' = m*(s-t)-8*y⁴
ry := addmod(mulmod(m, addmod(s, sub(p, t), p), p), sub(p, mulmod(8, mulmod(yy, yy, p), p)), p)
// z' = 2*y*z
rz := mulmod(2, mulmod(y, z, p), p)
}
}
function _jAdd(
uint256 x1,
uint256 y1,
uint256 z1,
uint256 x2,
uint256 y2,
uint256 z2
) private pure returns (uint256 rx, uint256 ry, uint256 rz) {
uint256 p = P256.P;
assembly ("memory-safe") {
let zz1 := mulmod(z1, z1, p) // zz1 = z1²
let zz2 := mulmod(z2, z2, p) // zz2 = z2²
let u1 := mulmod(x1, zz2, p) // u1 = x1*z2²
let u2 := mulmod(x2, zz1, p) // u2 = x2*z1²
let s1 := mulmod(y1, mulmod(zz2, z2, p), p) // s1 = y1*z2³
let s2 := mulmod(y2, mulmod(zz1, z1, p), p) // s2 = y2*z1³
let h := addmod(u2, sub(p, u1), p) // h = u2-u1
let hh := mulmod(h, h, p) // h²
let hhh := mulmod(h, hh, p) // h³
let r := addmod(s2, sub(p, s1), p) // r = s2-s1
// x' = r²-h³-2*u1*h²
rx := addmod(addmod(mulmod(r, r, p), sub(p, hhh), p), sub(p, mulmod(2, mulmod(u1, hh, p), p)), p)
// y' = r*(u1*h²-x')-s1*h³
ry := addmod(mulmod(r, addmod(mulmod(u1, hh, p), sub(p, rx), p), p), sub(p, mulmod(s1, hhh, p)), p)
// z' = h*z1*z2
rz := mulmod(h, mulmod(z1, z2, p), p)
}
}
}

@ -0,0 +1,156 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { secp256r1 } = require('@noble/curves/p256');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const N = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
// As in ECDSA, signatures are malleable and the tooling produce both high and low S values.
// We need to ensure that the s value is in the lower half of the order of the curve.
const ensureLowerOrderS = ({ s, recovery, ...rest }) => {
if (s > N / 2n) {
s = N - s;
recovery = 1 - recovery;
}
return { s, recovery, ...rest };
};
const prepareSignature = (
privateKey = secp256r1.utils.randomPrivateKey(),
messageHash = ethers.hexlify(ethers.randomBytes(0x20)),
) => {
const publicKey = [
secp256r1.getPublicKey(privateKey, false).slice(0x01, 0x21),
secp256r1.getPublicKey(privateKey, false).slice(0x21, 0x41),
].map(ethers.hexlify);
const { r, s, recovery } = ensureLowerOrderS(secp256r1.sign(messageHash.replace(/0x/, ''), privateKey));
const signature = [r, s].map(v => ethers.toBeHex(v, 0x20));
return { privateKey, publicKey, signature, recovery, messageHash };
};
describe('P256', function () {
async function fixture() {
return { mock: await ethers.deployContract('$P256') };
}
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('with signature', function () {
beforeEach(async function () {
Object.assign(this, prepareSignature());
});
it('verify valid signature', async function () {
expect(await this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.be.true;
expect(await this.mock.$verifySolidity(this.messageHash, ...this.signature, ...this.publicKey)).to.be.true;
await expect(this.mock.$verifyNative(this.messageHash, ...this.signature, ...this.publicKey))
.to.be.revertedWithCustomError(this.mock, 'MissingPrecompile')
.withArgs('0x0000000000000000000000000000000000000100');
});
it('recover public key', async function () {
expect(await this.mock.$recovery(this.messageHash, this.recovery, ...this.signature)).to.deep.equal(
this.publicKey,
);
});
it('reject signature with flipped public key coordinates ([x,y] >> [y,x])', async function () {
// flip public key
this.publicKey.reverse();
expect(await this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.be.false;
expect(await this.mock.$verifySolidity(this.messageHash, ...this.signature, ...this.publicKey)).to.be.false;
expect(await this.mock.$verifyNative(this.messageHash, ...this.signature, ...this.publicKey)).to.be.false; // Flipped public key is not in the curve
});
it('reject signature with flipped signature values ([r,s] >> [s,r])', async function () {
// Preselected signature where `r < N/2` and `s < N/2`
this.signature = [
'0x45350225bad31e89db662fcc4fb2f79f349adbb952b3f652eed1f2aa72fb0356',
'0x513eb68424c42630012309eee4a3b43e0bdc019d179ef0e0c461800845e237ee',
];
// Corresponding hash and public key
this.messageHash = '0x2ad1f900fe63745deeaedfdf396cb6f0f991c4338a9edf114d52f7d1812040a0';
this.publicKey = [
'0x9e30de165e521257996425d9bf12a7d366925614bf204eabbb78172b48e52e59',
'0x94bf0fe72f99654d7beae4780a520848e306d46a1275b965c4f4c2b8e9a2c08d',
];
// Make sure it works
expect(await this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.be.true;
// Flip signature
this.signature.reverse();
expect(await this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.be.false;
expect(await this.mock.$verifySolidity(this.messageHash, ...this.signature, ...this.publicKey)).to.be.false;
await expect(this.mock.$verifyNative(this.messageHash, ...this.signature, ...this.publicKey))
.to.be.revertedWithCustomError(this.mock, 'MissingPrecompile')
.withArgs('0x0000000000000000000000000000000000000100');
expect(await this.mock.$recovery(this.messageHash, this.recovery, ...this.signature)).to.not.deep.equal(
this.publicKey,
);
});
it('reject signature with invalid message hash', async function () {
// random message hash
this.messageHash = ethers.hexlify(ethers.randomBytes(32));
expect(await this.mock.$verify(this.messageHash, ...this.signature, ...this.publicKey)).to.be.false;
expect(await this.mock.$verifySolidity(this.messageHash, ...this.signature, ...this.publicKey)).to.be.false;
await expect(this.mock.$verifyNative(this.messageHash, ...this.signature, ...this.publicKey))
.to.be.revertedWithCustomError(this.mock, 'MissingPrecompile')
.withArgs('0x0000000000000000000000000000000000000100');
expect(await this.mock.$recovery(this.messageHash, this.recovery, ...this.signature)).to.not.deep.equal(
this.publicKey,
);
});
it('fail to recover signature with invalid recovery bit', async function () {
// flip recovery bit
this.recovery = 1 - this.recovery;
expect(await this.mock.$recovery(this.messageHash, this.recovery, ...this.signature)).to.not.deep.equal(
this.publicKey,
);
});
});
// test cases for https://github.com/C2SP/wycheproof/blob/4672ff74d68766e7785c2cac4c597effccef2c5c/testvectors/ecdsa_secp256r1_sha256_p1363_test.json
describe('wycheproof tests', function () {
for (const { key, tests } of require('./ecdsa_secp256r1_sha256_p1363_test.json').testGroups) {
// parse public key
let [x, y] = [key.wx, key.wy].map(v => ethers.stripZerosLeft('0x' + v, 32));
if (x.length > 66 || y.length > 66) continue;
x = ethers.zeroPadValue(x, 32);
y = ethers.zeroPadValue(y, 32);
// run all tests for this key
for (const { tcId, comment, msg, sig, result } of tests) {
// only keep properly formatted signatures
if (sig.length != 128) continue;
it(`${tcId}: ${comment}`, async function () {
// split signature, and reduce modulo N
let [r, s] = Array(2)
.fill()
.map((_, i) => ethers.toBigInt('0x' + sig.substring(64 * i, 64 * (i + 1))));
// move s to lower part of the curve if needed
if (s <= N && s > N / 2n) s = N - s;
// prepare signature
r = ethers.toBeHex(r, 32);
s = ethers.toBeHex(s, 32);
// hash
const messageHash = ethers.sha256('0x' + msg);
// check verify
expect(await this.mock.$verify(messageHash, r, s, x, y)).to.equal(result == 'valid');
});
}
}
});
});

@ -0,0 +1,17 @@
const path = require('path');
const fs = require('fs');
module.exports = function* parse(file) {
const cache = {};
const data = fs.readFileSync(path.resolve(__dirname, file), 'utf8');
for (const line of data.split('\r\n')) {
const groups = line.match(/^(?<key>\w+) = (?<value>\w+)(?<extra>.*)$/)?.groups;
if (groups) {
const { key, value, extra } = groups;
cache[key] = value;
if (groups.key === 'Result') {
yield Object.assign({ extra: extra.trim() }, cache);
}
}
}
};

@ -0,0 +1,97 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const parse = require('./RSA.helper');
async function fixture() {
return { mock: await ethers.deployContract('$RSA') };
}
describe('RSA', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
// Load test cases from file SigVer15_186-3.rsp from:
// https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Algorithm-Validation-Program/documents/dss/186-2rsatestvectors.zip
describe('SigVer15_186-3.rsp tests', function () {
for (const test of parse('SigVer15_186-3.rsp')) {
const { length } = Buffer.from(test.S, 'hex');
/// For now, RSA only supports digest that are 32bytes long. If we ever extend that, we can use these hashing functions for @noble:
// const { sha1 } = require('@noble/hashes/sha1');
// const { sha224, sha256 } = require('@noble/hashes/sha256');
// const { sha384, sha512 } = require('@noble/hashes/sha512');
if (test.SHAAlg === 'SHA256') {
const result = test.Result === 'P';
it(`signature length ${length} ${test.extra} ${result ? 'works' : 'fails'}`, async function () {
const data = '0x' + test.Msg;
const sig = '0x' + test.S;
const exp = '0x' + test.e;
const mod = '0x' + test.n;
expect(await this.mock.$pkcs1(ethers.sha256(data), sig, exp, mod)).to.equal(result);
expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.equal(result);
});
}
}
});
describe('others tests', function () {
it('openssl', async function () {
const data = ethers.toUtf8Bytes('hello world');
const sig =
'0x079bed733b48d69bdb03076cb17d9809072a5a765460bc72072d687dba492afe951d75b814f561f253ee5cc0f3d703b6eab5b5df635b03a5437c0a5c179309812f5b5c97650361c645bc99f806054de21eb187bc0a704ed38d3d4c2871a117c19b6da7e9a3d808481c46b22652d15b899ad3792da5419e50ee38759560002388';
const exp =
'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001';
const mod =
'0xdf3edde009b96bc5b03b48bd73fe70a3ad20eaf624d0dc1ba121a45cc739893741b7cf82acf1c91573ec8266538997c6699760148de57e54983191eca0176f518e547b85fe0bb7d9e150df19eee734cf5338219c7f8f7b13b39f5384179f62c135e544cb70be7505751f34568e06981095aeec4f3a887639718a3e11d48c240d';
expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.be.true;
});
// According to RFC4055, pg.5 and RFC8017, pg. 64, for SHA-1, and the SHA-2 family,
// the algorithm parameter has to be NULL and both explicit NULL parameter and implicit
// NULL parameter (ie, absent NULL parameter) are considered to be legal and equivalent.
it('rfc8017 implicit null parameter', async function () {
const data = ethers.toUtf8Bytes('hello world!');
const sig =
'0xa0073057133ff3758e7e111b4d7441f1d8cbe4b2dd5ee4316a14264290dee5ed7f175716639bd9bb43a14e4f9fcb9e84dedd35e2205caac04828b2c053f68176d971ea88534dd2eeec903043c3469fc69c206b2a8694fd262488441ed8852280c3d4994e9d42bd1d575c7024095f1a20665925c2175e089c0d731471f6cc145404edf5559fd2276e45e448086f71c78d0cc6628fad394a34e51e8c10bc39bfe09ed2f5f742cc68bee899d0a41e4c75b7b80afd1c321d89ccd9fe8197c44624d91cc935dfa48de3c201099b5b417be748aef29248527e8bbb173cab76b48478d4177b338fe1f1244e64d7d23f07add560d5ad50b68d6649a49d7bc3db686daaa7';
const exp = '0x03';
const mod =
'0xe932ac92252f585b3a80a4dd76a897c8b7652952fe788f6ec8dd640587a1ee5647670a8ad4c2be0f9fa6e49c605adf77b5174230af7bd50e5d6d6d6d28ccf0a886a514cc72e51d209cc772a52ef419f6a953f3135929588ebe9b351fca61ced78f346fe00dbb6306e5c2a4c6dfc3779af85ab417371cf34d8387b9b30ae46d7a5ff5a655b8d8455f1b94ae736989d60a6f2fd5cadbffbd504c5a756a2e6bb5cecc13bca7503f6df8b52ace5c410997e98809db4dc30d943de4e812a47553dce54844a78e36401d13f77dc650619fed88d8b3926e3d8e319c80c744779ac5d6abe252896950917476ece5e8fc27d5f053d6018d91b502c4787558a002b9283da7';
expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.be.true;
});
it('returns false for a very short n', async function () {
const data = ethers.toUtf8Bytes('hello world!');
const sig = '0x0102';
const exp = '0x03';
const mod = '0x0405';
expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.be.false;
});
it('returns false for a signature with different length to n', async function () {
const data = ethers.toUtf8Bytes('hello world!');
const sig = '0x00112233';
const exp = '0x03';
const mod =
'0xe932ac92252f585b3a80a4dd76a897c8b7652952fe788f6ec8dd640587a1ee5647670a8ad4c2be0f9fa6e49c605adf77b5174230af7bd50e5d6d6d6d28ccf0a886a514cc72e51d209cc772a52ef419f6a953f3135929588ebe9b351fca61ced78f346fe00dbb6306e5c2a4c6dfc3779af85ab417371cf34d8387b9b30ae46d7a5ff5a655b8d8455f1b94ae736989d60a6f2fd5cadbffbd504c5a756a2e6bb5cecc13bca7503f6df8b52ace5c410997e98809db4dc30d943de4e812a47553dce54844a78e36401d13f77dc650619fed88d8b3926e3d8e319c80c744779ac5d6abe252896950917476ece5e8fc27d5f053d6018d91b502c4787558a002b9283da7';
expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.be.false;
});
it('returns false if s >= n', async function () {
// this is the openssl example where sig has been replaced by sig + mod
const data = ethers.toUtf8Bytes('hello world');
const sig =
'0xe6dacb53450242618b3e502a257c08acb44b456c7931988da84f0cda8182b435d6d5453ac1e72b07c7dadf2747609b7d544d15f3f14081f9dbad9c48b7aa78d2bdafd81d630f19a0270d7911f4ec82b171e9a95889ffc9e740dc9fac89407a82d152ecb514967d4d9165e67ce0d7f39a3082657cdfca148a5fc2b3a7348c4795';
const exp =
'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001';
const mod =
'0xdf3edde009b96bc5b03b48bd73fe70a3ad20eaf624d0dc1ba121a45cc739893741b7cf82acf1c91573ec8266538997c6699760148de57e54983191eca0176f518e547b85fe0bb7d9e150df19eee734cf5338219c7f8f7b13b39f5384179f62c135e544cb70be7505751f34568e06981095aeec4f3a887639718a3e11d48c240d';
expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.be.false;
});
});
});

File diff suppressed because it is too large Load Diff

@ -7,12 +7,12 @@ import {Test, stdError} from "forge-std/Test.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
contract MathTest is Test {
function testTernary(bool f, uint256 a, uint256 b) public {
function testSymbolicTernary(bool f, uint256 a, uint256 b) public {
assertEq(Math.ternary(f, a, b), f ? a : b);
}
// MIN & MAX
function testMinMax(uint256 a, uint256 b) public {
function testSymbolicMinMax(uint256 a, uint256 b) public {
assertEq(Math.min(a, b), a < b ? a : b);
assertEq(Math.max(a, b), a > b ? a : b);
}

@ -8,18 +8,18 @@ import {Math} from "../../../contracts/utils/math/Math.sol";
import {SignedMath} from "../../../contracts/utils/math/SignedMath.sol";
contract SignedMathTest is Test {
function testTernary(bool f, int256 a, int256 b) public {
function testSymbolicTernary(bool f, int256 a, int256 b) public {
assertEq(SignedMath.ternary(f, a, b), f ? a : b);
}
// MIN & MAX
function testMinMax(int256 a, int256 b) public {
function testSymbolicMinMax(int256 a, int256 b) public {
assertEq(SignedMath.min(a, b), a < b ? a : b);
assertEq(SignedMath.max(a, b), a > b ? a : b);
}
// MIN
function testMin(int256 a, int256 b) public {
function testSymbolicMin(int256 a, int256 b) public {
int256 result = SignedMath.min(a, b);
assertLe(result, a);
@ -28,7 +28,7 @@ contract SignedMathTest is Test {
}
// MAX
function testMax(int256 a, int256 b) public {
function testSymbolicMax(int256 a, int256 b) public {
int256 result = SignedMath.max(a, b);
assertGe(result, a);
@ -69,7 +69,7 @@ contract SignedMathTest is Test {
}
// ABS
function testAbs(int256 a) public {
function testSymbolicAbs(int256 a) public {
uint256 result = SignedMath.abs(a);
unchecked {

Loading…
Cancel
Save