Add NoncesKeyed variant (#5272)

pull/5101/head^2
Hadrien Croubois 4 months ago committed by GitHub
parent 205f59e9b6
commit 2fa4d103fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/lovely-dodos-lay.md
  2. 2
      contracts/mocks/Stateless.sol
  3. 60
      contracts/utils/NoncesKeyed.sol
  4. 3
      contracts/utils/README.adoc
  5. 152
      test/utils/Nonces.behavior.js
  6. 65
      test/utils/Nonces.test.js
  7. 17
      test/utils/NoncesKeyed.test.js

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---
`NoncesKeyed`: Add a variant of `Nonces` that implements the ERC-4337 entrypoint nonce system.

@ -29,6 +29,8 @@ import {Heap} from "../utils/structs/Heap.sol";
import {Math} from "../utils/math/Math.sol";
import {MerkleProof} from "../utils/cryptography/MerkleProof.sol";
import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol";
import {Nonces} from "../utils/Nonces.sol";
import {NoncesKeyed} from "../utils/NoncesKeyed.sol";
import {P256} from "../utils/cryptography/P256.sol";
import {Panic} from "../utils/Panic.sol";
import {Packing} from "../utils/Packing.sol";

@ -0,0 +1,60 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Nonces} from "./Nonces.sol";
/**
* @dev Alternative to {Nonces}, that support key-ed nonces.
*
* Follows the https://eips.ethereum.org/EIPS/eip-4337#semi-abstracted-nonce-support[ERC-4337's semi-abstracted nonce system].
*/
abstract contract NoncesKeyed is Nonces {
mapping(address owner => mapping(uint192 key => uint64)) private _nonces;
/// @dev Returns the next unused nonce for an address and key. Result contains the key prefix.
function nonces(address owner, uint192 key) public view virtual returns (uint256) {
return key == 0 ? nonces(owner) : ((uint256(key) << 64) | _nonces[owner][key]);
}
/**
* @dev Consumes the next unused nonce for an address and key.
*
* Returns the current value without the key prefix. Consumed nonce is increased, so calling this functions twice
* with the same arguments will return different (sequential) results.
*/
function _useNonce(address owner, uint192 key) internal virtual returns (uint256) {
// For each account, the nonce has an initial value of 0, can only be incremented by one, and cannot be
// decremented or reset. This guarantees that the nonce never overflows.
unchecked {
// It is important to do x++ and not ++x here.
return key == 0 ? _useNonce(owner) : _nonces[owner][key]++;
}
}
/**
* @dev Same as {_useNonce} but checking that `nonce` is the next valid for `owner`.
*
* This version takes the key and the nonce in a single uint256 parameter:
* - use the first 8 bytes for the key
* - use the last 24 bytes for the nonce
*/
function _useCheckedNonce(address owner, uint256 keyNonce) internal virtual override {
_useCheckedNonce(owner, uint192(keyNonce >> 64), uint64(keyNonce));
}
/**
* @dev Same as {_useNonce} but checking that `nonce` is the next valid for `owner`.
*
* This version takes the key and the nonce as two different parameters.
*/
function _useCheckedNonce(address owner, uint192 key, uint64 nonce) internal virtual {
if (key == 0) {
super._useCheckedNonce(owner, nonce);
} else {
uint256 current = _useNonce(owner, key);
if (nonce != current) {
revert InvalidAccountNonce(owner, current);
}
}
}
}

