Skip to content

Commit 5ee90cb

Browse files
committed
added EIP-4337-like nonce
1 parent 53f953f commit 5ee90cb

File tree

2 files changed

+120
-21
lines changed

2 files changed

+120
-21
lines changed

packages/ethereum-contracts/contracts/utils/Only712MacroForwarder.sol

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,51 @@ import { IUserDefined712Macro } from "../interfaces/utils/IUserDefinedMacro.sol"
77
import { ISuperfluid } from "../interfaces/superfluid/ISuperfluid.sol";
88
import { ForwarderBase } from "./ForwarderBase.sol";
99

10+
11+
/**
12+
* Nonce management functionality following the semantics of ERC-4337.
13+
* Each nonce consists of a 192-bit key and a 64-bit sequence number.
14+
* This allows senders to both have a practically unlimited number of parallel operations
15+
* (meaning signed pending transactions can't block each other), and also the option to enforce
16+
* sequential execution according to the sequence number.
17+
*/
18+
abstract contract NonceManager {
19+
/// nonce already used or out of sequence
20+
error InvalidNonce(address sender, uint256 nonce);
21+
22+
/// data structure keeping track of the next sequence number by sender and key
23+
mapping(address => mapping(uint192 => uint256)) internal _nonceSequenceNumber;
24+
25+
/// Returns the next nonce for a given sender and key
26+
function getNonce(address sender, uint192 key) public virtual view returns (uint256 nonce) {
27+
return _nonceSequenceNumber[sender][key] | (uint256(key) << 64);
28+
}
29+
30+
/// validates the nonce and updates the data structure for correct sequencing
31+
function _validateAndUpdateNonce(address sender, uint256 nonce) internal virtual {
32+
uint192 key = uint192(nonce >> 64);
33+
uint64 seq = uint64(nonce);
34+
if (_nonceSequenceNumber[sender][key]++ != seq) {
35+
revert InvalidNonce(sender, nonce);
36+
}
37+
}
38+
}
39+
1040
/**
1141
* @dev EIP-712-aware macro forwarder (clear signing).
1242
* In this minimal iteration: decodes payload as appParams and passes through to the macro.
1343
* Envelope verification, nonce, and registry checks to be added in follow-up.
44+
*
45+
* TODO:
46+
* -[] use SimpleACL as registry
47+
* -[X] add nonce verification
48+
* -[] add missing fields
49+
* -[] extract interface definition
50+
* -[] review naming
1451
*/
15-
contract Only712MacroForwarder is ForwarderBase, EIP712 {
52+
contract Only712MacroForwarder is ForwarderBase, EIP712, NonceManager {
53+
54+
// STRUCTS AND CONSTANTS
1655

1756
// top-level data structure
1857
// TODO: is "payload" a good name? Does EIP-712 give a good hint for naming this? Something "primary"?
@@ -44,18 +83,27 @@ contract Only712MacroForwarder is ForwarderBase, EIP712 {
4483
bytes internal constant _TYPEDEF_SECURITY = "Security(string provider,uint256 nonce)";
4584
bytes32 internal constant _TYPEHASH_SECURITY = keccak256(_TYPEDEF_SECURITY);
4685

86+
// ERRORS
87+
4788
error InvalidPayload(string message);
4889
error InvalidProvider(string provider);
4990
error InvalidSignature();
5091

92+
// INITIALIZATION
93+
5194
// Here EIP712 domain name and version are set.
5295
// TODO: should the name include "Superfluid"?
5396
constructor(ISuperfluid host, address /*registry*/) ForwarderBase(host) EIP712("ClearSigning", "1") {}
5497

98+
// PUBLIC FUNCTIONS
99+
55100
/**
56101
* @dev Run the macro with encoded payload (generic + macro specific fragments).
57102
* @param m Target macro.
58103
* @param params Encoded payload
104+
* @param signer The signer of the payload
105+
* @param signature The signature of the payload
106+
* @return bool True if the macro was executed successfully
59107
*/
60108
function runMacro(IUserDefined712Macro m, bytes calldata params, address signer, bytes calldata signature)
61109
external payable
@@ -67,7 +115,8 @@ contract Only712MacroForwarder is ForwarderBase, EIP712 {
67115
keccak256(bytes(payload.security.provider)) == keccak256(bytes("macros.superfluid.eth")),
68116
InvalidProvider(payload.security.provider)
69117
);
70-
// TODO: verify nonce (replay protection)
118+
119+
_validateAndUpdateNonce(signer, payload.security.nonce);
71120

72121
bytes32 digest = _getDigest(m, payload);
73122

@@ -106,9 +155,7 @@ contract Only712MacroForwarder is ForwarderBase, EIP712 {
106155
return _getDigest(m, abi.decode(params, (Payload)));
107156
}
108157

109-
// ==============================
110-
// Internal functions
111-
// ==============================
158+
// INTERNAL FUNCTIONS
112159

113160
function _getTypeDefinition(IUserDefined712Macro m) internal view returns (string memory) {
114161
return string(abi.encodePacked(

packages/ethereum-contracts/test/foundry/utils/Only712MacroForwarder.t.sol

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { VmSafe } from "forge-std/Vm.sol";
55
import { console } from "forge-std/console.sol";
66
import { ISuperfluid, ISuperfluidToken } from "../../../contracts/interfaces/superfluid/ISuperfluid.sol";
77
import { IUserDefined712Macro } from "../../../contracts/interfaces/utils/IUserDefinedMacro.sol";
8-
import { Only712MacroForwarder } from "../../../contracts/utils/Only712MacroForwarder.sol";
8+
import { Only712MacroForwarder, NonceManager } from "../../../contracts/utils/Only712MacroForwarder.sol";
99
import { FoundrySuperfluidTester } from "../FoundrySuperfluidTester.t.sol";
1010

1111
string constant MESSAGE_TITLE = "Hello 712";
@@ -14,12 +14,17 @@ string constant META_DOMAIN = "minimalmacro.xyz";
1414
string constant META_VERSION = "1";
1515
string constant SECURITY_PROVIDER = "macros.superfluid.eth";
1616

17-
// returns the encoded payload for the example macro
17+
// returns the encoded payload for the example macro (nonce = key 1, sequence 0)
1818
function getTestPayload() pure returns (bytes memory) {
19+
return getPayloadWithNonce(uint256(1) << 64);
20+
}
21+
22+
// returns the encoded payload with the given nonce (for nonce tests)
23+
function getPayloadWithNonce(uint256 nonce) pure returns (bytes memory) {
1924
Only712MacroForwarder.Payload memory payload = Only712MacroForwarder.Payload({
2025
meta: Only712MacroForwarder.PayloadMeta({ domain: META_DOMAIN, version: META_VERSION }),
2126
message: Only712MacroForwarder.PayloadMessage({ title: MESSAGE_TITLE, customPayload: new bytes(0) }),
22-
security: Only712MacroForwarder.PayloadSecurity({ provider: SECURITY_PROVIDER, nonce: 1 })
27+
security: Only712MacroForwarder.PayloadSecurity({ provider: SECURITY_PROVIDER, nonce: nonce })
2328
});
2429
return abi.encode(payload);
2530
}
@@ -78,20 +83,10 @@ contract Only712MacroForwarderTest is FoundrySuperfluidTester {
7883
sf.governance.enableTrustedForwarder(sf.host, ISuperfluidToken(address(0)), address(forwarder));
7984
}
8085

81-
/**
82-
* @dev Smoke test: build payload, get digest via getDigest(), sign with vm.createWallet + vm.sign,
83-
* call runMacro(m, params, signer, signature), assert success.
84-
*/
8586
function testRunMacro() external {
8687
VmSafe.Wallet memory signer = vm.createWallet("signer");
87-
bytes memory params = getTestPayload();
88-
bytes32 digest = forwarder.getDigest(IUserDefined712Macro(address(minimal712Macro)), params);
89-
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signer, digest);
90-
bytes memory signatureVRS = abi.encodePacked(r, s, v);
91-
92-
vm.prank(signer.addr);
93-
bool ok = forwarder.runMacro(IUserDefined712Macro(address(minimal712Macro)), params, signer.addr, signatureVRS);
94-
assertTrue(ok);
88+
(bytes memory params, bytes memory signatureVRS) = _signPayload(signer, uint256(1) << 64);
89+
assertTrue(_runMacroAs(signer.addr, params, signatureVRS));
9590
}
9691

9792
function testDigestCalculation() external view {
@@ -119,6 +114,47 @@ contract Only712MacroForwarderTest is FoundrySuperfluidTester {
119114
assertEq(digest, expectedDigest, "digest mismatch");
120115
}
121116

117+
function testGetNonce(uint192 key) external {
118+
VmSafe.Wallet memory signer = vm.createWallet("signer");
119+
120+
for (uint256 i = 0; i < 10; i++) {
121+
uint256 nonce = forwarder.getNonce(signer.addr, key);
122+
(bytes memory params, bytes memory signatureVRS) = _signPayload(signer, nonce);
123+
assertTrue(_runMacroAs(signer.addr, params, signatureVRS), "runMacro with getNonce() nonce should succeed");
124+
}
125+
}
126+
127+
function testCannotReuseNonce(uint192 key) external {
128+
VmSafe.Wallet memory signer = vm.createWallet("signer");
129+
130+
uint256 nonce = forwarder.getNonce(signer.addr, key);
131+
(bytes memory params, bytes memory signatureVRS) = _signPayload(signer, nonce);
132+
assertTrue(_runMacroAs(signer.addr, params, signatureVRS));
133+
134+
vm.expectRevert(abi.encodeWithSelector(NonceManager.InvalidNonce.selector, signer.addr, nonce));
135+
_runMacroAs(signer.addr, params, signatureVRS);
136+
}
137+
138+
/// For a given key, nonces must be used in sequence (0, 1, 2, ...). Skipping must revert.
139+
function testNonceEnforceInSequence(uint192 key) external {
140+
VmSafe.Wallet memory signer = vm.createWallet("signer");
141+
142+
// Using seq=1 before seq=0 must revert
143+
uint256 nonceSeq1 = (uint256(key) << 64) | 1;
144+
(bytes memory paramsSeq1, bytes memory sig1) = _signPayload(signer, nonceSeq1);
145+
146+
vm.expectRevert(abi.encodeWithSelector(NonceManager.InvalidNonce.selector, signer.addr, nonceSeq1));
147+
_runMacroAs(signer.addr, paramsSeq1, sig1);
148+
149+
// seq=0 must succeed
150+
uint256 nonceSeq0 = uint256(key) << 64;
151+
(bytes memory paramsSeq0, bytes memory sig0) = _signPayload(signer, nonceSeq0);
152+
assertTrue(_runMacroAs(signer.addr, paramsSeq0, sig0));
153+
154+
// now seq=1 must succeed
155+
assertTrue(_runMacroAs(signer.addr, paramsSeq1, sig1));
156+
}
157+
122158
// example: https://github.com/vaquita-fi/vaquita-lisk/blob/c4964af9157c9cca9cfb167ac1a4450e36edb29e/contracts/test/VaquitaPool.t.sol#L142
123159
// The splitting up into many functions avoids stack too deep error.
124160
function getDataToBeSignedJson() internal view returns (string memory) {
@@ -216,9 +252,25 @@ contract Only712MacroForwarderTest is FoundrySuperfluidTester {
216252
}
217253

218254
function _getSecurityJson() internal pure returns (string memory) {
255+
// Use string for nonce so Foundry's JSON parser accepts 2^64 as uint256 (avoids type mismatch)
219256
return string(abi.encodePacked(
220257
'"provider": "', SECURITY_PROVIDER, '",',
221-
'"nonce": ', '1'
258+
'"nonce": "', vm.toString(uint256(1) << 64), '"'
222259
));
223260
}
261+
262+
function _runMacroAs(address from, bytes memory params, bytes memory signatureVRS) internal returns (bool) {
263+
vm.prank(from);
264+
return forwarder.runMacro(minimal712Macro, params, from, signatureVRS);
265+
}
266+
267+
function _signPayload(VmSafe.Wallet memory signer, uint256 nonce)
268+
internal
269+
returns (bytes memory params, bytes memory signatureVRS)
270+
{
271+
params = getPayloadWithNonce(nonce);
272+
bytes32 digest = forwarder.getDigest(minimal712Macro, params);
273+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signer, digest);
274+
signatureVRS = abi.encodePacked(r, s, v);
275+
}
224276
}

0 commit comments

Comments
 (0)