-
Notifications
You must be signed in to change notification settings - Fork 364
/
Copy pathERC4337.sol
430 lines (391 loc) · 19 KB
/
ERC4337.sol
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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import {Receiver} from "./Receiver.sol";
import {LibZip} from "../utils/LibZip.sol";
import {Ownable} from "../auth/Ownable.sol";
import {UUPSUpgradeable} from "../utils/UUPSUpgradeable.sol";
import {SignatureCheckerLib, ERC1271} from "../accounts/ERC1271.sol";
/// @notice Simple ERC4337 account implementation.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/accounts/ERC4337.sol)
/// @author Infinitism (https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/samples/SimpleAccount.sol)
///
/// @dev Recommended usage:
/// 1. Deploy the ERC4337 as an implementation contract, and verify it on Etherscan.
/// 2. Create a factory that uses `LibClone.deployERC1967` or
/// `LibClone.deployDeterministicERC1967` to clone the implementation.
/// See: `ERC4337Factory.sol`.
///
/// Note:
/// ERC4337 is a very complicated standard with many potential gotchas.
/// Also, it is subject to change and has not been finalized
/// (so accounts are encouraged to be upgradeable).
/// Usually, ERC4337 account implementations are developed by companies with ample funds
/// for security reviews. This implementation is intended to serve as a base reference
/// for smart account developers working in such companies. If you are using this
/// implementation, please do get one or more security reviews before deployment.
abstract contract ERC4337 is Ownable, UUPSUpgradeable, Receiver, ERC1271 {
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STRUCTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev The packed ERC4337 user operation (userOp) struct.
struct PackedUserOperation {
address sender;
uint256 nonce;
bytes initCode; // Factory address and `factoryData` (or empty).
bytes callData;
bytes32 accountGasLimits; // `verificationGas` (16 bytes) and `callGas` (16 bytes).
uint256 preVerificationGas;
bytes32 gasFees; // `maxPriorityFee` (16 bytes) and `maxFeePerGas` (16 bytes).
bytes paymasterAndData; // Paymaster fields (or empty).
bytes signature;
}
/// @dev Call struct for the `executeBatch` function.
struct Call {
address target;
uint256 value;
bytes data;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CONSTRUCTOR */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Deploys this ERC4337 account implementation and disables initialization (see below).
constructor() payable {
_disableERC4337ImplementationInitializer();
}
/// @dev Automatically initializes the owner for the implementation. This blocks someone
/// from initializing the implementation and doing a delegatecall to SELFDESTRUCT.
/// Proxies to the implementation will still be able to initialize as per normal.
function _disableERC4337ImplementationInitializer() internal virtual {
// Note that `Ownable._guardInitializeOwner` has been and must be overridden
// to return true, to block double-initialization. We'll initialize to `address(1)`,
// so that it's easier to verify that the implementation has been initialized.
_initializeOwner(address(1));
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INITIALIZER */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Initializes the account with the owner. Can only be called once.
function initialize(address newOwner) public payable virtual {
_initializeOwner(newOwner);
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ENTRY POINT */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Returns the canonical ERC4337 EntryPoint contract (0.7).
/// Override this function to return a different EntryPoint.
function entryPoint() public view virtual returns (address) {
return 0x0000000071727De22E5E9d8BAf0edAc6f37da032;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* VALIDATION OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Validates the signature and nonce.
/// The EntryPoint will make the call to the recipient only if
/// this validation call returns successfully.
///
/// Signature failure should be reported by returning 1 (see: `_validateSignature`).
/// This allows making a "simulation call" without a valid signature.
/// Other failures (e.g. nonce mismatch, or invalid signature format)
/// should still revert to signal failure.
function validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
)
external
payable
virtual
onlyEntryPoint
payPrefund(missingAccountFunds)
returns (uint256 validationData)
{
validationData = _validateSignature(userOp, userOpHash);
_validateNonce(userOp.nonce);
}
/// @dev Validate `userOp.signature` for the `userOpHash`.
function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash)
internal
virtual
returns (uint256 validationData)
{
bool success = SignatureCheckerLib.isValidSignatureNowCalldata(
owner(), SignatureCheckerLib.toEthSignedMessageHash(userOpHash), userOp.signature
);
/// @solidity memory-safe-assembly
assembly {
// Returns 0 if the recovered address matches the owner.
// Else returns 1, which is equivalent to:
// `(success ? 0 : 1) | (uint256(validUntil) << 160) | (uint256(validAfter) << (160 + 48))`
// where `validUntil` is 0 (indefinite) and `validAfter` is 0.
validationData := iszero(success)
}
}
/// @dev Override to validate the nonce of the userOp.
/// This method may validate the nonce requirement of this account.
/// e.g.
/// To limit the nonce to use sequenced userOps only (no "out of order" userOps):
/// `require(nonce < type(uint64).max)`
/// For a hypothetical account that *requires* the nonce to be out-of-order:
/// `require(nonce & type(uint64).max == 0)`
///
/// The actual nonce uniqueness is managed by the EntryPoint, and thus no other
/// action is needed by the account itself.
function _validateNonce(uint256 nonce) internal virtual {
nonce = nonce; // Silence unused variable warning.
}
/// @dev Sends to the EntryPoint (i.e. `msg.sender`) the missing funds for this transaction.
/// Subclass MAY override this modifier for better funds management.
/// (e.g. send to the EntryPoint more than the minimum required, so that in future transactions
/// it will not be required to send again)
///
/// `missingAccountFunds` is the minimum value this modifier should send the EntryPoint,
/// which MAY be zero, in case there is enough deposit, or the userOp has a paymaster.
modifier payPrefund(uint256 missingAccountFunds) virtual {
_;
/// @solidity memory-safe-assembly
assembly {
if missingAccountFunds {
// Ignore failure (it's EntryPoint's job to verify, not the account's).
pop(call(gas(), caller(), missingAccountFunds, codesize(), 0x00, codesize(), 0x00))
}
}
}
/// @dev Requires that the caller is the EntryPoint.
modifier onlyEntryPoint() virtual {
if (msg.sender != entryPoint()) revert Unauthorized();
_;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* EXECUTION OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Execute a call from this account.
function execute(address target, uint256 value, bytes calldata data)
public
payable
virtual
onlyEntryPointOrOwner
returns (bytes memory result)
{
/// @solidity memory-safe-assembly
assembly {
result := mload(0x40)
calldatacopy(result, data.offset, data.length)
if iszero(call(gas(), target, value, result, data.length, codesize(), 0x00)) {
// Bubble up the revert if the call reverts.
returndatacopy(result, 0x00, returndatasize())
revert(result, returndatasize())
}
mstore(result, returndatasize()) // Store the length.
let o := add(result, 0x20)
returndatacopy(o, 0x00, returndatasize()) // Copy the returndata.
mstore(0x40, add(o, returndatasize())) // Allocate the memory.
}
}
/// @dev Execute a sequence of calls from this account.
function executeBatch(Call[] calldata calls)
public
payable
virtual
onlyEntryPointOrOwner
returns (bytes[] memory results)
{
/// @solidity memory-safe-assembly
assembly {
results := mload(0x40)
mstore(results, calls.length)
let r := add(0x20, results)
let m := add(r, shl(5, calls.length))
calldatacopy(r, calls.offset, shl(5, calls.length))
for { let end := m } iszero(eq(r, end)) { r := add(r, 0x20) } {
let e := add(calls.offset, mload(r))
let o := add(e, calldataload(add(e, 0x40)))
calldatacopy(m, add(o, 0x20), calldataload(o))
// forgefmt: disable-next-item
if iszero(call(gas(), calldataload(e), calldataload(add(e, 0x20)),
m, calldataload(o), codesize(), 0x00)) {
// Bubble up the revert if the call reverts.
returndatacopy(m, 0x00, returndatasize())
revert(m, returndatasize())
}
mstore(r, m) // Append `m` into `results`.
mstore(m, returndatasize()) // Store the length,
let p := add(m, 0x20)
returndatacopy(p, 0x00, returndatasize()) // and copy the returndata.
m := add(p, returndatasize()) // Advance `m`.
}
mstore(0x40, m) // Allocate the memory.
}
}
/// @dev Execute a delegatecall with `delegate` on this account.
function delegateExecute(address delegate, bytes calldata data)
public
payable
virtual
onlyEntryPointOrOwner
delegateExecuteGuard
returns (bytes memory result)
{
/// @solidity memory-safe-assembly
assembly {
result := mload(0x40)
calldatacopy(result, data.offset, data.length)
// Forwards the `data` to `delegate` via delegatecall.
if iszero(delegatecall(gas(), delegate, result, data.length, codesize(), 0x00)) {
// Bubble up the revert if the call reverts.
returndatacopy(result, 0x00, returndatasize())
revert(result, returndatasize())
}
mstore(result, returndatasize()) // Store the length.
let o := add(result, 0x20)
returndatacopy(o, 0x00, returndatasize()) // Copy the returndata.
mstore(0x40, add(o, returndatasize())) // Allocate the memory.
}
}
/// @dev Ensures that the owner and implementation slots' values aren't changed.
/// You can override this modifier to ensure the sanctity of other storage slots too.
modifier delegateExecuteGuard() virtual {
bytes32 ownerSlotValue;
bytes32 implementationSlotValue;
/// @solidity memory-safe-assembly
assembly {
implementationSlotValue := sload(_ERC1967_IMPLEMENTATION_SLOT)
ownerSlotValue := sload(_OWNER_SLOT)
}
_;
/// @solidity memory-safe-assembly
assembly {
if iszero(
and(
eq(implementationSlotValue, sload(_ERC1967_IMPLEMENTATION_SLOT)),
eq(ownerSlotValue, sload(_OWNER_SLOT))
)
) { revert(codesize(), 0x00) }
}
}
/// @dev Requires that the caller is the EntryPoint, the owner, or the account itself.
modifier onlyEntryPointOrOwner() virtual {
if (msg.sender != entryPoint()) _checkOwner();
_;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* DIRECT STORAGE OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Returns the raw storage value at `storageSlot`.
function storageLoad(bytes32 storageSlot) public view virtual returns (bytes32 result) {
/// @solidity memory-safe-assembly
assembly {
result := sload(storageSlot)
}
}
/// @dev Writes the raw storage value at `storageSlot`.
function storageStore(bytes32 storageSlot, bytes32 storageValue)
public
payable
virtual
onlyEntryPointOrOwner
storageStoreGuard(storageSlot)
{
/// @solidity memory-safe-assembly
assembly {
sstore(storageSlot, storageValue)
}
}
/// @dev Ensures that the `storageSlot` is prohibited for direct storage writes.
/// You can override this modifier to ensure the sanctity of other storage slots too.
modifier storageStoreGuard(bytes32 storageSlot) virtual {
/// @solidity memory-safe-assembly
assembly {
if or(eq(storageSlot, _OWNER_SLOT), eq(storageSlot, _ERC1967_IMPLEMENTATION_SLOT)) {
revert(codesize(), 0x00)
}
}
_;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* DEPOSIT OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Returns the account's balance on the EntryPoint.
function getDeposit() public view virtual returns (uint256 result) {
address ep = entryPoint();
/// @solidity memory-safe-assembly
assembly {
mstore(0x20, address()) // Store the `account` argument.
mstore(0x00, 0x70a08231) // `balanceOf(address)`.
result :=
mul( // Returns 0 if the EntryPoint does not exist.
mload(0x20),
and( // The arguments of `and` are evaluated from right to left.
gt(returndatasize(), 0x1f), // At least 32 bytes returned.
staticcall(gas(), ep, 0x1c, 0x24, 0x20, 0x20)
)
)
}
}
/// @dev Deposit more funds for this account in the EntryPoint.
function addDeposit() public payable virtual {
address ep = entryPoint();
/// @solidity memory-safe-assembly
assembly {
// Call `depositTo(address)` instead of `receive()` for better standard compliance.
mstore(0x00, 0xb760faf9) // `depositTo(address)`.
mstore(0x20, address()) // `account`.
// forgefmt: disable-next-item
if iszero(mul(extcodesize(ep), call(gas(), ep, callvalue(), 0x1c, 0x24, codesize(), 0x00))) {
revert(codesize(), 0x00) // For gas estimation.
}
}
}
/// @dev Withdraw ETH from the account's deposit on the EntryPoint.
function withdrawDepositTo(address to, uint256 amount) public payable virtual onlyOwner {
address ep = entryPoint();
/// @solidity memory-safe-assembly
assembly {
mstore(0x14, to) // Store the `to` argument.
mstore(0x34, amount) // Store the `amount` argument.
mstore(0x00, 0x205c2878000000000000000000000000) // `withdrawTo(address,uint256)`.
if iszero(mul(extcodesize(ep), call(gas(), ep, 0, 0x10, 0x44, codesize(), 0x00))) {
returndatacopy(mload(0x40), 0x00, returndatasize())
revert(mload(0x40), returndatasize())
}
mstore(0x34, 0) // Restore the part of the free memory pointer that was overwritten.
}
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* OVERRIDES */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Requires that the caller is the owner or the account itself.
/// This override affects the `onlyOwner` modifier.
function _checkOwner() internal view virtual override(Ownable) {
if (msg.sender != owner()) if (msg.sender != address(this)) revert Unauthorized();
}
/// @dev To prevent double-initialization (reuses the owner storage slot for efficiency).
function _guardInitializeOwner() internal pure virtual override(Ownable) returns (bool) {
return true;
}
/// @dev Uses the `owner` as the ERC1271 signer.
function _erc1271Signer() internal view virtual override(ERC1271) returns (address) {
return owner();
}
/// @dev Allow the entry point to skip the ERC7739 nested typed data workflow.
/// This is safe as the entry point already includes the smart account in the user op digest.
function _erc1271CallerIsSafe() internal view virtual override(ERC1271) returns (bool) {
return msg.sender == entryPoint() || ERC1271._erc1271CallerIsSafe();
}
/// @dev To ensure that only the owner or the account itself can upgrade the implementation.
function _authorizeUpgrade(address) internal virtual override(UUPSUpgradeable) onlyOwner {}
/// @dev If you don't need to use `LibZip.cdFallback`, override this function to return false.
function _useLibZipCdFallback() internal view virtual returns (bool) {
return true;
}
/// @dev Handle token callbacks. If no token callback is triggered,
/// use `LibZip.cdFallback` for generalized calldata decompression.
fallback() external payable virtual override(Receiver) receiverFallback {
if (_useLibZipCdFallback()) {
// Reverts with out-of-gas by recursing infinitely if the first 4 bytes
// of the decompressed `msg.data` doesn't match any function selector.
LibZip.cdFallback();
} else {
revert FnSelectorNotRecognized();
}
}
}