Skip to content

Commit 2fa4d10

Browse files
authored
Add NoncesKeyed variant (#5272)
1 parent 205f59e commit 2fa4d10

File tree

7 files changed

+242
-62
lines changed

7 files changed

+242
-62
lines changed

Diff for: .changeset/lovely-dodos-lay.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`NoncesKeyed`: Add a variant of `Nonces` that implements the ERC-4337 entrypoint nonce system.

Diff for: contracts/mocks/Stateless.sol

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import {Heap} from "../utils/structs/Heap.sol";
2929
import {Math} from "../utils/math/Math.sol";
3030
import {MerkleProof} from "../utils/cryptography/MerkleProof.sol";
3131
import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol";
32+
import {Nonces} from "../utils/Nonces.sol";
33+
import {NoncesKeyed} from "../utils/NoncesKeyed.sol";
3234
import {P256} from "../utils/cryptography/P256.sol";
3335
import {Panic} from "../utils/Panic.sol";
3436
import {Packing} from "../utils/Packing.sol";

Diff for: contracts/utils/NoncesKeyed.sol

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {Nonces} from "./Nonces.sol";
5+
6+
/**
7+
* @dev Alternative to {Nonces}, that support key-ed nonces.
8+
*
9+
* Follows the https://eips.ethereum.org/EIPS/eip-4337#semi-abstracted-nonce-support[ERC-4337's semi-abstracted nonce system].
10+
*/
11+
abstract contract NoncesKeyed is Nonces {
12+
mapping(address owner => mapping(uint192 key => uint64)) private _nonces;
13+
14+
/// @dev Returns the next unused nonce for an address and key. Result contains the key prefix.
15+
function nonces(address owner, uint192 key) public view virtual returns (uint256) {
16+
return key == 0 ? nonces(owner) : ((uint256(key) << 64) | _nonces[owner][key]);
17+
}
18+
19+
/**
20+
* @dev Consumes the next unused nonce for an address and key.
21+
*
22+
* Returns the current value without the key prefix. Consumed nonce is increased, so calling this functions twice
23+
* with the same arguments will return different (sequential) results.
24+
*/
25+
function _useNonce(address owner, uint192 key) internal virtual returns (uint256) {
26+
// For each account, the nonce has an initial value of 0, can only be incremented by one, and cannot be
27+
// decremented or reset. This guarantees that the nonce never overflows.
28+
unchecked {
29+
// It is important to do x++ and not ++x here.
30+
return key == 0 ? _useNonce(owner) : _nonces[owner][key]++;
31+
}
32+
}
33+
34+
/**
35+
* @dev Same as {_useNonce} but checking that `nonce` is the next valid for `owner`.
36+
*
37+
* This version takes the key and the nonce in a single uint256 parameter:
38+
* - use the first 8 bytes for the key
39+
* - use the last 24 bytes for the nonce
40+
*/
41+
function _useCheckedNonce(address owner, uint256 keyNonce) internal virtual override {
42+
_useCheckedNonce(owner, uint192(keyNonce >> 64), uint64(keyNonce));
43+
}
44+
45+
/**
46+
* @dev Same as {_useNonce} but checking that `nonce` is the next valid for `owner`.
47+
*
48+
* This version takes the key and the nonce as two different parameters.
49+
*/
50+
function _useCheckedNonce(address owner, uint192 key, uint64 nonce) internal virtual {
51+
if (key == 0) {
52+
super._useCheckedNonce(owner, nonce);
53+
} else {
54+
uint256 current = _useNonce(owner, key);
55+
if (nonce != current) {
56+
revert InvalidAccountNonce(owner, current);
57+
}
58+
}
59+
}
60+
}

Diff for: contracts/utils/README.adoc

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
1818
* {ReentrancyGuardTransient}: Variant of {ReentrancyGuard} that uses transient storage (https://eips.ethereum.org/EIPS/eip-1153[EIP-1153]).
1919
* {Pausable}: A common emergency response mechanism that can pause functionality while a remediation is pending.
2020
* {Nonces}: Utility for tracking and verifying address nonces that only increment.
21+
* {NoncesKeyed}: Alternative to {Nonces}, that support key-ed nonces following https://eips.ethereum.org/EIPS/eip-4337#semi-abstracted-nonce-support[ERC-4337 speciciations].
2122
* {ERC165}, {ERC165Checker}: Utilities for inspecting interfaces supported by contracts.
2223
* {BitMaps}: A simple library to manage boolean value mapped to a numerical index in an efficient way.
2324
* {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
8586

8687
{{Nonces}}
8788

89+
{{NoncesKeyed}}
90+
8891
== Introspection
8992

9093
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_.

Diff for: test/utils/Nonces.behavior.js

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
const { ethers } = require('hardhat');
2+
const { expect } = require('chai');
3+
4+
function shouldBehaveLikeNonces() {
5+
describe('should behave like Nonces', function () {
6+
const sender = ethers.Wallet.createRandom();
7+
const other = ethers.Wallet.createRandom();
8+
9+
it('gets a nonce', async function () {
10+
expect(this.mock.nonces(sender)).to.eventually.equal(0n);
11+
});
12+
13+
describe('_useNonce', function () {
14+
it('increments a nonce', async function () {
15+
expect(this.mock.nonces(sender)).to.eventually.equal(0n);
16+
17+
const eventName = ['return$_useNonce', 'return$_useNonce_address'].find(name =>
18+
this.mock.interface.getEvent(name),
19+
);
20+
21+
await expect(this.mock.$_useNonce(sender)).to.emit(this.mock, eventName).withArgs(0n);
22+
23+
expect(this.mock.nonces(sender)).to.eventually.equal(1n);
24+
});
25+
26+
it("increments only sender's nonce", async function () {
27+
expect(this.mock.nonces(sender)).to.eventually.equal(0n);
28+
expect(this.mock.nonces(other)).to.eventually.equal(0n);
29+
30+
await this.mock.$_useNonce(sender);
31+
32+
expect(this.mock.nonces(sender)).to.eventually.equal(1n);
33+
expect(this.mock.nonces(other)).to.eventually.equal(0n);
34+
});
35+
});
36+
37+
describe('_useCheckedNonce', function () {
38+
it('increments a nonce', async function () {
39+
// current nonce is 0n
40+
expect(this.mock.nonces(sender)).to.eventually.equal(0n);
41+
42+
await this.mock.$_useCheckedNonce(sender, 0n);
43+
44+
expect(this.mock.nonces(sender)).to.eventually.equal(1n);
45+
});
46+
47+
it("increments only sender's nonce", async function () {
48+
// current nonce is 0n
49+
expect(this.mock.nonces(sender)).to.eventually.equal(0n);
50+
expect(this.mock.nonces(other)).to.eventually.equal(0n);
51+
52+
await this.mock.$_useCheckedNonce(sender, 0n);
53+
54+
expect(this.mock.nonces(sender)).to.eventually.equal(1n);
55+
expect(this.mock.nonces(other)).to.eventually.equal(0n);
56+
});
57+
58+
it('reverts when nonce is not the expected', async function () {
59+
const currentNonce = await this.mock.nonces(sender);
60+
61+
await expect(this.mock.$_useCheckedNonce(sender, currentNonce + 1n))
62+
.to.be.revertedWithCustomError(this.mock, 'InvalidAccountNonce')
63+
.withArgs(sender, currentNonce);
64+
});
65+
});
66+
});
67+
}
68+
69+
function shouldBehaveLikeNoncesKeyed() {
70+
describe('should support nonces with keys', function () {
71+
const sender = ethers.Wallet.createRandom();
72+
73+
const keyOffset = key => key << 64n;
74+
75+
it('gets a nonce', async function () {
76+
expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(keyOffset(0n) + 0n);
77+
expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(keyOffset(17n) + 0n);
78+
});
79+
80+
describe('_useNonce', function () {
81+
it('default variant uses key 0', async function () {
82+
expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(keyOffset(0n) + 0n);
83+
expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(keyOffset(17n) + 0n);
84+
85+
await expect(this.mock.$_useNonce(sender)).to.emit(this.mock, 'return$_useNonce_address').withArgs(0n);
86+
87+
await expect(this.mock.$_useNonce(sender, ethers.Typed.uint192(0n)))
88+
.to.emit(this.mock, 'return$_useNonce_address_uint192')
89+
.withArgs(1n);
90+
91+
expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(keyOffset(0n) + 2n);
92+
expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(keyOffset(17n) + 0n);
93+
});
94+
95+
it('use nonce at another key', async function () {
96+
expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(keyOffset(0n) + 0n);
97+
expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(keyOffset(17n) + 0n);
98+
99+
await expect(this.mock.$_useNonce(sender, ethers.Typed.uint192(17n)))
100+
.to.emit(this.mock, 'return$_useNonce_address_uint192')
101+
.withArgs(0n);
102+
103+
await expect(this.mock.$_useNonce(sender, ethers.Typed.uint192(17n)))
104+
.to.emit(this.mock, 'return$_useNonce_address_uint192')
105+
.withArgs(1n);
106+
107+
expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(keyOffset(0n) + 0n);
108+
expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(keyOffset(17n) + 2n);
109+
});
110+
});
111+
112+
describe('_useCheckedNonce', function () {
113+
it('default variant uses key 0', async function () {
114+
const currentNonce = await this.mock.nonces(sender, ethers.Typed.uint192(0n));
115+
116+
await this.mock.$_useCheckedNonce(sender, currentNonce);
117+
118+
expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(currentNonce + 1n);
119+
});
120+
121+
it('use nonce at another key', async function () {
122+
const currentNonce = await this.mock.nonces(sender, ethers.Typed.uint192(17n));
123+
124+
await this.mock.$_useCheckedNonce(sender, currentNonce);
125+
126+
expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(currentNonce + 1n);
127+
});
128+
129+
it('reverts when nonce is not the expected', async function () {
130+
const currentNonce = await this.mock.nonces(sender, ethers.Typed.uint192(42n));
131+
132+
// use and increment
133+
await this.mock.$_useCheckedNonce(sender, currentNonce);
134+
135+
// reuse same nonce
136+
await expect(this.mock.$_useCheckedNonce(sender, currentNonce))
137+
.to.be.revertedWithCustomError(this.mock, 'InvalidAccountNonce')
138+
.withArgs(sender, 1);
139+
140+
// use "future" nonce too early
141+
await expect(this.mock.$_useCheckedNonce(sender, currentNonce + 10n))
142+
.to.be.revertedWithCustomError(this.mock, 'InvalidAccountNonce')
143+
.withArgs(sender, 1);
144+
});
145+
});
146+
});
147+
}
148+
149+
module.exports = {
150+
shouldBehaveLikeNonces,
151+
shouldBehaveLikeNoncesKeyed,
152+
};

Diff for: test/utils/Nonces.test.js

+3-62
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,16 @@
11
const { ethers } = require('hardhat');
2-
const { expect } = require('chai');
32
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
3+
const { shouldBehaveLikeNonces } = require('./Nonces.behavior');
44

55
async function fixture() {
6-
const [sender, other] = await ethers.getSigners();
7-
86
const mock = await ethers.deployContract('$Nonces');
9-
10-
return { sender, other, mock };
7+
return { mock };
118
}
129

1310
describe('Nonces', function () {
1411
beforeEach(async function () {
1512
Object.assign(this, await loadFixture(fixture));
1613
});
1714

18-
it('gets a nonce', async function () {
19-
expect(await this.mock.nonces(this.sender)).to.equal(0n);
20-
});
21-
22-
describe('_useNonce', function () {
23-
it('increments a nonce', async function () {
24-
expect(await this.mock.nonces(this.sender)).to.equal(0n);
25-
26-
await expect(await this.mock.$_useNonce(this.sender))
27-
.to.emit(this.mock, 'return$_useNonce')
28-
.withArgs(0n);
29-
30-
expect(await this.mock.nonces(this.sender)).to.equal(1n);
31-
});
32-
33-
it("increments only sender's nonce", async function () {
34-
expect(await this.mock.nonces(this.sender)).to.equal(0n);
35-
expect(await this.mock.nonces(this.other)).to.equal(0n);
36-
37-
await this.mock.$_useNonce(this.sender);
38-
39-
expect(await this.mock.nonces(this.sender)).to.equal(1n);
40-
expect(await this.mock.nonces(this.other)).to.equal(0n);
41-
});
42-
});
43-
44-
describe('_useCheckedNonce', function () {
45-
it('increments a nonce', async function () {
46-
const currentNonce = await this.mock.nonces(this.sender);
47-
48-
expect(currentNonce).to.equal(0n);
49-
50-
await this.mock.$_useCheckedNonce(this.sender, currentNonce);
51-
52-
expect(await this.mock.nonces(this.sender)).to.equal(1n);
53-
});
54-
55-
it("increments only sender's nonce", async function () {
56-
const currentNonce = await this.mock.nonces(this.sender);
57-
58-
expect(currentNonce).to.equal(0n);
59-
expect(await this.mock.nonces(this.other)).to.equal(0n);
60-
61-
await this.mock.$_useCheckedNonce(this.sender, currentNonce);
62-
63-
expect(await this.mock.nonces(this.sender)).to.equal(1n);
64-
expect(await this.mock.nonces(this.other)).to.equal(0n);
65-
});
66-
67-
it('reverts when nonce is not the expected', async function () {
68-
const currentNonce = await this.mock.nonces(this.sender);
69-
70-
await expect(this.mock.$_useCheckedNonce(this.sender, currentNonce + 1n))
71-
.to.be.revertedWithCustomError(this.mock, 'InvalidAccountNonce')
72-
.withArgs(this.sender, currentNonce);
73-
});
74-
});
15+
shouldBehaveLikeNonces();
7516
});

Diff for: test/utils/NoncesKeyed.test.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const { ethers } = require('hardhat');
2+
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
3+
const { shouldBehaveLikeNonces, shouldBehaveLikeNoncesKeyed } = require('./Nonces.behavior');
4+
5+
async function fixture() {
6+
const mock = await ethers.deployContract('$NoncesKeyed');
7+
return { mock };
8+
}
9+
10+
describe('NoncesKeyed', function () {
11+
beforeEach(async function () {
12+
Object.assign(this, await loadFixture(fixture));
13+
});
14+
15+
shouldBehaveLikeNonces();
16+
shouldBehaveLikeNoncesKeyed();
17+
});

0 commit comments

Comments
 (0)