Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
---

`NoncesSemiAbstracted`: Add a variant of `Nonces` that implements ERC-4337 semi-abstracted nonce system.
69 changes: 69 additions & 0 deletions contracts/utils/NoncesSemiAbstracted.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 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 NoncesSemiAbstracted is Nonces {
mapping(address owner => mapping(uint192 key => uint64)) private _nonce;

/**
* @dev Returns the next unused nonce for an address.
*/
function nonces(address owner) public view virtual override returns (uint256) {
return nonces(owner, 0);
}

/**
* @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 (uint256(key) << 64) | _nonce[owner][key];
}

/**
* @dev Consumes a nonce from the default key.
*
* Returns the current value and increments nonce.
*/
function _useNonce(address owner) internal virtual override returns (uint256) {
return _useNonce(owner, 0);
}

/**
* @dev Consumes a nonce from the given key.
*
* Returns the current value and increments nonce.
*/
function _useNonce(address owner, uint192 key) internal virtual returns (uint256) {
// TODO: use unchecked here? Do we expect 2**64 nonce ever be used for a single owner?
return _nonce[owner][key]++;
}

/**
* @dev Same as {_useNonce} but checking that `nonce` is the next valid for `owner`.
*
* This version takes a 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 a the key and the nonce as two different parameters.
*/
function _useCheckedNonce(address owner, uint192 key, uint64 nonce) internal virtual {
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.
* {NoncesSemiAbstracted}: 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}}

{{NoncesSemiAbstracted}}

== 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
158 changes: 158 additions & 0 deletions test/utils/Nonces.behavior.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
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(await this.mock.nonces(sender)).to.equal(0n);
});

describe('_useNonce', function () {
it('increments a nonce', async function () {
expect(await this.mock.nonces(sender)).to.equal(0n);

const eventName = ['return$_useNonce', 'return$_useNonce_address'].find(name =>
this.mock.interface.getEvent(name),
);

await expect(await this.mock.$_useNonce(sender))
.to.emit(this.mock, eventName)
.withArgs(0n);

expect(await this.mock.nonces(sender)).to.equal(1n);
});

it("increments only sender's nonce", async function () {
expect(await this.mock.nonces(sender)).to.equal(0n);
expect(await this.mock.nonces(other)).to.equal(0n);

await this.mock.$_useNonce(sender);

expect(await this.mock.nonces(sender)).to.equal(1n);
expect(await this.mock.nonces(other)).to.equal(0n);
});
});

describe('_useCheckedNonce', function () {
it('increments a nonce', async function () {
const currentNonce = await this.mock.nonces(sender);

expect(currentNonce).to.equal(0n);

await this.mock.$_useCheckedNonce(sender, currentNonce);

expect(await this.mock.nonces(sender)).to.equal(1n);
});

it("increments only sender's nonce", async function () {
const currentNonce = await this.mock.nonces(sender);

expect(currentNonce).to.equal(0n);
expect(await this.mock.nonces(other)).to.equal(0n);

await this.mock.$_useCheckedNonce(sender, currentNonce);

expect(await this.mock.nonces(sender)).to.equal(1n);
expect(await this.mock.nonces(other)).to.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 shouldBehaveLikeNoncesSemiAbstracted() {
describe("should implement ERC-4337's semi-abstracted nonces", function () {
const sender = ethers.Wallet.createRandom();

const keyOffset = key => key << 64n;

it('gets a nonce', async function () {
expect(await this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.equal(keyOffset(0n) + 0n);
expect(await this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.equal(keyOffset(17n) + 0n);
});

describe('_useNonce', function () {
it('default variant uses key 0', async function () {
expect(await this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.equal(keyOffset(0n) + 0n);
expect(await this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.equal(keyOffset(17n) + 0n);

await expect(await this.mock.$_useNonce(sender))
.to.emit(this.mock, 'return$_useNonce_address')
.withArgs(0n);

await expect(await this.mock.$_useNonce(sender, ethers.Typed.uint192(0n)))
.to.emit(this.mock, 'return$_useNonce_address_uint192')
.withArgs(1n);

expect(await this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.equal(keyOffset(0n) + 2n);
expect(await this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.equal(keyOffset(17n) + 0n);
});

it('use nonce at another key', async function () {
expect(await this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.equal(keyOffset(0n) + 0n);
expect(await this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.equal(keyOffset(17n) + 0n);

await expect(await this.mock.$_useNonce(sender, ethers.Typed.uint192(17n)))
.to.emit(this.mock, 'return$_useNonce_address_uint192')
.withArgs(0n);

await expect(await this.mock.$_useNonce(sender, ethers.Typed.uint192(17n)))
.to.emit(this.mock, 'return$_useNonce_address_uint192')
.withArgs(1n);

expect(await this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.equal(keyOffset(0n) + 0n);
expect(await this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.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(await this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.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(await this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.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,
shouldBehaveLikeNoncesSemiAbstracted,
};
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/NoncesSemiAbstracted.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, shouldBehaveLikeNoncesSemiAbstracted } = require('./Nonces.behavior');

async function fixture() {
const mock = await ethers.deployContract('$NoncesSemiAbstracted');
return { mock };
}

describe('NoncesSemiAbstracted', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});

shouldBehaveLikeNonces();
shouldBehaveLikeNoncesSemiAbstracted();
});