Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NoncesKeyed variant #5272

Merged
merged 10 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lovely-dodos-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`NoncesKeyed`: Add a variant of `Nonces` that implements the ERC-4337 entrypoint nonce system.
2 changes: 2 additions & 0 deletions contracts/mocks/Stateless.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
60 changes: 60 additions & 0 deletions contracts/utils/NoncesKeyed.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
3 changes: 3 additions & 0 deletions contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down Expand Up @@ -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_.
Expand Down
152 changes: 152 additions & 0 deletions test/utils/Nonces.behavior.js
Original file line number Diff line number Diff line change
@@ -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,
};
65 changes: 3 additions & 62 deletions test/utils/Nonces.test.js
Original file line number Diff line number Diff line change
@@ -1,75 +1,16 @@
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 () {
beforeEach(async 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();
});
17 changes: 17 additions & 0 deletions test/utils/NoncesKeyed.test.js
Original file line number Diff line number Diff line change
@@ -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();
});