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 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();
});