Skip to content

Commit bd8b010

Browse files
authored
Use new Entrypoint v0.6 setup with account + account factory contracts (#373)
* Make all top level functions virtual on ContractKit bases * pkg update * Make MAX_BPS private in Marketplace plugins * Add sample AA implementation * Add design objectives for TWAccount * Update TWAccountFactory * Emit event on Account creation * Update TWAccount * Fix compile errors * WIP: TWRouter has everything * docs update * delete dummy contract * Two separate wallets: regular and dynamic * Create TWDynamicAccount factory * WIP: TWAccount benchmark tests * Upgrade to Entrypoint 0.6.0 and update its dependents * Update UserOperation lib util * fix benchmark tests for TWAccount * cleanup test events * Update createAccount: return instead of revert for used salt * Fix TWDynamicAccount initialize * Add receive function to TWAccount * Update account contracts using new entrypoint * fix naming in test file * fix solhint build error * fix test * Fix casing * fix casing again
1 parent dd6dc1b commit bd8b010

File tree

11 files changed

+315
-217
lines changed

11 files changed

+315
-217
lines changed

contracts/smart-wallet/interfaces/IEntrypoint.sol

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ pragma solidity ^0.8.12;
1212
import "../utils/UserOperation.sol";
1313
import "./IStakeManager.sol";
1414
import "./IAggregator.sol";
15+
import "./INonceManager.sol";
1516

16-
interface IEntryPoint is IStakeManager {
17+
interface IEntryPoint is IStakeManager, INonceManager {
1718
/***
1819
* An event emitted after each successful request
1920
* @param userOpHash - unique identifier for the request (hash its entire content, except signature).
@@ -57,6 +58,12 @@ interface IEntryPoint is IStakeManager {
5758
bytes revertReason
5859
);
5960

61+
/**
62+
* an event emitted by handleOps(), before starting the execution loop.
63+
* any event emitted before this event, is part of the validation.
64+
*/
65+
event BeforeExecution();
66+
6067
/**
6168
* signature aggregator used by the following UserOperationEvents within this bundle.
6269
*/
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity ^0.8.12;
3+
4+
interface INonceManager {
5+
/**
6+
* Return the next nonce for this sender.
7+
* Within a given key, the nonce values are sequenced (starting with zero, and incremented by one on each userop)
8+
* But UserOp with different keys can come with arbitrary order.
9+
*
10+
* @param sender the account address
11+
* @param key the high 192 bit of the nonce
12+
* @return nonce a full nonce to pass for next UserOp with this sender.
13+
*/
14+
function getNonce(address sender, uint192 key) external view returns (uint256 nonce);
15+
16+
/**
17+
* Manually increment the nonce of the sender.
18+
* This method is exposed just for completeness..
19+
* Account does NOT need to call it, neither during validation, nor elsewhere,
20+
* as the EntryPoint will update the nonce regardless.
21+
* Possible use-case is call it with various keys to "initialize" their nonces to one, so that future
22+
* UserOperations will not pay extra for the first transaction with a given key.
23+
*/
24+
function incrementNonce(uint192 key) external;
25+
}

contracts/smart-wallet/managed/AccountCore.sol

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,6 @@ import "../../dynamic-contracts/extension/PermissionsEnumerable.sol";
2525
// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ |
2626
// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/
2727

28-
/*///////////////////////////////////////////////////////////////
29-
Storage layout
30-
//////////////////////////////////////////////////////////////*/
31-
32-
library AccountStorage {
33-
bytes32 internal constant ACCOUNT_STORAGE_POSITION = keccak256("account.storage");
34-
35-
struct Data {
36-
uint256 nonce;
37-
}
38-
39-
function accountStorage() internal pure returns (Data storage accountData) {
40-
bytes32 position = ACCOUNT_STORAGE_POSITION;
41-
assembly {
42-
accountData.slot := position
43-
}
44-
}
45-
}
46-
4728
contract AccountCore is Initializable, Multicall, BaseAccount {
4829
using ECDSA for bytes32;
4930

@@ -77,12 +58,6 @@ contract AccountCore is Initializable, Multicall, BaseAccount {
7758
View functions
7859
//////////////////////////////////////////////////////////////*/
7960

80-
/// @notice Returns the nonce of the account.
81-
function nonce() public view virtual override returns (uint256) {
82-
AccountStorage.Data storage accountData = AccountStorage.accountStorage();
83-
return accountData.nonce;
84-
}
85-
8661
/// @notice Returns the EIP 4337 entrypoint contract.
8762
function entryPoint() public view virtual override returns (IEntryPoint) {
8863
return entrypointContract;
@@ -117,14 +92,6 @@ contract AccountCore is Initializable, Multicall, BaseAccount {
11792
Internal functions
11893
//////////////////////////////////////////////////////////////*/
11994

120-
/// @dev Validates the nonce of a user operation and updates account nonce.
121-
function _validateAndUpdateNonce(UserOperation calldata userOp) internal override {
122-
AccountStorage.Data storage data = AccountStorage.accountStorage();
123-
require(data.nonce == userOp.nonce, "Account: invalid nonce");
124-
125-
data.nonce += 1;
126-
}
127-
12895
/// @notice Validates the signature of a user operation.
12996
function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash)
13097
internal

contracts/smart-wallet/non-upgradeable/Account.sol

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,6 @@ import "../../openzeppelin-presets/utils/cryptography/ECDSA.sol";
2828
// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ |
2929
// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/
3030

31-
/*///////////////////////////////////////////////////////////////
32-
Storage layout
33-
//////////////////////////////////////////////////////////////*/
34-
35-
library AccountStorage {
36-
bytes32 internal constant ACCOUNT_STORAGE_POSITION = keccak256("account.storage");
37-
38-
struct Data {
39-
uint256 nonce;
40-
}
41-
42-
function accountStorage() internal pure returns (Data storage accountData) {
43-
bytes32 position = ACCOUNT_STORAGE_POSITION;
44-
assembly {
45-
accountData.slot := position
46-
}
47-
}
48-
}
49-
5031
contract Account is
5132
Initializable,
5233
Multicall,
@@ -104,12 +85,6 @@ contract Account is
10485
super.supportsInterface(interfaceId);
10586
}
10687

107-
/// @notice Returns the nonce of the account.
108-
function nonce() public view virtual override returns (uint256) {
109-
AccountStorage.Data storage accountData = AccountStorage.accountStorage();
110-
return accountData.nonce;
111-
}
112-
11388
/// @notice Returns the EIP 4337 entrypoint contract.
11489
function entryPoint() public view virtual override returns (IEntryPoint) {
11590
return entrypointContract;
@@ -178,14 +153,6 @@ contract Account is
178153
}
179154
}
180155

181-
/// @dev Validates the nonce of a user operation and updates account nonce.
182-
function _validateAndUpdateNonce(UserOperation calldata userOp) internal override {
183-
AccountStorage.Data storage data = AccountStorage.accountStorage();
184-
require(data.nonce == userOp.nonce, "Account: invalid nonce");
185-
186-
data.nonce += 1;
187-
}
188-
189156
/// @notice Validates the signature of a user operation.
190157
function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash)
191158
internal

contracts/smart-wallet/utils/BaseAccount.sol

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
pragma solidity ^0.8.12;
33

44
/* solhint-disable avoid-low-level-calls */
5-
/* solhint-disable no-inline-assembly */
6-
/* solhint-disable reason-string */
5+
/* solhint-disable no-empty-blocks */
76

87
import "../interfaces/IAccount.sol";
98
import "../interfaces/IEntrypoint.sol";
9+
import "./Helpers.sol";
1010

1111
/**
1212
* Basic account implementation.
@@ -21,10 +21,13 @@ abstract contract BaseAccount is IAccount {
2121
uint256 internal constant SIG_VALIDATION_FAILED = 1;
2222

2323
/**
24-
* return the account nonce.
25-
* subclass should return a nonce value that is used both by _validateAndUpdateNonce, and by the external provider (to read the current nonce)
24+
* Return the account nonce.
25+
* This method returns the next sequential nonce.
26+
* For a nonce of a specific key, use `entrypoint.getNonce(account, key)`
2627
*/
27-
function nonce() public view virtual returns (uint256);
28+
function getNonce() public view virtual returns (uint256) {
29+
return entryPoint().getNonce(address(this), 0);
30+
}
2831

2932
/**
3033
* return the entryPoint used by this account.
@@ -43,9 +46,7 @@ abstract contract BaseAccount is IAccount {
4346
) external virtual override returns (uint256 validationData) {
4447
_requireFromEntryPoint();
4548
validationData = _validateSignature(userOp, userOpHash);
46-
if (userOp.initCode.length == 0) {
47-
_validateAndUpdateNonce(userOp);
48-
}
49+
_validateNonce(userOp.nonce);
4950
_payPrefund(missingAccountFunds);
5051
}
5152

@@ -75,12 +76,22 @@ abstract contract BaseAccount is IAccount {
7576
returns (uint256 validationData);
7677

7778
/**
78-
* validate the current nonce matches the UserOperation nonce.
79-
* then it should update the account's state to prevent replay of this UserOperation.
80-
* called only if initCode is empty (since "nonce" field is used as "salt" on account creation)
81-
* @param userOp the op to validate.
79+
* Validate the nonce of the UserOperation.
80+
* This method may validate the nonce requirement of this account.
81+
* e.g.
82+
* To limit the nonce to use sequenced UserOps only (no "out of order" UserOps):
83+
* `require(nonce < type(uint64).max)`
84+
* For a hypothetical account that *requires* the nonce to be out-of-order:
85+
* `require(nonce & type(uint64).max == 0)`
86+
*
87+
* The actual nonce uniqueness is managed by the EntryPoint, and thus no other
88+
* action is needed by the account itself.
89+
*
90+
* @param nonce to validate
91+
*
92+
* solhint-disable-next-line no-empty-blocks
8293
*/
83-
function _validateAndUpdateNonce(UserOperation calldata userOp) internal virtual;
94+
function _validateNonce(uint256 nonce) internal view virtual {}
8495

8596
/**
8697
* sends to the entrypoint (msg.sender) the missing funds for this transaction.

contracts/smart-wallet/utils/Entrypoint.sol

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ import "../interfaces/IAccount.sol";
1212
import "../interfaces/IPaymaster.sol";
1313
import "../interfaces/IEntrypoint.sol";
1414

15-
import "./Exec.sol";
15+
import ".//Exec.sol";
1616
import "./StakeManager.sol";
1717
import "./SenderCreator.sol";
1818
import "./Helpers.sol";
19+
import "./NonceManager.sol";
20+
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
1921

20-
contract EntryPoint is IEntryPoint, StakeManager {
22+
contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard {
2123
using UserOperationLib for UserOperation;
2224

2325
SenderCreator private immutable senderCreator = new SenderCreator();
@@ -90,7 +92,7 @@ contract EntryPoint is IEntryPoint, StakeManager {
9092
* @param ops the operations to execute
9193
* @param beneficiary the address to receive the fees
9294
*/
93-
function handleOps(UserOperation[] calldata ops, address payable beneficiary) public {
95+
function handleOps(UserOperation[] calldata ops, address payable beneficiary) public nonReentrant {
9496
uint256 opslen = ops.length;
9597
UserOpInfo[] memory opInfos = new UserOpInfo[](opslen);
9698

@@ -102,6 +104,7 @@ contract EntryPoint is IEntryPoint, StakeManager {
102104
}
103105

104106
uint256 collected = 0;
107+
emit BeforeExecution();
105108

106109
for (uint256 i = 0; i < opslen; i++) {
107110
collected += _executeUserOp(i, ops[i], opInfos[i]);
@@ -116,7 +119,10 @@ contract EntryPoint is IEntryPoint, StakeManager {
116119
* @param opsPerAggregator the operations to execute, grouped by aggregator (or address(0) for no-aggregator accounts)
117120
* @param beneficiary the address to receive the fees
118121
*/
119-
function handleAggregatedOps(UserOpsPerAggregator[] calldata opsPerAggregator, address payable beneficiary) public {
122+
function handleAggregatedOps(UserOpsPerAggregator[] calldata opsPerAggregator, address payable beneficiary)
123+
public
124+
nonReentrant
125+
{
120126
uint256 opasLen = opsPerAggregator.length;
121127
uint256 totalOps = 0;
122128
for (uint256 i = 0; i < opasLen; i++) {
@@ -139,6 +145,8 @@ contract EntryPoint is IEntryPoint, StakeManager {
139145

140146
UserOpInfo[] memory opInfos = new UserOpInfo[](totalOps);
141147

148+
emit BeforeExecution();
149+
142150
uint256 opIndex = 0;
143151
for (uint256 a = 0; a < opasLen; a++) {
144152
UserOpsPerAggregator calldata opa = opsPerAggregator[a];
@@ -373,7 +381,8 @@ contract EntryPoint is IEntryPoint, StakeManager {
373381
* @param initCode the constructor code to be passed into the UserOperation.
374382
*/
375383
function getSenderAddress(bytes calldata initCode) public {
376-
revert SenderAddressResult(senderCreator.createSender(initCode));
384+
address sender = senderCreator.createSender(initCode);
385+
revert SenderAddressResult(sender);
377386
}
378387

379388
function _simulationOnlyValidations(UserOperation calldata userOp) internal view {
@@ -416,9 +425,6 @@ contract EntryPoint is IEntryPoint, StakeManager {
416425
* revert (with FailedOp) in case validateUserOp reverts, or account didn't send required prefund.
417426
* decrement account's deposit if needed
418427
*/
419-
420-
event OpHash(bytes32 hashVal);
421-
422428
function _validateAccountPrepayment(
423429
uint256 opIndex,
424430
UserOperation calldata op,
@@ -437,7 +443,6 @@ contract EntryPoint is IEntryPoint, StakeManager {
437443
uint256 bal = balanceOf(sender);
438444
missingAccountFunds = bal > requiredPrefund ? 0 : requiredPrefund - bal;
439445
}
440-
emit OpHash(opInfo.userOpHash);
441446
try
442447
IAccount(sender).validateUserOp{ gas: mUserOp.verificationGasLimit }(
443448
op,
@@ -579,6 +584,11 @@ contract EntryPoint is IEntryPoint, StakeManager {
579584
outOpInfo,
580585
requiredPreFund
581586
);
587+
588+
if (!_validateAndUpdateNonce(mUserOp.sender, mUserOp.nonce)) {
589+
revert FailedOp(opIndex, "AA25 invalid account nonce");
590+
}
591+
582592
//a "marker" where account opcode validation is done and paymaster opcode validation is about to start
583593
// (used only by off-chain simulateValidation)
584594
numberMarker();

contracts/smart-wallet/utils/Helpers.sol

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: GPL-3.0
22
pragma solidity ^0.8.12;
33

4+
/* solhint-disable no-inline-assembly */
45
/* solhint-disable func-visibility */
56

67
/**
@@ -72,3 +73,16 @@ function _packValidationData(
7273
) pure returns (uint256) {
7374
return (sigFailed ? 1 : 0) | (uint256(validUntil) << 160) | (uint256(validAfter) << (160 + 48));
7475
}
76+
77+
/**
78+
* keccak function over calldata.
79+
* @dev copy calldata into memory, do keccak and drop allocated memory. Strangely, this is more efficient than letting solidity do it.
80+
*/
81+
function calldataKeccak(bytes calldata data) pure returns (bytes32 ret) {
82+
assembly {
83+
let mem := mload(0x40)
84+
let len := data.length
85+
calldatacopy(mem, data.offset, len)
86+
ret := keccak256(mem, len)
87+
}
88+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity ^0.8.12;
3+
4+
import "../interfaces/IEntrypoint.sol";
5+
6+
/**
7+
* nonce management functionality
8+
*/
9+
contract NonceManager is INonceManager {
10+
/**
11+
* The next valid sequence number for a given nonce key.
12+
*/
13+
mapping(address => mapping(uint192 => uint256)) public nonceSequenceNumber;
14+
15+
function getNonce(address sender, uint192 key) public view override returns (uint256 nonce) {
16+
return nonceSequenceNumber[sender][key] | (uint256(key) << 64);
17+
}
18+
19+
// allow an account to manually increment its own nonce.
20+
// (mainly so that during construction nonce can be made non-zero,
21+
// to "absorb" the gas cost of first nonce increment to 1st transaction (construction),
22+
// not to 2nd transaction)
23+
function incrementNonce(uint192 key) public override {
24+
nonceSequenceNumber[msg.sender][key]++;
25+
}
26+
27+
/**
28+
* validate nonce uniqueness for this account.
29+
* called just after validateUserOp()
30+
*/
31+
function _validateAndUpdateNonce(address sender, uint256 nonce) internal returns (bool) {
32+
uint192 key = uint192(nonce >> 64);
33+
uint64 seq = uint64(nonce);
34+
return nonceSequenceNumber[sender][key]++ == seq;
35+
}
36+
}

0 commit comments

Comments
 (0)