-
Notifications
You must be signed in to change notification settings - Fork 12.4k
Expand file tree
/
Copy pathdraft-ERC7579Utils.t.sol
More file actions
508 lines (436 loc) · 20.9 KB
/
draft-ERC7579Utils.t.sol
File metadata and controls
508 lines (436 loc) · 20.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
// Parts of this test file are adapted from Adam Egyed (@adamegyed) proof of concept available at:
// https://github.com/adamegyed/erc7579-execute-vulnerability/tree/4589a30ff139e143d6c57183ac62b5c029217a90
//
// solhint-disable no-console
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import {PackedUserOperation, IAccount, IEntryPoint} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
import {
ERC7579Utils,
Mode,
CallType,
ExecType,
ModeSelector,
ModePayload,
Execution
} from "@openzeppelin/contracts/account/utils/draft-ERC7579Utils.sol";
import {Test, Vm, console} from "forge-std/Test.sol";
contract SampleAccount is IAccount, Ownable {
using ECDSA for *;
using MessageHashUtils for *;
using ERC4337Utils for *;
using ERC7579Utils for *;
event Log(bool duringValidation, Execution[] calls);
error UnsupportedCallType(CallType callType);
constructor(address initialOwner) Ownable(initialOwner) {}
function validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external override returns (uint256 validationData) {
require(msg.sender == address(ERC4337Utils.ENTRYPOINT_V07), "only from EP");
// Check signature
if (userOpHash.toEthSignedMessageHash().recover(userOp.signature) != owner()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
// If this is an execute call with a batch operation, log the call details from the calldata
if (bytes4(userOp.callData[0x00:0x04]) == this.execute.selector) {
(CallType callType, , , ) = Mode.wrap(bytes32(userOp.callData[0x04:0x24])).decodeMode();
if (callType == ERC7579Utils.CALLTYPE_BATCH) {
// Remove the selector
bytes calldata params = userOp.callData[0x04:];
// Use the same vulnerable assignment technique here, but assert afterwards that the checks aren't
// broken here by comparing to the result of `abi.decode(...)`.
bytes calldata executionCalldata;
assembly ("memory-safe") {
let dataptr := add(params.offset, calldataload(add(params.offset, 0x20)))
executionCalldata.offset := add(dataptr, 32)
executionCalldata.length := calldataload(dataptr)
}
// Check that this decoding step is done correctly.
(, bytes memory executionCalldataMemory) = abi.decode(params, (bytes32, bytes));
require(
keccak256(executionCalldata) == keccak256(executionCalldataMemory),
"decoding during validation failed"
);
// Now, we know that we have `bytes calldata executionCalldata` as would be decoded by the solidity
// builtin decoder for the `execute` function.
// This is where the vulnerability from ExecutionLib results in a different result between validation
// and execution.
emit Log(true, executionCalldata.decodeBatch());
}
}
if (missingAccountFunds > 0) {
(bool success, ) = payable(msg.sender).call{value: missingAccountFunds}("");
success; // Silence warning. The entrypoint should validate the result.
}
return ERC4337Utils.SIG_VALIDATION_SUCCESS;
}
function execute(Mode mode, bytes calldata executionCalldata) external payable {
require(msg.sender == address(this) || msg.sender == address(ERC4337Utils.ENTRYPOINT_V07), "not auth");
(CallType callType, ExecType execType, , ) = mode.decodeMode();
// check if calltype is batch or single
if (callType == ERC7579Utils.CALLTYPE_SINGLE) {
executionCalldata.execSingle(execType);
} else if (callType == ERC7579Utils.CALLTYPE_BATCH) {
executionCalldata.execBatch(execType);
emit Log(false, executionCalldata.decodeBatch());
} else if (callType == ERC7579Utils.CALLTYPE_DELEGATECALL) {
executionCalldata.execDelegateCall(execType);
} else {
revert UnsupportedCallType(callType);
}
}
}
contract ERC7579UtilsTest is Test {
using MessageHashUtils for *;
using ERC4337Utils for *;
using ERC7579Utils for *;
address private _owner;
uint256 private _ownerKey;
address private _account;
address private _beneficiary;
address private _recipient1;
address private _recipient2;
constructor() {
// EntryPoint070
vm.etch(
0x0000000071727De22E5E9d8BAf0edAc6f37da032,
vm.readFileBinary("node_modules/hardhat-predeploy/bin/0x0000000071727De22E5E9d8BAf0edAc6f37da032.bytecode")
);
// SenderCreator070
vm.etch(
0xEFC2c1444eBCC4Db75e7613d20C6a62fF67A167C,
vm.readFileBinary("node_modules/hardhat-predeploy/bin/0xEFC2c1444eBCC4Db75e7613d20C6a62fF67A167C.bytecode")
);
// signing key
(_owner, _ownerKey) = makeAddrAndKey("owner");
// ERC-4337 account
_account = address(new SampleAccount(_owner));
vm.deal(_account, 1 ether);
// other
_beneficiary = makeAddr("beneficiary");
_recipient1 = makeAddr("recipient1");
_recipient2 = makeAddr("recipient2");
}
function testExecuteBatchDecodeCorrectly() public {
Execution[] memory calls = new Execution[](2);
calls[0] = Execution({target: _recipient1, value: 1 wei, callData: ""});
calls[1] = Execution({target: _recipient2, value: 1 wei, callData: ""});
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
userOps[0] = PackedUserOperation({
sender: _account,
nonce: 0,
initCode: "",
callData: abi.encodeCall(
SampleAccount.execute,
(
ERC7579Utils.encodeMode(
ERC7579Utils.CALLTYPE_BATCH,
ERC7579Utils.EXECTYPE_DEFAULT,
ModeSelector.wrap(0x00),
ModePayload.wrap(0x00)
),
ERC7579Utils.encodeBatch(calls)
)
),
accountGasLimits: _packGas(500_000, 500_000),
preVerificationGas: 0,
gasFees: _packGas(1, 1),
paymasterAndData: "",
signature: ""
});
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
_ownerKey,
this.hashUserOperation(userOps[0]).toEthSignedMessageHash()
);
userOps[0].signature = abi.encodePacked(r, s, v);
vm.recordLogs();
ERC4337Utils.ENTRYPOINT_V07.handleOps(userOps, payable(_beneficiary));
assertEq(_recipient1.balance, 1 wei);
assertEq(_recipient2.balance, 1 wei);
_collectAndPrintLogs(false);
}
function testExecuteBatchDecodeEmpty() public {
bytes memory fakeCalls = abi.encodePacked(
uint256(1), // Length of execution[]
uint256(0x20), // offset
uint256(uint160(_recipient1)), // target
uint256(1), // value: 1 wei
uint256(0x60), // offset of data
uint256(0) // length of
);
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
userOps[0] = PackedUserOperation({
sender: _account,
nonce: 0,
initCode: "",
callData: abi.encodeCall(
SampleAccount.execute,
(
ERC7579Utils.encodeMode(
ERC7579Utils.CALLTYPE_BATCH,
ERC7579Utils.EXECTYPE_DEFAULT,
ModeSelector.wrap(0x00),
ModePayload.wrap(0x00)
),
abi.encodePacked(
uint256(0x70) // fake offset pointing to paymasterAndData
)
)
),
accountGasLimits: _packGas(500_000, 500_000),
preVerificationGas: 0,
gasFees: _packGas(1, 1),
paymasterAndData: abi.encodePacked(address(0), fakeCalls),
signature: ""
});
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
_ownerKey,
this.hashUserOperation(userOps[0]).toEthSignedMessageHash()
);
userOps[0].signature = abi.encodePacked(r, s, v);
vm.expectRevert(
abi.encodeWithSelector(
IEntryPoint.FailedOpWithRevert.selector,
0,
"AA23 reverted",
abi.encodeWithSelector(ERC7579Utils.ERC7579DecodingError.selector)
)
);
ERC4337Utils.ENTRYPOINT_V07.handleOps(userOps, payable(_beneficiary));
_collectAndPrintLogs(false);
}
function testExecuteBatchDecodeDifferent() public {
bytes memory execCallData = abi.encodePacked(
uint256(0x20), // offset pointing to the next segment
uint256(5), // Length of execution[]
uint256(0), // offset of calls[0], and target (!!)
uint256(0x20), // offset of calls[1], and value (!!)
uint256(0), // offset of calls[2], and rel offset of data (!!)
uint256(0) // offset of calls[3].
// There is one more to read by the array length, but it's not present here. This will be
// paymasterAndData.length during validation, pointing to an all-zero call.
// During execution, the offset will be 0, pointing to a call with value.
);
PackedUserOperation[] memory userOps = new PackedUserOperation[](1);
userOps[0] = PackedUserOperation({
sender: _account,
nonce: 0,
initCode: "",
callData: abi.encodePacked(
SampleAccount.execute.selector,
ERC7579Utils.encodeMode(
ERC7579Utils.CALLTYPE_BATCH,
ERC7579Utils.EXECTYPE_DEFAULT,
ModeSelector.wrap(0x00),
ModePayload.wrap(0x00)
),
uint256(0x5c), // offset pointing to the next segment
uint224(type(uint224).max), // Padding to align the `bytes` types
// type(uint256).max, // unknown padding
uint256(execCallData.length), // Length of the data
execCallData
),
accountGasLimits: _packGas(500_000, 500_000),
preVerificationGas: 0,
gasFees: _packGas(1, 1),
paymasterAndData: abi.encodePacked(uint256(0), uint256(0)), // padding length to create an offset
signature: ""
});
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
_ownerKey,
this.hashUserOperation(userOps[0]).toEthSignedMessageHash()
);
userOps[0].signature = abi.encodePacked(r, s, v);
vm.expectRevert(
abi.encodeWithSelector(
IEntryPoint.FailedOpWithRevert.selector,
0,
"AA23 reverted",
abi.encodeWithSelector(ERC7579Utils.ERC7579DecodingError.selector)
)
);
ERC4337Utils.ENTRYPOINT_V07.handleOps(userOps, payable(_beneficiary));
_collectAndPrintLogs(true);
}
uint256 private constant TEST_DECODE = 0x01;
uint256 private constant TEST_GETFIRST = 0x02;
uint256 private constant TEST_GETFIRSTBYTES = 0x04;
uint256 private constant FAIL_DECODE = 0x10;
uint256 private constant FAIL_GETFIRST = 0x20;
uint256 private constant FAIL_GETFIRSTBYTES = 0x40;
uint256 private constant FAIL_ANY = FAIL_DECODE | FAIL_GETFIRST | FAIL_GETFIRSTBYTES;
// BAD: buffer empty
function testDecodeBatchEmptyBuffer() public {
_testDecodeBatch("", TEST_DECODE | FAIL_DECODE);
}
// BAD: buffer too short
function testDecodeBatchBufferTooShort() public {
_testDecodeBatch(abi.encodePacked(uint128(0)), TEST_DECODE | FAIL_DECODE);
}
// GOOD
function testDecodeBatchZero() public {
_testDecodeBatch(abi.encode(0), TEST_DECODE);
// Note: Solidity also supports this even though it's odd. Offset 0 means array is at the same location, which
// is interpreted as an array of length 0, which doesn't require any more data
// solhint-disable-next-line var-name-mixedcase
uint256[] memory _1 = abi.decode(abi.encode(0), (uint256[]));
_1;
}
// BAD: offset is out of bounds
function testDecodeBatchOffsetOutOfBound() public {
_testDecodeBatch(abi.encode(1), TEST_DECODE | FAIL_DECODE);
}
// GOOD
function testDecodeBatchEmptyArray() public {
_testDecodeBatch(abi.encode(32, 0), TEST_DECODE);
}
// BAD: reported array length extends beyond bounds
function testDecodeBatchLengthExtendsOutOfBound() public {
_testDecodeBatch(abi.encode(32, 1), TEST_DECODE | FAIL_DECODE);
}
// GOOD
function testDecodeBatchFirstBytes() public view {
// 0000000000000000000000000000000000000000000000000000000000000020 (32) offset
// 0000000000000000000000000000000000000000000000000000000000000001 ( 1) array length
// 0000000000000000000000000000000000000000000000000000000000000020 (32) element 0 offset
// 000000000000000000000000xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (recipient) target for element #0
// 000000000000000000000000000000000000000000000000000000000000002a (42) value for element #0
// 0000000000000000000000000000000000000000000000000000000000000060 (96) offset to calldata for element #0
// 000000000000000000000000000000000000000000000000000000000000000c (12) length of the calldata for element #0
// 48656c6c6f20576f726c64210000000000000000000000000000000000000000 (..) buffer for the calldata for element #0
assertEq(
bytes("Hello World!"),
this.callDecodeBatchAndGetFirstBytes(
abi.encode(32, 1, 32, _recipient1, 42, 96, 12, bytes12("Hello World!"))
)
);
}
// GOOD at first level, BAD when dereferencing
function testDecodeBatchDeepOutOfBound1() public {
// This is invalid, the first element of the array points is out of bounds
//
// 0000000000000000000000000000000000000000000000000000000000000020 (32) offset
// 0000000000000000000000000000000000000000000000000000000000000001 ( 1) array length
// 0000000000000000000000000000000000000000000000000000000000000000 ( 0) element 0 offset
// <missing element>
_testDecodeBatch(abi.encode(32, 1, 0), TEST_DECODE | TEST_GETFIRST | FAIL_GETFIRST);
}
// GOOD at first level, BAD when dereferencing
function testDecodeBatchDeepOutOfBound2() public {
// This is invalid, the first element of the array points is out of bounds
//
// 0000000000000000000000000000000000000000000000000000000000000020 (32) offset
// 0000000000000000000000000000000000000000000000000000000000000001 ( 1) array length
// 0000000000000000000000000000000000000000000000000000000000000020 (32) element 0 offset
// <missing element>
_testDecodeBatch(abi.encode(32, 1, 32), TEST_DECODE | TEST_GETFIRST | FAIL_GETFIRST);
}
function testDecodeBatchDeepOutOfBound3() public {
// This is invalid: the first item is partly out of bounds.
//
// 0000000000000000000000000000000000000000000000000000000000000020 (32) offset
// 0000000000000000000000000000000000000000000000000000000000000001 ( 1) array length
// 0000000000000000000000000000000000000000000000000000000000000020 (32) element 0 offset
// 000000000000000000000000xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (recipient) target for element #0
// <missing data>
_testDecodeBatch(abi.encode(32, 1, 32, _recipient1), TEST_DECODE | TEST_GETFIRST | FAIL_GETFIRST);
}
function testDecodeBatchDeepOutOfBound4() public {
// This is invalid: the bytes field of the first element of the array is out of bounds
//
// 0000000000000000000000000000000000000000000000000000000000000020 (32) offset
// 0000000000000000000000000000000000000000000000000000000000000001 ( 1) array length
// 0000000000000000000000000000000000000000000000000000000000000020 (32) element 0 offset
// 000000000000000000000000xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (recipient) target for element #0
// 000000000000000000000000000000000000000000000000000000000000002a (42) value for element #0
// 0000000000000000000000000000000000000000000000000000000000000060 (96) offset to calldata for element #0
// <missing data>
_testDecodeBatch(
abi.encode(32, 1, 32, _recipient1, 42, 96),
TEST_DECODE | TEST_GETFIRST | TEST_GETFIRSTBYTES | FAIL_GETFIRSTBYTES
);
}
function _testDecodeBatch(bytes memory encoded, uint256 test) private {
bytes memory extraData = new bytes(256);
if (test & TEST_DECODE > 0) {
if (test & FAIL_DECODE > 0) vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector);
this.callDecodeBatch(encoded);
if (test & FAIL_ANY > 0) vm.expectRevert();
this.callDecodeBatchWithCalldata(encoded, extraData);
}
if (test & TEST_GETFIRST > 0) {
if (test & FAIL_GETFIRST > 0) vm.expectRevert();
this.callDecodeBatchAndGetFirst(encoded);
if (test & FAIL_ANY > 0) vm.expectRevert();
this.callDecodeBatchAndGetFirstWithCalldata(encoded, extraData);
}
if (test & TEST_GETFIRSTBYTES > 0) {
if (test & FAIL_GETFIRSTBYTES > 0) vm.expectRevert();
this.callDecodeBatchAndGetFirstBytes(encoded);
if (test & FAIL_ANY > 0) vm.expectRevert();
this.callDecodeBatchAndGetFirstBytesWithCalldata(encoded, extraData);
}
}
function callDecodeBatch(bytes calldata executionCalldata) public pure {
ERC7579Utils.decodeBatch(executionCalldata);
}
function callDecodeBatchWithCalldata(bytes calldata executionCalldata, bytes calldata) public pure {
ERC7579Utils.decodeBatch(executionCalldata);
}
function callDecodeBatchAndGetFirst(bytes calldata executionCalldata) public pure {
ERC7579Utils.decodeBatch(executionCalldata)[0];
}
function callDecodeBatchAndGetFirstWithCalldata(bytes calldata executionCalldata, bytes calldata) public pure {
ERC7579Utils.decodeBatch(executionCalldata)[0];
}
function callDecodeBatchAndGetFirstBytes(bytes calldata executionCalldata) public pure returns (bytes calldata) {
return ERC7579Utils.decodeBatch(executionCalldata)[0].callData;
}
function callDecodeBatchAndGetFirstBytesWithCalldata(
bytes calldata executionCalldata,
bytes calldata
) public pure returns (bytes calldata) {
return ERC7579Utils.decodeBatch(executionCalldata)[0].callData;
}
function hashUserOperation(PackedUserOperation calldata useroperation) public view returns (bytes32) {
return useroperation.hash(address(ERC4337Utils.ENTRYPOINT_V07));
}
function _collectAndPrintLogs(bool includeTotalValue) internal {
Vm.Log[] memory logs = vm.getRecordedLogs();
for (uint256 i = 0; i < logs.length; i++) {
if (logs[i].emitter == _account) {
_printDecodedCalls(logs[i].data, includeTotalValue);
}
}
}
function _printDecodedCalls(bytes memory logData, bool includeTotalValue) internal pure {
(bool duringValidation, Execution[] memory calls) = abi.decode(logData, (bool, Execution[]));
console.log(
string.concat(
"Batch execute contents, as read during ",
duringValidation ? "validation" : "execution",
": "
)
);
console.log(" Execution[] length: %s", calls.length);
uint256 totalValue = 0;
for (uint256 i = 0; i < calls.length; ++i) {
console.log(string.concat(" calls[", vm.toString(i), "].target = ", vm.toString(calls[i].target)));
console.log(string.concat(" calls[", vm.toString(i), "].value = ", vm.toString(calls[i].value)));
console.log(string.concat(" calls[", vm.toString(i), "].data = ", vm.toString(calls[i].callData)));
totalValue += calls[i].value;
}
if (includeTotalValue) {
console.log(" Total value: %s", totalValue);
}
}
function _packGas(uint256 upper, uint256 lower) internal pure returns (bytes32) {
return bytes32(uint256((upper << 128) | uint128(lower)));
}
}