@ -18,6 +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.
* {NoncesKeyed}: Alternative to {Nonces}, that support key-ed nonces following https://eips.ethereum.org/EIPS/eip-4337#semi-abstracted-nonce-support[ERC-4337 speciciations].
* {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`).
@ -85,6 +86,8 @@ Because Solidity does not support generic types, {EnumerableMap} and {Enumerable
{{Nonces}}
{{NoncesKeyed}}
== Introspection
This set of interfaces and contracts deal with https://en.wikipedia.org/wiki/Type_introspection[type introspection] of contracts, that is, examining which functions can be called on them. This is usually referred to as a contract's _interface_.

@ -0,0 +1,152 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
function shouldBehaveLikeNonces() {
describe('should behave like Nonces', function () {
const sender = ethers.Wallet.createRandom();
const other = ethers.Wallet.createRandom();
it('gets a nonce', async function () {
expect(this.mock.nonces(sender)).to.eventually.equal(0n);
});
describe('_useNonce', function () {
it('increments a nonce', async function () {
expect(this.mock.nonces(sender)).to.eventually.equal(0n);
const eventName = ['return$_useNonce', 'return$_useNonce_address'].find(name =>
this.mock.interface.getEvent(name),
);
await expect(this.mock.$_useNonce(sender)).to.emit(this.mock, eventName).withArgs(0n);
expect(this.mock.nonces(sender)).to.eventually.equal(1n);
});
it("increments only sender's nonce", async function () {
expect(this.mock.nonces(sender)).to.eventually.equal(0n);
expect(this.mock.nonces(other)).to.eventually.equal(0n);
await this.mock.$_useNonce(sender);
expect(this.mock.nonces(sender)).to.eventually.equal(1n);
expect(this.mock.nonces(other)).to.eventually.equal(0n);
});
});
describe('_useCheckedNonce', function () {
it('increments a nonce', async function () {
// current nonce is 0n
expect(this.mock.nonces(sender)).to.eventually.equal(0n);
await this.mock.$_useCheckedNonce(sender, 0n);
expect(this.mock.nonces(sender)).to.eventually.equal(1n);
});
it("increments only sender's nonce", async function () {
// current nonce is 0n
expect(this.mock.nonces(sender)).to.eventually.equal(0n);
expect(this.mock.nonces(other)).to.eventually.equal(0n);
await this.mock.$_useCheckedNonce(sender, 0n);
expect(this.mock.nonces(sender)).to.eventually.equal(1n);
expect(this.mock.nonces(other)).to.eventually.equal(0n);
});
it('reverts when nonce is not the expected', async function () {
const currentNonce = await this.mock.nonces(sender);
await expect(this.mock.$_useCheckedNonce(sender, currentNonce + 1n))
.to.be.revertedWithCustomError(this.mock, 'InvalidAccountNonce')
.withArgs(sender, currentNonce);
});
});
});
}
function shouldBehaveLikeNoncesKeyed() {
describe('should support nonces with keys', function () {
const sender = ethers.Wallet.createRandom();
const keyOffset = key => key << 64n;
it('gets a nonce', async function () {
expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(keyOffset(0n) + 0n);
expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(keyOffset(17n) + 0n);
});
describe('_useNonce', function () {
it('default variant uses key 0', async function () {
expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(keyOffset(0n) + 0n);
expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(keyOffset(17n) + 0n);
await expect(this.mock.$_useNonce(sender)).to.emit(this.mock, 'return$_useNonce_address').withArgs(0n);
await expect(this.mock.$_useNonce(sender, ethers.Typed.uint192(0n)))
.to.emit(this.mock, 'return$_useNonce_address_uint192')
.withArgs(1n);
expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(keyOffset(0n) + 2n);
expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(keyOffset(17n) + 0n);
});
it('use nonce at another key', async function () {
expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(keyOffset(0n) + 0n);
expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(keyOffset(17n) + 0n);
await expect(this.mock.$_useNonce(sender, ethers.Typed.uint192(17n)))
.to.emit(this.mock, 'return$_useNonce_address_uint192')
.withArgs(0n);
await expect(this.mock.$_useNonce(sender, ethers.Typed.uint192(17n)))
.to.emit(this.mock, 'return$_useNonce_address_uint192')
.withArgs(1n);
expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(keyOffset(0n) + 0n);
expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(keyOffset(17n) + 2n);
});
});
describe('_useCheckedNonce', function () {
it('default variant uses key 0', async function () {
const currentNonce = await this.mock.nonces(sender, ethers.Typed.uint192(0n));
await this.mock.$_useCheckedNonce(sender, currentNonce);
expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(currentNonce + 1n);
});
it('use nonce at another key', async function () {
const currentNonce = await this.mock.nonces(sender, ethers.Typed.uint192(17n));
await this.mock.$_useCheckedNonce(sender, currentNonce);
expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(currentNonce + 1n);
});
it('reverts when nonce is not the expected', async function () {
const currentNonce = await this.mock.nonces(sender, ethers.Typed.uint192(42n));
// use and increment
await this.mock.$_useCheckedNonce(sender, currentNonce);
// reuse same nonce
await expect(this.mock.$_useCheckedNonce(sender, currentNonce))
.to.be.revertedWithCustomError(this.mock, 'InvalidAccountNonce')
.withArgs(sender, 1);
// use "future" nonce too early
await expect(this.mock.$_useCheckedNonce(sender, currentNonce + 10n))
.to.be.revertedWithCustomError(this.mock, 'InvalidAccountNonce')
.withArgs(sender, 1);
});
});
});
}
module.exports = {
shouldBehaveLikeNonces,
shouldBehaveLikeNoncesKeyed,
};

@ -1,13 +1,10 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { shouldBehaveLikeNonces } = require('./Nonces.behavior');
async function fixture() {
const [sender, other] = await ethers.getSigners();
const mock = await ethers.deployContract('$Nonces');
return { sender, other, mock };
return { mock };
}
describe('Nonces', function () {
@ -15,61 +12,5 @@ describe('Nonces', function () {
Object.assign(this, await loadFixture(fixture));
});
it('gets a nonce', async function () {
expect(await this.mock.nonces(this.sender)).to.equal(0n);
});
describe('_useNonce', function () {
it('increments a nonce', async function () {
expect(await this.mock.nonces(this.sender)).to.equal(0n);
await expect(await this.mock.$_useNonce(this.sender))
.to.emit(this.mock, 'return$_useNonce')
.withArgs(0n);
expect(await this.mock.nonces(this.sender)).to.equal(1n);
});
it("increments only sender's nonce", async function () {
expect(await this.mock.nonces(this.sender)).to.equal(0n);
expect(await this.mock.nonces(this.other)).to.equal(0n);
await this.mock.$_useNonce(this.sender);
expect(await this.mock.nonces(this.sender)).to.equal(1n);
expect(await this.mock.nonces(this.other)).to.equal(0n);
});
});
describe('_useCheckedNonce', function () {
it('increments a nonce', async function () {
const currentNonce = await this.mock.nonces(this.sender);
expect(currentNonce).to.equal(0n);
await this.mock.$_useCheckedNonce(this.sender, currentNonce);
expect(await this.mock.nonces(this.sender)).to.equal(1n);
});
it("increments only sender's nonce", async function () {
const currentNonce = await this.mock.nonces(this.sender);
expect(currentNonce).to.equal(0n);
expect(await this.mock.nonces(this.other)).to.equal(0n);
await this.mock.$_useCheckedNonce(this.sender, currentNonce);
expect(await this.mock.nonces(this.sender)).to.equal(1n);
expect(await this.mock.nonces(this.other)).to.equal(0n);
});
it('reverts when nonce is not the expected', async function () {
const currentNonce = await this.mock.nonces(this.sender);
await expect(this.mock.$_useCheckedNonce(this.sender, currentNonce + 1n))
.to.be.revertedWithCustomError(this.mock, 'InvalidAccountNonce')
.withArgs(this.sender, currentNonce);
});
});
shouldBehaveLikeNonces();
});

@ -0,0 +1,17 @@
const { ethers } = require('hardhat');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { shouldBehaveLikeNonces, shouldBehaveLikeNoncesKeyed } = require('./Nonces.behavior');
async function fixture() {
const mock = await ethers.deployContract('$NoncesKeyed');
return { mock };
}
describe('NoncesKeyed', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
shouldBehaveLikeNonces();
shouldBehaveLikeNoncesKeyed();
});
Loading…
Cancel
Save