mirror of
https://github.com/getwax/zk-account-abstraction.git
synced 2026-01-08 04:03:59 -05:00
AA-161 validate Nonce in EntryPoint (#247)
* EntryPoint to manage Nonce - NonceManager to handle 2d nonces - _validateAndUpdateNonce() called after validateUserOp - getNonce(sender, key) - remove nonce checking from BaseAccount - BaseAccount implements getNonce() using ep.getNonce
This commit is contained in:
@@ -2,8 +2,7 @@
|
||||
pragma solidity ^0.8.12;
|
||||
|
||||
/* solhint-disable avoid-low-level-calls */
|
||||
/* solhint-disable no-inline-assembly */
|
||||
/* solhint-disable reason-string */
|
||||
/* solhint-disable no-empty-blocks */
|
||||
|
||||
import "../interfaces/IAccount.sol";
|
||||
import "../interfaces/IEntryPoint.sol";
|
||||
@@ -22,10 +21,13 @@ abstract contract BaseAccount is IAccount {
|
||||
uint256 constant internal SIG_VALIDATION_FAILED = 1;
|
||||
|
||||
/**
|
||||
* return the account nonce.
|
||||
* subclass should return a nonce value that is used both by _validateAndUpdateNonce, and by the external provider (to read the current nonce)
|
||||
* Return the account nonce.
|
||||
* This method returns the next sequential nonce.
|
||||
* For a nonce of a specific key, use `entrypoint.getNonce(account, key)`
|
||||
*/
|
||||
function nonce() public view virtual returns (uint256);
|
||||
function getNonce() public view virtual returns (uint256) {
|
||||
return entryPoint().getNonce(address(this), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* return the entryPoint used by this account.
|
||||
@@ -41,9 +43,7 @@ abstract contract BaseAccount is IAccount {
|
||||
external override virtual returns (uint256 validationData) {
|
||||
_requireFromEntryPoint();
|
||||
validationData = _validateSignature(userOp, userOpHash);
|
||||
if (userOp.initCode.length == 0) {
|
||||
_validateAndUpdateNonce(userOp);
|
||||
}
|
||||
_validateNonce(userOp.nonce);
|
||||
_payPrefund(missingAccountFunds);
|
||||
}
|
||||
|
||||
@@ -71,12 +71,23 @@ abstract contract BaseAccount is IAccount {
|
||||
internal virtual returns (uint256 validationData);
|
||||
|
||||
/**
|
||||
* validate the current nonce matches the UserOperation nonce.
|
||||
* then it should update the account's state to prevent replay of this UserOperation.
|
||||
* called only if initCode is empty (since "nonce" field is used as "salt" on account creation)
|
||||
* @param userOp the op to validate.
|
||||
* Validate the nonce of the UserOperation.
|
||||
* 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.
|
||||
*
|
||||
* @param nonce to validate
|
||||
*
|
||||
* solhint-disable-next-line no-empty-blocks
|
||||
*/
|
||||
function _validateAndUpdateNonce(UserOperation calldata userOp) internal virtual;
|
||||
function _validateNonce(uint256 nonce) internal view virtual {
|
||||
}
|
||||
|
||||
/**
|
||||
* sends to the entrypoint (msg.sender) the missing funds for this transaction.
|
||||
|
||||
@@ -16,8 +16,9 @@ import "../utils/Exec.sol";
|
||||
import "./StakeManager.sol";
|
||||
import "./SenderCreator.sol";
|
||||
import "./Helpers.sol";
|
||||
import "./NonceManager.sol";
|
||||
|
||||
contract EntryPoint is IEntryPoint, StakeManager {
|
||||
contract EntryPoint is IEntryPoint, StakeManager, NonceManager {
|
||||
|
||||
using UserOperationLib for UserOperation;
|
||||
|
||||
@@ -349,7 +350,8 @@ contract EntryPoint is IEntryPoint, StakeManager {
|
||||
* @param initCode the constructor code to be passed into the UserOperation.
|
||||
*/
|
||||
function getSenderAddress(bytes calldata initCode) public {
|
||||
revert SenderAddressResult(senderCreator.createSender(initCode));
|
||||
address sender = senderCreator.createSender(initCode);
|
||||
revert SenderAddressResult(sender);
|
||||
}
|
||||
|
||||
function _simulationOnlyValidations(UserOperation calldata userOp) internal view {
|
||||
@@ -511,6 +513,11 @@ contract EntryPoint is IEntryPoint, StakeManager {
|
||||
uint256 gasUsedByValidateAccountPrepayment;
|
||||
(uint256 requiredPreFund) = _getRequiredPrefund(mUserOp);
|
||||
(gasUsedByValidateAccountPrepayment, validationData) = _validateAccountPrepayment(opIndex, userOp, outOpInfo, requiredPreFund);
|
||||
|
||||
if (!_validateAndUpdateNonce(mUserOp.sender, mUserOp.nonce)) {
|
||||
revert FailedOp(opIndex, "AA25 invalid account nonce");
|
||||
}
|
||||
|
||||
//a "marker" where account opcode validation is done and paymaster opcode validation is about to start
|
||||
// (used only by off-chain simulateValidation)
|
||||
numberMarker();
|
||||
|
||||
40
contracts/core/NonceManager.sol
Normal file
40
contracts/core/NonceManager.sol
Normal file
@@ -0,0 +1,40 @@
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
pragma solidity ^0.8.12;
|
||||
|
||||
import "../interfaces/IEntryPoint.sol";
|
||||
|
||||
/**
|
||||
* nonce management functionality
|
||||
*/
|
||||
contract NonceManager is INonceManager {
|
||||
|
||||
/**
|
||||
* The next valid sequence number for a given nonce key.
|
||||
*/
|
||||
mapping(address => mapping(uint192 => uint256)) public nonceSequenceNumber;
|
||||
|
||||
function getNonce(address sender, uint192 key)
|
||||
public view override returns (uint256 nonce) {
|
||||
return nonceSequenceNumber[sender][key] | (uint256(key) << 64);
|
||||
}
|
||||
|
||||
// allow an account to manually increment its own nonce.
|
||||
// (mainly so that during construction nonce can be made non-zero,
|
||||
// to "absorb" the gas cost of first nonce increment to 1st transaction (construction),
|
||||
// not to 2nd transaction)
|
||||
function incrementNonce(uint192 key) public override {
|
||||
nonceSequenceNumber[msg.sender][key]++;
|
||||
}
|
||||
|
||||
/**
|
||||
* validate nonce uniqueness for this account.
|
||||
* called just after validateUserOp()
|
||||
*/
|
||||
function _validateAndUpdateNonce(address sender, uint256 nonce) internal returns (bool) {
|
||||
|
||||
uint192 key = uint192(nonce >> 64);
|
||||
uint64 seq = uint64(nonce);
|
||||
return nonceSequenceNumber[sender][key]++ == seq;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,8 +12,9 @@ pragma solidity ^0.8.12;
|
||||
import "./UserOperation.sol";
|
||||
import "./IStakeManager.sol";
|
||||
import "./IAggregator.sol";
|
||||
import "./INonceManager.sol";
|
||||
|
||||
interface IEntryPoint is IStakeManager {
|
||||
interface IEntryPoint is IStakeManager, INonceManager {
|
||||
|
||||
/***
|
||||
* An event emitted after each successful request
|
||||
|
||||
27
contracts/interfaces/INonceManager.sol
Normal file
27
contracts/interfaces/INonceManager.sol
Normal file
@@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
pragma solidity ^0.8.12;
|
||||
|
||||
interface INonceManager {
|
||||
|
||||
/**
|
||||
* Return the next nonce for this sender.
|
||||
* Within a given key, the nonce values are sequenced (starting with zero, and incremented by one on each userop)
|
||||
* But UserOp with different keys can come with arbitrary order.
|
||||
*
|
||||
* @param sender the account address
|
||||
* @param key the high 192 bit of the nonce
|
||||
* @return nonce a full nonce to pass for next UserOp with this sender.
|
||||
*/
|
||||
function getNonce(address sender, uint192 key)
|
||||
external view returns (uint256 nonce);
|
||||
|
||||
/**
|
||||
* Manually increment the nonce of the sender.
|
||||
* This method is exposed just for completeness..
|
||||
* Account does NOT need to call it, neither during validation, nor elsewhere,
|
||||
* as the EntryPoint will update the nonce regardless.
|
||||
* Possible use-case is call it with various keys to "initialize" their nonces to one, so that future
|
||||
* UserOperations will not pay extra for the first transaction with a given key.
|
||||
*/
|
||||
function incrementNonce(uint192 key) external;
|
||||
}
|
||||
@@ -21,12 +21,6 @@ import "./callback/TokenCallbackHandler.sol";
|
||||
contract SimpleAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, Initializable {
|
||||
using ECDSA for bytes32;
|
||||
|
||||
//filler member, to push the nonce and owner to the same slot
|
||||
// the "Initializeble" class takes 2 bytes in the first slot
|
||||
bytes28 private _filler;
|
||||
|
||||
//explicit sizes of nonce, to fit a single storage cell with "owner"
|
||||
uint96 private _nonce;
|
||||
address public owner;
|
||||
|
||||
IEntryPoint private immutable _entryPoint;
|
||||
@@ -38,11 +32,6 @@ contract SimpleAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, In
|
||||
_;
|
||||
}
|
||||
|
||||
/// @inheritdoc BaseAccount
|
||||
function nonce() public view virtual override returns (uint256) {
|
||||
return _nonce;
|
||||
}
|
||||
|
||||
/// @inheritdoc BaseAccount
|
||||
function entryPoint() public view virtual override returns (IEntryPoint) {
|
||||
return _entryPoint;
|
||||
@@ -100,11 +89,6 @@ contract SimpleAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, In
|
||||
require(msg.sender == address(entryPoint()) || msg.sender == owner, "account: not Owner or EntryPoint");
|
||||
}
|
||||
|
||||
/// implement template method of BaseAccount
|
||||
function _validateAndUpdateNonce(UserOperation calldata userOp) internal override {
|
||||
require(_nonce++ == userOp.nonce, "account: invalid nonce");
|
||||
}
|
||||
|
||||
/// implement template method of BaseAccount
|
||||
function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash)
|
||||
internal override virtual returns (uint256 validationData) {
|
||||
|
||||
@@ -50,6 +50,14 @@ contract EIP4337Fallback is DefaultCallbackHandler, IAccount, IERC1271 {
|
||||
return abi.decode(ret, (uint256));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for wallet to get the next nonce.
|
||||
*/
|
||||
function getNonce() public returns (uint256 nonce) {
|
||||
bytes memory ret = delegateToManager();
|
||||
(nonce) = abi.decode(ret, (uint256));
|
||||
}
|
||||
|
||||
/**
|
||||
* called from the Safe. delegate actual work to EIP4337Manager
|
||||
*/
|
||||
@@ -78,4 +86,4 @@ contract EIP4337Fallback is DefaultCallbackHandler, IAccount, IERC1271 {
|
||||
return 0xffffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +55,8 @@ contract EIP4337Manager is IAccount, GnosisSafeStorage, Executor {
|
||||
validationData = SIG_VALIDATION_FAILED;
|
||||
}
|
||||
|
||||
if (userOp.initCode.length == 0) {
|
||||
require(uint256(nonce) == userOp.nonce, "account: invalid nonce");
|
||||
nonce = bytes32(uint256(nonce) + 1);
|
||||
}
|
||||
// mimic normal Safe nonce behaviour: prevent parallel nonces
|
||||
require(userOp.nonce < type(uint64).max, "account: nonsequential nonce");
|
||||
|
||||
if (missingAccountFunds > 0) {
|
||||
//Note: MAY pay more than the minimum, to deposit for future transactions
|
||||
@@ -105,6 +103,12 @@ contract EIP4337Manager is IAccount, GnosisSafeStorage, Executor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for wallet to get the next nonce.
|
||||
*/
|
||||
function getNonce() public view returns (uint256) {
|
||||
return IEntryPoint(entryPoint).getNonce(address(this), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* set up a safe as EIP-4337 enabled.
|
||||
@@ -158,7 +162,8 @@ contract EIP4337Manager is IAccount, GnosisSafeStorage, Executor {
|
||||
sig[64] = bytes1(uint8(27));
|
||||
sig[2] = bytes1(uint8(1));
|
||||
sig[35] = bytes1(uint8(1));
|
||||
UserOperation memory userOp = UserOperation(address(safe), uint256(nonce), "", "", 0, 1000000, 0, 0, 0, "", sig);
|
||||
uint256 nonce = uint256(IEntryPoint(manager.entryPoint()).getNonce(address(safe), 0));
|
||||
UserOperation memory userOp = UserOperation(address(safe), nonce, "", "", 0, 1000000, 0, 0, 0, "", sig);
|
||||
UserOperation[] memory userOps = new UserOperation[](1);
|
||||
userOps[0] = userOp;
|
||||
IEntryPoint _entryPoint = IEntryPoint(payable(manager.entryPoint()));
|
||||
|
||||
@@ -12,11 +12,12 @@ contract MaliciousAccount is IAccount {
|
||||
function validateUserOp(UserOperation calldata userOp, bytes32, uint256 missingAccountFunds)
|
||||
external returns (uint256 validationData) {
|
||||
ep.depositTo{value : missingAccountFunds}(address(this));
|
||||
// Now calculate basefee per EntryPoint.getUserOpGasPrice() and compare it to the basefe we pass off-chain as nonce
|
||||
// Now calculate basefee per EntryPoint.getUserOpGasPrice() and compare it to the basefe we pass off-chain in the signature
|
||||
uint256 externalBaseFee = abi.decode(userOp.signature, (uint256));
|
||||
uint256 requiredGas = userOp.callGasLimit + userOp.verificationGasLimit + userOp.preVerificationGas;
|
||||
uint256 gasPrice = missingAccountFunds / requiredGas;
|
||||
uint256 basefee = gasPrice - userOp.maxPriorityFeePerGas;
|
||||
require (basefee == userOp.nonce, "Revert after first validation");
|
||||
require (basefee == externalBaseFee, "Revert after first validation");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ const deploySimpleAccountFactory: DeployFunction = async function (hre: HardhatR
|
||||
from,
|
||||
args: [entrypoint.address],
|
||||
gasLimit: 6e6,
|
||||
log: true,
|
||||
deterministicDeployment: true
|
||||
})
|
||||
console.log('==SimpleAccountFactory addr=', ret.address)
|
||||
|
||||
@@ -51,7 +51,7 @@ To avoid Ethereum consensus changes, we do not attempt to create new transaction
|
||||
| Field | Type | Description
|
||||
| - | - | - |
|
||||
| `sender` | `address` | The account making the operation |
|
||||
| `nonce` | `uint256` | Anti-replay parameter; also used as the salt for first-time account creation |
|
||||
| `nonce` | `uint256` | Anti-replay parameter |
|
||||
| `initCode` | `bytes` | The initCode of the account (needed if and only if the account is not yet on-chain and needs to be created) |
|
||||
| `callData` | `bytes` | The data to pass to the `sender` during the main execution call |
|
||||
| `callGasLimit` | `uint256` | The amount of gas to allocate the main execution call |
|
||||
@@ -128,6 +128,7 @@ The account:
|
||||
* MUST validate the caller is a trusted EntryPoint
|
||||
* If the account does not support signature aggregation, it MUST validate the signature is a valid signature of the `userOpHash`, and
|
||||
SHOULD return SIG_VALIDATION_FAILED (and not revert) on signature mismatch. Any other error should revert.
|
||||
* The MAY check the nonce field, but should not implement the replay protection mechanism: the EntryPoint maintains uniqueness of nonces per user account.
|
||||
* MUST pay the entryPoint (caller) at least the "missingAccountFunds" (which might be zero, in case current account's deposit is high enough)
|
||||
* The account MAY pay more than this minimum, to cover future transactions (it can always issue `withdrawTo` to retrieve it)
|
||||
* The return value MUST be packed of `authorizer`, `validUntil` and `validAfter` timestamps.
|
||||
@@ -182,6 +183,7 @@ The entry point's `handleOps` function must perform the following steps (we firs
|
||||
* **Create the account if it does not yet exist**, using the initcode provided in the `UserOperation`. If the account does not exist, _and_ the initcode is empty, or does not deploy a contract at the "sender" address, the call must fail.
|
||||
* **Call `validateUserOp` on the account**, passing in the `UserOperation`, the required fee and aggregator (if there is one). The account should verify the operation's signature, and pay the fee if the account considers the operation valid. If any `validateUserOp` call fails, `handleOps` must skip execution of at least that operation, and may revert entirely.
|
||||
* Validate the account's deposit in the entryPoint is high enough to cover the max possible cost (cover the already-done verification and max execution gas)
|
||||
* Validate the nonce uniqueness. see [Keep Nonce Uniqueness](#keep-nonce-uniqueness) below
|
||||
|
||||
In the execution loop, the `handleOps` call must perform the following steps for each `UserOperation`:
|
||||
|
||||
@@ -192,6 +194,17 @@ In the execution loop, the `handleOps` call must perform the following steps for
|
||||
Before accepting a `UserOperation`, bundlers should use an RPC method to locally call the `simulateValidation` function of the entry point, to verify that the signature is correct and the operation actually pays fees; see the [Simulation section below](#simulation) for details.
|
||||
A node/bundler SHOULD drop (not add to the mempool) a `UserOperation` that fails the validation
|
||||
|
||||
### Keep Nonce Uniqueness
|
||||
|
||||
The EntryPoint maintains nonce uniqueness for each submitted UserOperation using the following algorithm:
|
||||
* The nonce is treated as 2 separate fields:
|
||||
* 64-bit "sequence"
|
||||
* 192-bit "key"
|
||||
* Within each "key", the "sequence" value must have consecutive values, starting with zero.
|
||||
* That is, a nonce with a new "key" value is allowed, as long as the "sequence" part is zero. The next nonce for that key must be "1", and so on.
|
||||
* The EntryPoint exports a method `getNonce(address sender, uint192 key)` to return the next valid nonce for this key.
|
||||
* The behaviour of a "classic" sequential nonce can be achieved by validating that the "key" part is always zero.
|
||||
|
||||
### Extension: paymasters
|
||||
|
||||
We extend the entry point logic to support **paymasters** that can sponsor transactions for other users. This feature can be used to allow application developers to subsidize fees for their users, allow users to pay fees with [ERC-20](./eip-20.md) tokens and many other use cases. When the paymaster is not equal to the zero address, the entry point implements a different flow:
|
||||
@@ -907,7 +920,7 @@ See `https://github.com/eth-infinitism/account-abstraction/tree/main/contracts`
|
||||
|
||||
## Security Considerations
|
||||
|
||||
The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [ERC-4337](./eip-4337.md). In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _accounts_ have to do becomes much smaller (they need only verify the `validateUserOp` function and its "check signature, increment nonce and pay fees" logic) and check that other functions are `msg.sender == ENTRY_POINT` gated (perhaps also allowing `msg.sender == self`), but it is nevertheless the case that this is done precisely by concentrating security risk in the entry point contract that needs to be verified to be very robust.
|
||||
The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [EIP-4337](./eip-4337.md). In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _accounts_ have to do becomes much smaller (they need only verify the `validateUserOp` function and its "check signature, and pay fees" logic) and check that other functions are `msg.sender == ENTRY_POINT` gated (perhaps also allowing `msg.sender == self`), but it is nevertheless the case that this is done precisely by concentrating security risk in the entry point contract that needs to be verified to be very robust.
|
||||
|
||||
Verification would need to cover two primary claims (not including claims needed to protect paymasters, and claims needed to establish p2p-level DoS resistance):
|
||||
|
||||
|
||||
@@ -14,12 +14,13 @@ import {
|
||||
} from '../typechain'
|
||||
import { BigNumberish, Wallet } from 'ethers'
|
||||
import hre from 'hardhat'
|
||||
import { fillAndSign } from '../test/UserOp'
|
||||
import { fillAndSign, fillUserOp, signUserOp } from '../test/UserOp'
|
||||
import { TransactionReceipt } from '@ethersproject/abstract-provider'
|
||||
import { table, TableUserConfig } from 'table'
|
||||
import { Create2Factory } from '../src/Create2Factory'
|
||||
import * as fs from 'fs'
|
||||
import { SimpleAccountInterface } from '../typechain/contracts/samples/SimpleAccount'
|
||||
import { UserOperation } from '../test/UserOperation'
|
||||
|
||||
const gasCheckerLogFile = './reports/gas-checker.txt'
|
||||
|
||||
@@ -111,6 +112,8 @@ export class GasChecker {
|
||||
])
|
||||
}
|
||||
|
||||
createdAccounts = new Set<string>()
|
||||
|
||||
/**
|
||||
* create accounts up to this counter.
|
||||
* make sure they all have balance.
|
||||
@@ -127,11 +130,29 @@ export class GasChecker {
|
||||
console.log('factaddr', factoryAddress)
|
||||
const fact = SimpleAccountFactory__factory.connect(factoryAddress, ethersSigner)
|
||||
// create accounts
|
||||
const creationOps: UserOperation[] = []
|
||||
for (const n of range(count)) {
|
||||
const salt = n
|
||||
// const initCode = this.accountInitCode(fact, salt)
|
||||
|
||||
const addr = await fact.getAddress(this.accountOwner.address, salt)
|
||||
|
||||
if (!this.createdAccounts.has(addr)) {
|
||||
// explicit call to fillUseROp with no "entryPoint", to make sure we manually fill everything and
|
||||
// not attempt to fill from blockchain.
|
||||
const op = signUserOp(await fillUserOp({
|
||||
sender: addr,
|
||||
nonce: 0,
|
||||
callGasLimit: 30000,
|
||||
verificationGasLimit: 1000000,
|
||||
// paymasterAndData: paymaster,
|
||||
preVerificationGas: 1,
|
||||
maxFeePerGas: 0
|
||||
}), this.accountOwner, this.entryPoint().address, await provider.getNetwork().then(net => net.chainId))
|
||||
creationOps.push(op)
|
||||
this.createdAccounts.add(addr)
|
||||
}
|
||||
|
||||
this.accounts[addr] = this.accountOwner
|
||||
// deploy if not already deployed.
|
||||
await fact.createAccount(this.accountOwner.address, salt)
|
||||
@@ -140,6 +161,7 @@ export class GasChecker {
|
||||
await GasCheckCollector.inst.entryPoint.depositTo(addr, { value: minDepositOrBalance.mul(5) })
|
||||
}
|
||||
}
|
||||
await this.entryPoint().handleOps(creationOps, ethersSigner.getAddress())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,7 +260,9 @@ export class GasChecker {
|
||||
title: info.title
|
||||
// receipt: rcpt
|
||||
}
|
||||
if (info.diffLastGas) { ret1.gasDiff = gasDiff }
|
||||
if (info.diffLastGas) {
|
||||
ret1.gasDiff = gasDiff
|
||||
}
|
||||
console.debug(ret1)
|
||||
return ret1
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
the destination is "account.entryPoint()", which is known to be "hot" address used by this account
|
||||
it little higher than EOA call: its an exec from entrypoint (or account owner) into account contract, verifying msg.sender and exec to target)
|
||||
╔══════════════════════════╤════════╗
|
||||
║ gas estimate "simple" │ 28992 ║
|
||||
║ gas estimate "simple" │ 29014 ║
|
||||
╟──────────────────────────┼────────╢
|
||||
║ gas estimate "big tx 5k" │ 125237 ║
|
||||
║ gas estimate "big tx 5k" │ 125260 ║
|
||||
╚══════════════════════════╧════════╝
|
||||
|
||||
╔════════════════════════════════╤═══════╤═══════════════╤════════════════╤═════════════════════╗
|
||||
@@ -12,28 +12,28 @@
|
||||
║ │ │ │ (delta for │ (compared to ║
|
||||
║ │ │ │ one UserOp) │ account.exec()) ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ simple │ 1 │ 76822 │ │ ║
|
||||
║ simple │ 1 │ 78743 │ │ ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ simple - diff from previous │ 2 │ │ 42237 │ 13245 ║
|
||||
║ simple - diff from previous │ 2 │ │ 44162 │ 15148 ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ simple │ 10 │ 457444 │ │ ║
|
||||
║ simple │ 10 │ 476282 │ │ ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ simple - diff from previous │ 11 │ │ 42465 │ 13473 ║
|
||||
║ simple - diff from previous │ 11 │ │ 44174 │ 15160 ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ simple paymaster │ 1 │ 83105 │ │ ║
|
||||
║ simple paymaster │ 1 │ 85002 │ │ ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ simple paymaster with diff │ 2 │ │ 41274 │ 12282 ║
|
||||
║ simple paymaster with diff │ 2 │ │ 43139 │ 14125 ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ simple paymaster │ 10 │ 454764 │ │ ║
|
||||
║ simple paymaster │ 10 │ 473554 │ │ ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ simple paymaster with diff │ 11 │ │ 41429 │ 12437 ║
|
||||
║ simple paymaster with diff │ 11 │ │ 43150 │ 14136 ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ big tx 5k │ 1 │ 177879 │ │ ║
|
||||
║ big tx 5k │ 1 │ 179788 │ │ ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ big tx - diff from previous │ 2 │ │ 142808 │ 17571 ║
|
||||
║ big tx - diff from previous │ 2 │ │ 144673 │ 19413 ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ big tx 5k │ 10 │ 1463196 │ │ ║
|
||||
║ big tx 5k │ 10 │ 1481914 │ │ ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ big tx - diff from previous │ 11 │ │ 142893 │ 17656 ║
|
||||
║ big tx - diff from previous │ 11 │ │ 144698 │ 19438 ║
|
||||
╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ export function packUserOp1 (op: UserOperation): string {
|
||||
'bytes32', // initCode
|
||||
'bytes32', // callData
|
||||
'uint256', // callGasLimit
|
||||
'uint', // verificationGasLimit
|
||||
'uint', // preVerificationGas
|
||||
'uint256', // verificationGasLimit
|
||||
'uint256', // preVerificationGas
|
||||
'uint256', // maxFeePerGas
|
||||
'uint256', // maxPriorityFeePerGas
|
||||
'bytes32' // paymasterAndData
|
||||
@@ -74,7 +74,7 @@ export const DefaultsForUserOp: UserOperation = {
|
||||
initCode: '0x',
|
||||
callData: '0x',
|
||||
callGasLimit: 0,
|
||||
verificationGasLimit: 100000, // default verification gas. will add create2 cost (3200+200*length) if initCode exists
|
||||
verificationGasLimit: 150000, // default verification gas. will add create2 cost (3200+200*length) if initCode exists
|
||||
preVerificationGas: 21000, // should also cover calldata cost.
|
||||
maxFeePerGas: 0,
|
||||
maxPriorityFeePerGas: 1e9,
|
||||
@@ -119,13 +119,13 @@ export function fillUserOpDefaults (op: Partial<UserOperation>, defaults = Defau
|
||||
// - calculate sender by eth_call the deployment code
|
||||
// - default verificationGasLimit estimateGas of deployment code plus default 100000
|
||||
// no initCode:
|
||||
// - update nonce from account.nonce()
|
||||
// - update nonce from account.getNonce()
|
||||
// entryPoint param is only required to fill in "sender address when specifying "initCode"
|
||||
// nonce: assume contract as "nonce()" function, and fill in.
|
||||
// nonce: assume contract as "getNonce()" function, and fill in.
|
||||
// sender - only in case of construction: fill sender from initCode.
|
||||
// callGasLimit: VERY crude estimation (by estimating call to account, and add rough entryPoint overhead
|
||||
// verificationGasLimit: hard-code default at 100k. should add "create2" cost
|
||||
export async function fillUserOp (op: Partial<UserOperation>, entryPoint?: EntryPoint): Promise<UserOperation> {
|
||||
export async function fillUserOp (op: Partial<UserOperation>, entryPoint?: EntryPoint, getNonceFunction = 'getNonce'): Promise<UserOperation> {
|
||||
const op1 = { ...op }
|
||||
const provider = entryPoint?.provider
|
||||
if (op.initCode != null) {
|
||||
@@ -157,8 +157,8 @@ export async function fillUserOp (op: Partial<UserOperation>, entryPoint?: Entry
|
||||
}
|
||||
if (op1.nonce == null) {
|
||||
if (provider == null) throw new Error('must have entryPoint to autofill nonce')
|
||||
const c = new Contract(op.sender!, ['function nonce() view returns(uint256)'], provider)
|
||||
op1.nonce = await c.nonce().catch(rethrow())
|
||||
const c = new Contract(op.sender!, [`function ${getNonceFunction}() view returns(uint256)`], provider)
|
||||
op1.nonce = await c[getNonceFunction]().catch(rethrow())
|
||||
}
|
||||
if (op1.callGasLimit == null && op.callData != null) {
|
||||
if (provider == null) throw new Error('must have entryPoint for callGasLimit estimate')
|
||||
@@ -191,9 +191,9 @@ export async function fillUserOp (op: Partial<UserOperation>, entryPoint?: Entry
|
||||
return op2
|
||||
}
|
||||
|
||||
export async function fillAndSign (op: Partial<UserOperation>, signer: Wallet | Signer, entryPoint?: EntryPoint): Promise<UserOperation> {
|
||||
export async function fillAndSign (op: Partial<UserOperation>, signer: Wallet | Signer, entryPoint?: EntryPoint, getNonceFunction = 'getNonce'): Promise<UserOperation> {
|
||||
const provider = entryPoint?.provider
|
||||
const op2 = await fillUserOp(op, entryPoint)
|
||||
const op2 = await fillUserOp(op, entryPoint, getNonceFunction)
|
||||
|
||||
const chainId = await provider!.getNetwork().then(net => net.chainId)
|
||||
const message = arrayify(getUserOpHash(op2, entryPoint!.address, chainId))
|
||||
|
||||
@@ -233,7 +233,7 @@ describe('EntryPoint', function () {
|
||||
// using wrong nonce
|
||||
const op = await fillAndSign({ sender: account.address, nonce: 1234 }, accountOwner, entryPoint)
|
||||
await expect(entryPoint.callStatic.simulateValidation(op)).to
|
||||
.revertedWith('AA23 reverted: account: invalid nonce')
|
||||
.revertedWith('AA25 invalid account nonce')
|
||||
})
|
||||
|
||||
it('should report signature failure without revert', async () => {
|
||||
@@ -411,7 +411,8 @@ describe('EntryPoint', function () {
|
||||
|
||||
const userOp: UserOperation = {
|
||||
sender: maliciousAccount.address,
|
||||
nonce: block.baseFeePerGas,
|
||||
nonce: await entryPoint.getNonce(maliciousAccount.address, 0),
|
||||
signature: defaultAbiCoder.encode(['uint256'], [block.baseFeePerGas]),
|
||||
initCode: '0x',
|
||||
callData: '0x',
|
||||
callGasLimit: '0x' + 1e5.toString(16),
|
||||
@@ -420,8 +421,7 @@ describe('EntryPoint', function () {
|
||||
// we need maxFeeperGas > block.basefee + maxPriorityFeePerGas so requiredPrefund onchain is basefee + maxPriorityFeePerGas
|
||||
maxFeePerGas: block.baseFeePerGas.mul(3),
|
||||
maxPriorityFeePerGas: block.baseFeePerGas,
|
||||
paymasterAndData: '0x',
|
||||
signature: '0x'
|
||||
paymasterAndData: '0x'
|
||||
}
|
||||
try {
|
||||
await expect(entryPoint.simulateValidation(userOp, { gasLimit: 1e6 }))
|
||||
@@ -447,13 +447,14 @@ describe('EntryPoint', function () {
|
||||
sender: testRevertAccount.address,
|
||||
callGasLimit: 1e5,
|
||||
maxFeePerGas: 1,
|
||||
nonce: await entryPoint.getNonce(testRevertAccount.address, 0),
|
||||
verificationGasLimit: 1e5,
|
||||
callData: badData.data!
|
||||
}
|
||||
const beneficiaryAddress = createAddress()
|
||||
await expect(entryPoint.simulateValidation(badOp, { gasLimit: 3e5 }))
|
||||
.to.revertedWith('ValidationResult')
|
||||
const tx = await entryPoint.handleOps([badOp], beneficiaryAddress, { gasLimit: 3e5 })
|
||||
const tx = await entryPoint.handleOps([badOp], beneficiaryAddress) // { gasLimit: 3e5 })
|
||||
const receipt = await tx.wait()
|
||||
const userOperationRevertReasonEvent = receipt.events?.find(event => event.event === 'UserOperationRevertReason')
|
||||
expect(userOperationRevertReasonEvent?.event).to.equal('UserOperationRevertReason')
|
||||
@@ -510,6 +511,71 @@ describe('EntryPoint', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('2d nonces', () => {
|
||||
const beneficiaryAddress = createAddress()
|
||||
let sender: string
|
||||
const key = 1
|
||||
const keyShifted = BigNumber.from(key).shl(64)
|
||||
|
||||
before(async () => {
|
||||
const { proxy } = await createAccount(ethersSigner, accountOwner.address, entryPoint.address)
|
||||
sender = proxy.address
|
||||
await fund(sender)
|
||||
})
|
||||
|
||||
it('should fail nonce with new key and seq!=0', async () => {
|
||||
const op = await fillAndSign({
|
||||
sender,
|
||||
nonce: keyShifted.add(1)
|
||||
}, accountOwner, entryPoint)
|
||||
await expect(entryPoint.callStatic.handleOps([op], beneficiaryAddress)).to.revertedWith('AA25 invalid account nonce')
|
||||
})
|
||||
|
||||
describe('with key=1, seq=1', () => {
|
||||
before(async () => {
|
||||
const op = await fillAndSign({
|
||||
sender,
|
||||
nonce: keyShifted
|
||||
}, accountOwner, entryPoint)
|
||||
await entryPoint.handleOps([op], beneficiaryAddress)
|
||||
})
|
||||
|
||||
it('should get next nonce value by getNonce', async () => {
|
||||
expect(await entryPoint.getNonce(sender, key)).to.eql(keyShifted.add(1))
|
||||
})
|
||||
|
||||
it('should allow to increment nonce of different key', async () => {
|
||||
const op = await fillAndSign({
|
||||
sender,
|
||||
nonce: await entryPoint.getNonce(sender, key)
|
||||
}, accountOwner, entryPoint)
|
||||
await entryPoint.callStatic.handleOps([op], beneficiaryAddress)
|
||||
})
|
||||
|
||||
it('should allow manual nonce increment', async () => {
|
||||
// must be called from account itself
|
||||
const incNonceKey = 5
|
||||
const incrementCallData = entryPoint.interface.encodeFunctionData('incrementNonce', [incNonceKey])
|
||||
const callData = account.interface.encodeFunctionData('execute', [entryPoint.address, 0, incrementCallData])
|
||||
const op = await fillAndSign({
|
||||
sender,
|
||||
callData,
|
||||
nonce: await entryPoint.getNonce(sender, key)
|
||||
}, accountOwner, entryPoint)
|
||||
await entryPoint.handleOps([op], beneficiaryAddress)
|
||||
|
||||
expect(await entryPoint.getNonce(sender, incNonceKey)).to.equal(BigNumber.from(incNonceKey).shl(64).add(1))
|
||||
})
|
||||
it('should fail with nonsequential seq', async () => {
|
||||
const op = await fillAndSign({
|
||||
sender,
|
||||
nonce: keyShifted.add(3)
|
||||
}, accountOwner, entryPoint)
|
||||
await expect(entryPoint.callStatic.handleOps([op], beneficiaryAddress)).to.revertedWith('AA25 invalid account nonce')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('without paymaster (account pays in eth)', () => {
|
||||
describe('#handleOps', () => {
|
||||
let counter: TestCounter
|
||||
@@ -1026,8 +1092,7 @@ describe('EntryPoint', function () {
|
||||
addr = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender)
|
||||
await ethersSigner.sendTransaction({ to: addr, value: parseEther('0.1') })
|
||||
userOp = await fillAndSign({
|
||||
initCode,
|
||||
nonce: 10
|
||||
initCode
|
||||
}, accountOwner, entryPoint)
|
||||
})
|
||||
it('simulateValidation should return aggregator and its stake', async () => {
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('Gnosis Proxy', function () {
|
||||
it('should fail from wrong entrypoint', async function () {
|
||||
const op = await fillAndSign({
|
||||
sender: proxy.address
|
||||
}, owner, entryPoint)
|
||||
}, owner, entryPoint, 'getNonce')
|
||||
|
||||
const anotherEntryPoint = await new EntryPoint__factory(ethersSigner).deploy()
|
||||
|
||||
@@ -116,14 +116,14 @@ describe('Gnosis Proxy', function () {
|
||||
nonce: 1234,
|
||||
callGasLimit: 1e6,
|
||||
callData: safe_execTxCallData
|
||||
}, owner, entryPoint)
|
||||
await expect(entryPoint.handleOps([op], beneficiary)).to.revertedWith('account: invalid nonce')
|
||||
}, owner, entryPoint, 'getNonce')
|
||||
await expect(entryPoint.handleOps([op], beneficiary)).to.revertedWith('AA25 invalid account nonce')
|
||||
|
||||
op = await fillAndSign({
|
||||
sender: proxy.address,
|
||||
callGasLimit: 1e6,
|
||||
callData: safe_execTxCallData
|
||||
}, owner, entryPoint)
|
||||
}, owner, entryPoint, 'getNonce')
|
||||
// invalidate the signature
|
||||
op.callGasLimit = 1
|
||||
await expect(entryPoint.handleOps([op], beneficiary)).to.revertedWith('FailedOp(0, "AA24 signature error")')
|
||||
@@ -134,7 +134,7 @@ describe('Gnosis Proxy', function () {
|
||||
sender: proxy.address,
|
||||
callGasLimit: 1e6,
|
||||
callData: safe_execTxCallData
|
||||
}, owner, entryPoint)
|
||||
}, owner, entryPoint, 'getNonce')
|
||||
const rcpt = await entryPoint.handleOps([op], beneficiary).then(async r => r.wait())
|
||||
console.log('gasUsed=', rcpt.gasUsed, rcpt.transactionHash)
|
||||
|
||||
@@ -151,7 +151,8 @@ describe('Gnosis Proxy', function () {
|
||||
sender: proxy.address,
|
||||
callGasLimit: 1e6,
|
||||
callData: safe_execFailTxCallData
|
||||
}, owner, entryPoint)
|
||||
}, owner, entryPoint, 'getNonce')
|
||||
|
||||
const rcpt = await entryPoint.handleOps([op], beneficiary).then(async r => r.wait())
|
||||
console.log('gasUsed=', rcpt.gasUsed, rcpt.transactionHash)
|
||||
|
||||
@@ -183,7 +184,7 @@ describe('Gnosis Proxy', function () {
|
||||
sender: counterfactualAddress,
|
||||
initCode,
|
||||
verificationGasLimit: 400000
|
||||
}, owner, entryPoint)
|
||||
}, owner, entryPoint, 'getNonce')
|
||||
|
||||
const rcpt = await entryPoint.handleOps([op], beneficiary).then(async r => r.wait())
|
||||
console.log('gasUsed=', rcpt.gasUsed, rcpt.transactionHash)
|
||||
@@ -200,7 +201,7 @@ describe('Gnosis Proxy', function () {
|
||||
const op = await fillAndSign({
|
||||
sender: counterfactualAddress,
|
||||
callData: safe_execTxCallData
|
||||
}, owner, entryPoint)
|
||||
}, owner, entryPoint, 'getNonce')
|
||||
|
||||
const rcpt = await entryPoint.handleOps([op], beneficiary).then(async r => r.wait())
|
||||
console.log('gasUsed=', rcpt.gasUsed, rcpt.transactionHash)
|
||||
|
||||
@@ -2,31 +2,36 @@ import { Wallet } from 'ethers'
|
||||
import { ethers } from 'hardhat'
|
||||
import { expect } from 'chai'
|
||||
import {
|
||||
ERC1967Proxy__factory,
|
||||
SimpleAccount,
|
||||
SimpleAccountFactory__factory,
|
||||
SimpleAccount__factory,
|
||||
TestUtil,
|
||||
TestUtil__factory
|
||||
} from '../typechain'
|
||||
import {
|
||||
createAccount,
|
||||
createAddress,
|
||||
createAccountOwner,
|
||||
deployEntryPoint,
|
||||
getBalance,
|
||||
isDeployed,
|
||||
ONE_ETH,
|
||||
createAccount, HashZero
|
||||
HashZero
|
||||
} from './testutils'
|
||||
import { fillUserOpDefaults, getUserOpHash, packUserOp, signUserOp } from './UserOp'
|
||||
import { parseEther } from 'ethers/lib/utils'
|
||||
import { UserOperation } from './UserOperation'
|
||||
|
||||
describe('SimpleAccount', function () {
|
||||
const entryPoint = '0x'.padEnd(42, '2')
|
||||
let entryPoint: string
|
||||
let accounts: string[]
|
||||
let testUtil: TestUtil
|
||||
let accountOwner: Wallet
|
||||
const ethersSigner = ethers.provider.getSigner()
|
||||
|
||||
before(async function () {
|
||||
entryPoint = await deployEntryPoint().then(e => e.address)
|
||||
accounts = await ethers.provider.listAccounts()
|
||||
// ignore in geth.. this is just a sanity test. should be refactored to use a single-account mode..
|
||||
if (accounts.length < 2) this.skip()
|
||||
@@ -59,11 +64,18 @@ describe('SimpleAccount', function () {
|
||||
let expectedPay: number
|
||||
|
||||
const actualGasPrice = 1e9
|
||||
// for testing directly validateUserOp, we initialize the account with EOA as entryPoint.
|
||||
let entryPointEoa: string
|
||||
|
||||
before(async () => {
|
||||
// that's the account of ethersSigner
|
||||
const entryPoint = accounts[2];
|
||||
({ proxy: account } = await createAccount(await ethers.getSigner(entryPoint), accountOwner.address, entryPoint))
|
||||
entryPointEoa = accounts[2]
|
||||
const epAsSigner = await ethers.getSigner(entryPointEoa)
|
||||
|
||||
// cant use "SimpleAccountFactory", since it attempts to increment nonce first
|
||||
const implementation = await new SimpleAccount__factory(ethersSigner).deploy(entryPointEoa)
|
||||
const proxy = await new ERC1967Proxy__factory(ethersSigner).deploy(implementation.address, '0x')
|
||||
account = SimpleAccount__factory.connect(proxy.address, epAsSigner)
|
||||
|
||||
await ethersSigner.sendTransaction({ from: accounts[0], to: account.address, value: parseEther('0.2') })
|
||||
const callGasLimit = 200000
|
||||
const verificationGasLimit = 100000
|
||||
@@ -75,9 +87,9 @@ describe('SimpleAccount', function () {
|
||||
callGasLimit,
|
||||
verificationGasLimit,
|
||||
maxFeePerGas
|
||||
}), accountOwner, entryPoint, chainId)
|
||||
}), accountOwner, entryPointEoa, chainId)
|
||||
|
||||
userOpHash = await getUserOpHash(userOp, entryPoint, chainId)
|
||||
userOpHash = await getUserOpHash(userOp, entryPointEoa, chainId)
|
||||
|
||||
expectedPay = actualGasPrice * (callGasLimit + verificationGasLimit)
|
||||
|
||||
@@ -91,20 +103,13 @@ describe('SimpleAccount', function () {
|
||||
expect(preBalance - postBalance).to.eql(expectedPay)
|
||||
})
|
||||
|
||||
it('should increment nonce', async () => {
|
||||
expect(await account.nonce()).to.equal(1)
|
||||
})
|
||||
|
||||
it('should reject same TX on nonce error', async () => {
|
||||
await expect(account.validateUserOp(userOp, userOpHash, 0)).to.revertedWith('invalid nonce')
|
||||
})
|
||||
|
||||
it('should return NO_SIG_VALIDATION on wrong signature', async () => {
|
||||
const userOpHash = HashZero
|
||||
const deadline = await account.callStatic.validateUserOp({ ...userOp, nonce: 1 }, userOpHash, 0)
|
||||
expect(deadline).to.eq(1)
|
||||
})
|
||||
})
|
||||
|
||||
context('SimpleAccountFactory', () => {
|
||||
it('sanity: check deployer', async () => {
|
||||
const ownerAddr = createAddress()
|
||||
|
||||
@@ -184,8 +184,7 @@ describe('bls account', function () {
|
||||
await fund(senderAddress, '0.01')
|
||||
const userOp = await fillUserOp({
|
||||
sender: senderAddress,
|
||||
initCode,
|
||||
nonce: 2
|
||||
initCode
|
||||
}, entrypoint)
|
||||
const requestHash = await blsAgg.getUserOpHash(userOp)
|
||||
const sigParts = signer3.sign(requestHash)
|
||||
|
||||
Reference in New Issue
Block a user