mirror of
https://github.com/getwax/zk-account-abstraction.git
synced 2026-01-08 20:18:05 -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;
|
pragma solidity ^0.8.12;
|
||||||
|
|
||||||
/* solhint-disable avoid-low-level-calls */
|
/* solhint-disable avoid-low-level-calls */
|
||||||
/* solhint-disable no-inline-assembly */
|
/* solhint-disable no-empty-blocks */
|
||||||
/* solhint-disable reason-string */
|
|
||||||
|
|
||||||
import "../interfaces/IAccount.sol";
|
import "../interfaces/IAccount.sol";
|
||||||
import "../interfaces/IEntryPoint.sol";
|
import "../interfaces/IEntryPoint.sol";
|
||||||
@@ -22,10 +21,13 @@ abstract contract BaseAccount is IAccount {
|
|||||||
uint256 constant internal SIG_VALIDATION_FAILED = 1;
|
uint256 constant internal SIG_VALIDATION_FAILED = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* return the account nonce.
|
* 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)
|
* 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.
|
* return the entryPoint used by this account.
|
||||||
@@ -41,9 +43,7 @@ abstract contract BaseAccount is IAccount {
|
|||||||
external override virtual returns (uint256 validationData) {
|
external override virtual returns (uint256 validationData) {
|
||||||
_requireFromEntryPoint();
|
_requireFromEntryPoint();
|
||||||
validationData = _validateSignature(userOp, userOpHash);
|
validationData = _validateSignature(userOp, userOpHash);
|
||||||
if (userOp.initCode.length == 0) {
|
_validateNonce(userOp.nonce);
|
||||||
_validateAndUpdateNonce(userOp);
|
|
||||||
}
|
|
||||||
_payPrefund(missingAccountFunds);
|
_payPrefund(missingAccountFunds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,12 +71,23 @@ abstract contract BaseAccount is IAccount {
|
|||||||
internal virtual returns (uint256 validationData);
|
internal virtual returns (uint256 validationData);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* validate the current nonce matches the UserOperation nonce.
|
* Validate the nonce of the UserOperation.
|
||||||
* then it should update the account's state to prevent replay of this UserOperation.
|
* This method may validate the nonce requirement of this account.
|
||||||
* called only if initCode is empty (since "nonce" field is used as "salt" on account creation)
|
* e.g.
|
||||||
* @param userOp the op to validate.
|
* 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.
|
* sends to the entrypoint (msg.sender) the missing funds for this transaction.
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ import "../utils/Exec.sol";
|
|||||||
import "./StakeManager.sol";
|
import "./StakeManager.sol";
|
||||||
import "./SenderCreator.sol";
|
import "./SenderCreator.sol";
|
||||||
import "./Helpers.sol";
|
import "./Helpers.sol";
|
||||||
|
import "./NonceManager.sol";
|
||||||
|
|
||||||
contract EntryPoint is IEntryPoint, StakeManager {
|
contract EntryPoint is IEntryPoint, StakeManager, NonceManager {
|
||||||
|
|
||||||
using UserOperationLib for UserOperation;
|
using UserOperationLib for UserOperation;
|
||||||
|
|
||||||
@@ -349,7 +350,8 @@ contract EntryPoint is IEntryPoint, StakeManager {
|
|||||||
* @param initCode the constructor code to be passed into the UserOperation.
|
* @param initCode the constructor code to be passed into the UserOperation.
|
||||||
*/
|
*/
|
||||||
function getSenderAddress(bytes calldata initCode) public {
|
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 {
|
function _simulationOnlyValidations(UserOperation calldata userOp) internal view {
|
||||||
@@ -511,6 +513,11 @@ contract EntryPoint is IEntryPoint, StakeManager {
|
|||||||
uint256 gasUsedByValidateAccountPrepayment;
|
uint256 gasUsedByValidateAccountPrepayment;
|
||||||
(uint256 requiredPreFund) = _getRequiredPrefund(mUserOp);
|
(uint256 requiredPreFund) = _getRequiredPrefund(mUserOp);
|
||||||
(gasUsedByValidateAccountPrepayment, validationData) = _validateAccountPrepayment(opIndex, userOp, outOpInfo, requiredPreFund);
|
(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
|
//a "marker" where account opcode validation is done and paymaster opcode validation is about to start
|
||||||
// (used only by off-chain simulateValidation)
|
// (used only by off-chain simulateValidation)
|
||||||
numberMarker();
|
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 "./UserOperation.sol";
|
||||||
import "./IStakeManager.sol";
|
import "./IStakeManager.sol";
|
||||||
import "./IAggregator.sol";
|
import "./IAggregator.sol";
|
||||||
|
import "./INonceManager.sol";
|
||||||
|
|
||||||
interface IEntryPoint is IStakeManager {
|
interface IEntryPoint is IStakeManager, INonceManager {
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* An event emitted after each successful request
|
* 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 {
|
contract SimpleAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, Initializable {
|
||||||
using ECDSA for bytes32;
|
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;
|
address public owner;
|
||||||
|
|
||||||
IEntryPoint private immutable _entryPoint;
|
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
|
/// @inheritdoc BaseAccount
|
||||||
function entryPoint() public view virtual override returns (IEntryPoint) {
|
function entryPoint() public view virtual override returns (IEntryPoint) {
|
||||||
return _entryPoint;
|
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");
|
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
|
/// implement template method of BaseAccount
|
||||||
function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash)
|
function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash)
|
||||||
internal override virtual returns (uint256 validationData) {
|
internal override virtual returns (uint256 validationData) {
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ contract EIP4337Fallback is DefaultCallbackHandler, IAccount, IERC1271 {
|
|||||||
return abi.decode(ret, (uint256));
|
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
|
* called from the Safe. delegate actual work to EIP4337Manager
|
||||||
*/
|
*/
|
||||||
@@ -78,4 +86,4 @@ contract EIP4337Fallback is DefaultCallbackHandler, IAccount, IERC1271 {
|
|||||||
return 0xffffffff;
|
return 0xffffffff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,10 +55,8 @@ contract EIP4337Manager is IAccount, GnosisSafeStorage, Executor {
|
|||||||
validationData = SIG_VALIDATION_FAILED;
|
validationData = SIG_VALIDATION_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userOp.initCode.length == 0) {
|
// mimic normal Safe nonce behaviour: prevent parallel nonces
|
||||||
require(uint256(nonce) == userOp.nonce, "account: invalid nonce");
|
require(userOp.nonce < type(uint64).max, "account: nonsequential nonce");
|
||||||
nonce = bytes32(uint256(nonce) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (missingAccountFunds > 0) {
|
if (missingAccountFunds > 0) {
|
||||||
//Note: MAY pay more than the minimum, to deposit for future transactions
|
//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.
|
* set up a safe as EIP-4337 enabled.
|
||||||
@@ -158,7 +162,8 @@ contract EIP4337Manager is IAccount, GnosisSafeStorage, Executor {
|
|||||||
sig[64] = bytes1(uint8(27));
|
sig[64] = bytes1(uint8(27));
|
||||||
sig[2] = bytes1(uint8(1));
|
sig[2] = bytes1(uint8(1));
|
||||||
sig[35] = 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);
|
UserOperation[] memory userOps = new UserOperation[](1);
|
||||||
userOps[0] = userOp;
|
userOps[0] = userOp;
|
||||||
IEntryPoint _entryPoint = IEntryPoint(payable(manager.entryPoint()));
|
IEntryPoint _entryPoint = IEntryPoint(payable(manager.entryPoint()));
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ contract MaliciousAccount is IAccount {
|
|||||||
function validateUserOp(UserOperation calldata userOp, bytes32, uint256 missingAccountFunds)
|
function validateUserOp(UserOperation calldata userOp, bytes32, uint256 missingAccountFunds)
|
||||||
external returns (uint256 validationData) {
|
external returns (uint256 validationData) {
|
||||||
ep.depositTo{value : missingAccountFunds}(address(this));
|
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 requiredGas = userOp.callGasLimit + userOp.verificationGasLimit + userOp.preVerificationGas;
|
||||||
uint256 gasPrice = missingAccountFunds / requiredGas;
|
uint256 gasPrice = missingAccountFunds / requiredGas;
|
||||||
uint256 basefee = gasPrice - userOp.maxPriorityFeePerGas;
|
uint256 basefee = gasPrice - userOp.maxPriorityFeePerGas;
|
||||||
require (basefee == userOp.nonce, "Revert after first validation");
|
require (basefee == externalBaseFee, "Revert after first validation");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const deploySimpleAccountFactory: DeployFunction = async function (hre: HardhatR
|
|||||||
from,
|
from,
|
||||||
args: [entrypoint.address],
|
args: [entrypoint.address],
|
||||||
gasLimit: 6e6,
|
gasLimit: 6e6,
|
||||||
|
log: true,
|
||||||
deterministicDeployment: true
|
deterministicDeployment: true
|
||||||
})
|
})
|
||||||
console.log('==SimpleAccountFactory addr=', ret.address)
|
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
|
| Field | Type | Description
|
||||||
| - | - | - |
|
| - | - | - |
|
||||||
| `sender` | `address` | The account making the operation |
|
| `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) |
|
| `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 |
|
| `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 |
|
| `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
|
* 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
|
* 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.
|
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)
|
* 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 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.
|
* 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.
|
* **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.
|
* **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 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`:
|
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.
|
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
|
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
|
### 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:
|
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
|
## 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):
|
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'
|
} from '../typechain'
|
||||||
import { BigNumberish, Wallet } from 'ethers'
|
import { BigNumberish, Wallet } from 'ethers'
|
||||||
import hre from 'hardhat'
|
import hre from 'hardhat'
|
||||||
import { fillAndSign } from '../test/UserOp'
|
import { fillAndSign, fillUserOp, signUserOp } from '../test/UserOp'
|
||||||
import { TransactionReceipt } from '@ethersproject/abstract-provider'
|
import { TransactionReceipt } from '@ethersproject/abstract-provider'
|
||||||
import { table, TableUserConfig } from 'table'
|
import { table, TableUserConfig } from 'table'
|
||||||
import { Create2Factory } from '../src/Create2Factory'
|
import { Create2Factory } from '../src/Create2Factory'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import { SimpleAccountInterface } from '../typechain/contracts/samples/SimpleAccount'
|
import { SimpleAccountInterface } from '../typechain/contracts/samples/SimpleAccount'
|
||||||
|
import { UserOperation } from '../test/UserOperation'
|
||||||
|
|
||||||
const gasCheckerLogFile = './reports/gas-checker.txt'
|
const gasCheckerLogFile = './reports/gas-checker.txt'
|
||||||
|
|
||||||
@@ -111,6 +112,8 @@ export class GasChecker {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createdAccounts = new Set<string>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create accounts up to this counter.
|
* create accounts up to this counter.
|
||||||
* make sure they all have balance.
|
* make sure they all have balance.
|
||||||
@@ -127,11 +130,29 @@ export class GasChecker {
|
|||||||
console.log('factaddr', factoryAddress)
|
console.log('factaddr', factoryAddress)
|
||||||
const fact = SimpleAccountFactory__factory.connect(factoryAddress, ethersSigner)
|
const fact = SimpleAccountFactory__factory.connect(factoryAddress, ethersSigner)
|
||||||
// create accounts
|
// create accounts
|
||||||
|
const creationOps: UserOperation[] = []
|
||||||
for (const n of range(count)) {
|
for (const n of range(count)) {
|
||||||
const salt = n
|
const salt = n
|
||||||
// const initCode = this.accountInitCode(fact, salt)
|
// const initCode = this.accountInitCode(fact, salt)
|
||||||
|
|
||||||
const addr = await fact.getAddress(this.accountOwner.address, 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
|
this.accounts[addr] = this.accountOwner
|
||||||
// deploy if not already deployed.
|
// deploy if not already deployed.
|
||||||
await fact.createAccount(this.accountOwner.address, salt)
|
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 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
|
title: info.title
|
||||||
// receipt: rcpt
|
// receipt: rcpt
|
||||||
}
|
}
|
||||||
if (info.diffLastGas) { ret1.gasDiff = gasDiff }
|
if (info.diffLastGas) {
|
||||||
|
ret1.gasDiff = gasDiff
|
||||||
|
}
|
||||||
console.debug(ret1)
|
console.debug(ret1)
|
||||||
return ret1
|
return ret1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
the destination is "account.entryPoint()", which is known to be "hot" address used by this account
|
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)
|
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 ║
|
║ │ │ │ (delta for │ (compared to ║
|
||||||
║ │ │ │ one UserOp) │ account.exec()) ║
|
║ │ │ │ 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', // initCode
|
||||||
'bytes32', // callData
|
'bytes32', // callData
|
||||||
'uint256', // callGasLimit
|
'uint256', // callGasLimit
|
||||||
'uint', // verificationGasLimit
|
'uint256', // verificationGasLimit
|
||||||
'uint', // preVerificationGas
|
'uint256', // preVerificationGas
|
||||||
'uint256', // maxFeePerGas
|
'uint256', // maxFeePerGas
|
||||||
'uint256', // maxPriorityFeePerGas
|
'uint256', // maxPriorityFeePerGas
|
||||||
'bytes32' // paymasterAndData
|
'bytes32' // paymasterAndData
|
||||||
@@ -74,7 +74,7 @@ export const DefaultsForUserOp: UserOperation = {
|
|||||||
initCode: '0x',
|
initCode: '0x',
|
||||||
callData: '0x',
|
callData: '0x',
|
||||||
callGasLimit: 0,
|
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.
|
preVerificationGas: 21000, // should also cover calldata cost.
|
||||||
maxFeePerGas: 0,
|
maxFeePerGas: 0,
|
||||||
maxPriorityFeePerGas: 1e9,
|
maxPriorityFeePerGas: 1e9,
|
||||||
@@ -119,13 +119,13 @@ export function fillUserOpDefaults (op: Partial<UserOperation>, defaults = Defau
|
|||||||
// - calculate sender by eth_call the deployment code
|
// - calculate sender by eth_call the deployment code
|
||||||
// - default verificationGasLimit estimateGas of deployment code plus default 100000
|
// - default verificationGasLimit estimateGas of deployment code plus default 100000
|
||||||
// no initCode:
|
// 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"
|
// 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.
|
// sender - only in case of construction: fill sender from initCode.
|
||||||
// callGasLimit: VERY crude estimation (by estimating call to account, and add rough entryPoint overhead
|
// callGasLimit: VERY crude estimation (by estimating call to account, and add rough entryPoint overhead
|
||||||
// verificationGasLimit: hard-code default at 100k. should add "create2" cost
|
// 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 op1 = { ...op }
|
||||||
const provider = entryPoint?.provider
|
const provider = entryPoint?.provider
|
||||||
if (op.initCode != null) {
|
if (op.initCode != null) {
|
||||||
@@ -157,8 +157,8 @@ export async function fillUserOp (op: Partial<UserOperation>, entryPoint?: Entry
|
|||||||
}
|
}
|
||||||
if (op1.nonce == null) {
|
if (op1.nonce == null) {
|
||||||
if (provider == null) throw new Error('must have entryPoint to autofill nonce')
|
if (provider == null) throw new Error('must have entryPoint to autofill nonce')
|
||||||
const c = new Contract(op.sender!, ['function nonce() view returns(uint256)'], provider)
|
const c = new Contract(op.sender!, [`function ${getNonceFunction}() view returns(uint256)`], provider)
|
||||||
op1.nonce = await c.nonce().catch(rethrow())
|
op1.nonce = await c[getNonceFunction]().catch(rethrow())
|
||||||
}
|
}
|
||||||
if (op1.callGasLimit == null && op.callData != null) {
|
if (op1.callGasLimit == null && op.callData != null) {
|
||||||
if (provider == null) throw new Error('must have entryPoint for callGasLimit estimate')
|
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
|
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 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 chainId = await provider!.getNetwork().then(net => net.chainId)
|
||||||
const message = arrayify(getUserOpHash(op2, entryPoint!.address, chainId))
|
const message = arrayify(getUserOpHash(op2, entryPoint!.address, chainId))
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ describe('EntryPoint', function () {
|
|||||||
// using wrong nonce
|
// using wrong nonce
|
||||||
const op = await fillAndSign({ sender: account.address, nonce: 1234 }, accountOwner, entryPoint)
|
const op = await fillAndSign({ sender: account.address, nonce: 1234 }, accountOwner, entryPoint)
|
||||||
await expect(entryPoint.callStatic.simulateValidation(op)).to
|
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 () => {
|
it('should report signature failure without revert', async () => {
|
||||||
@@ -411,7 +411,8 @@ describe('EntryPoint', function () {
|
|||||||
|
|
||||||
const userOp: UserOperation = {
|
const userOp: UserOperation = {
|
||||||
sender: maliciousAccount.address,
|
sender: maliciousAccount.address,
|
||||||
nonce: block.baseFeePerGas,
|
nonce: await entryPoint.getNonce(maliciousAccount.address, 0),
|
||||||
|
signature: defaultAbiCoder.encode(['uint256'], [block.baseFeePerGas]),
|
||||||
initCode: '0x',
|
initCode: '0x',
|
||||||
callData: '0x',
|
callData: '0x',
|
||||||
callGasLimit: '0x' + 1e5.toString(16),
|
callGasLimit: '0x' + 1e5.toString(16),
|
||||||
@@ -420,8 +421,7 @@ describe('EntryPoint', function () {
|
|||||||
// we need maxFeeperGas > block.basefee + maxPriorityFeePerGas so requiredPrefund onchain is basefee + maxPriorityFeePerGas
|
// we need maxFeeperGas > block.basefee + maxPriorityFeePerGas so requiredPrefund onchain is basefee + maxPriorityFeePerGas
|
||||||
maxFeePerGas: block.baseFeePerGas.mul(3),
|
maxFeePerGas: block.baseFeePerGas.mul(3),
|
||||||
maxPriorityFeePerGas: block.baseFeePerGas,
|
maxPriorityFeePerGas: block.baseFeePerGas,
|
||||||
paymasterAndData: '0x',
|
paymasterAndData: '0x'
|
||||||
signature: '0x'
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await expect(entryPoint.simulateValidation(userOp, { gasLimit: 1e6 }))
|
await expect(entryPoint.simulateValidation(userOp, { gasLimit: 1e6 }))
|
||||||
@@ -447,13 +447,14 @@ describe('EntryPoint', function () {
|
|||||||
sender: testRevertAccount.address,
|
sender: testRevertAccount.address,
|
||||||
callGasLimit: 1e5,
|
callGasLimit: 1e5,
|
||||||
maxFeePerGas: 1,
|
maxFeePerGas: 1,
|
||||||
|
nonce: await entryPoint.getNonce(testRevertAccount.address, 0),
|
||||||
verificationGasLimit: 1e5,
|
verificationGasLimit: 1e5,
|
||||||
callData: badData.data!
|
callData: badData.data!
|
||||||
}
|
}
|
||||||
const beneficiaryAddress = createAddress()
|
const beneficiaryAddress = createAddress()
|
||||||
await expect(entryPoint.simulateValidation(badOp, { gasLimit: 3e5 }))
|
await expect(entryPoint.simulateValidation(badOp, { gasLimit: 3e5 }))
|
||||||
.to.revertedWith('ValidationResult')
|
.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 receipt = await tx.wait()
|
||||||
const userOperationRevertReasonEvent = receipt.events?.find(event => event.event === 'UserOperationRevertReason')
|
const userOperationRevertReasonEvent = receipt.events?.find(event => event.event === 'UserOperationRevertReason')
|
||||||
expect(userOperationRevertReasonEvent?.event).to.equal('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('without paymaster (account pays in eth)', () => {
|
||||||
describe('#handleOps', () => {
|
describe('#handleOps', () => {
|
||||||
let counter: TestCounter
|
let counter: TestCounter
|
||||||
@@ -1026,8 +1092,7 @@ describe('EntryPoint', function () {
|
|||||||
addr = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender)
|
addr = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender)
|
||||||
await ethersSigner.sendTransaction({ to: addr, value: parseEther('0.1') })
|
await ethersSigner.sendTransaction({ to: addr, value: parseEther('0.1') })
|
||||||
userOp = await fillAndSign({
|
userOp = await fillAndSign({
|
||||||
initCode,
|
initCode
|
||||||
nonce: 10
|
|
||||||
}, accountOwner, entryPoint)
|
}, accountOwner, entryPoint)
|
||||||
})
|
})
|
||||||
it('simulateValidation should return aggregator and its stake', async () => {
|
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 () {
|
it('should fail from wrong entrypoint', async function () {
|
||||||
const op = await fillAndSign({
|
const op = await fillAndSign({
|
||||||
sender: proxy.address
|
sender: proxy.address
|
||||||
}, owner, entryPoint)
|
}, owner, entryPoint, 'getNonce')
|
||||||
|
|
||||||
const anotherEntryPoint = await new EntryPoint__factory(ethersSigner).deploy()
|
const anotherEntryPoint = await new EntryPoint__factory(ethersSigner).deploy()
|
||||||
|
|
||||||
@@ -116,14 +116,14 @@ describe('Gnosis Proxy', function () {
|
|||||||
nonce: 1234,
|
nonce: 1234,
|
||||||
callGasLimit: 1e6,
|
callGasLimit: 1e6,
|
||||||
callData: safe_execTxCallData
|
callData: safe_execTxCallData
|
||||||
}, owner, entryPoint)
|
}, owner, entryPoint, 'getNonce')
|
||||||
await expect(entryPoint.handleOps([op], beneficiary)).to.revertedWith('account: invalid nonce')
|
await expect(entryPoint.handleOps([op], beneficiary)).to.revertedWith('AA25 invalid account nonce')
|
||||||
|
|
||||||
op = await fillAndSign({
|
op = await fillAndSign({
|
||||||
sender: proxy.address,
|
sender: proxy.address,
|
||||||
callGasLimit: 1e6,
|
callGasLimit: 1e6,
|
||||||
callData: safe_execTxCallData
|
callData: safe_execTxCallData
|
||||||
}, owner, entryPoint)
|
}, owner, entryPoint, 'getNonce')
|
||||||
// invalidate the signature
|
// invalidate the signature
|
||||||
op.callGasLimit = 1
|
op.callGasLimit = 1
|
||||||
await expect(entryPoint.handleOps([op], beneficiary)).to.revertedWith('FailedOp(0, "AA24 signature error")')
|
await expect(entryPoint.handleOps([op], beneficiary)).to.revertedWith('FailedOp(0, "AA24 signature error")')
|
||||||
@@ -134,7 +134,7 @@ describe('Gnosis Proxy', function () {
|
|||||||
sender: proxy.address,
|
sender: proxy.address,
|
||||||
callGasLimit: 1e6,
|
callGasLimit: 1e6,
|
||||||
callData: safe_execTxCallData
|
callData: safe_execTxCallData
|
||||||
}, owner, entryPoint)
|
}, owner, entryPoint, 'getNonce')
|
||||||
const rcpt = await entryPoint.handleOps([op], beneficiary).then(async r => r.wait())
|
const rcpt = await entryPoint.handleOps([op], beneficiary).then(async r => r.wait())
|
||||||
console.log('gasUsed=', rcpt.gasUsed, rcpt.transactionHash)
|
console.log('gasUsed=', rcpt.gasUsed, rcpt.transactionHash)
|
||||||
|
|
||||||
@@ -151,7 +151,8 @@ describe('Gnosis Proxy', function () {
|
|||||||
sender: proxy.address,
|
sender: proxy.address,
|
||||||
callGasLimit: 1e6,
|
callGasLimit: 1e6,
|
||||||
callData: safe_execFailTxCallData
|
callData: safe_execFailTxCallData
|
||||||
}, owner, entryPoint)
|
}, owner, entryPoint, 'getNonce')
|
||||||
|
|
||||||
const rcpt = await entryPoint.handleOps([op], beneficiary).then(async r => r.wait())
|
const rcpt = await entryPoint.handleOps([op], beneficiary).then(async r => r.wait())
|
||||||
console.log('gasUsed=', rcpt.gasUsed, rcpt.transactionHash)
|
console.log('gasUsed=', rcpt.gasUsed, rcpt.transactionHash)
|
||||||
|
|
||||||
@@ -183,7 +184,7 @@ describe('Gnosis Proxy', function () {
|
|||||||
sender: counterfactualAddress,
|
sender: counterfactualAddress,
|
||||||
initCode,
|
initCode,
|
||||||
verificationGasLimit: 400000
|
verificationGasLimit: 400000
|
||||||
}, owner, entryPoint)
|
}, owner, entryPoint, 'getNonce')
|
||||||
|
|
||||||
const rcpt = await entryPoint.handleOps([op], beneficiary).then(async r => r.wait())
|
const rcpt = await entryPoint.handleOps([op], beneficiary).then(async r => r.wait())
|
||||||
console.log('gasUsed=', rcpt.gasUsed, rcpt.transactionHash)
|
console.log('gasUsed=', rcpt.gasUsed, rcpt.transactionHash)
|
||||||
@@ -200,7 +201,7 @@ describe('Gnosis Proxy', function () {
|
|||||||
const op = await fillAndSign({
|
const op = await fillAndSign({
|
||||||
sender: counterfactualAddress,
|
sender: counterfactualAddress,
|
||||||
callData: safe_execTxCallData
|
callData: safe_execTxCallData
|
||||||
}, owner, entryPoint)
|
}, owner, entryPoint, 'getNonce')
|
||||||
|
|
||||||
const rcpt = await entryPoint.handleOps([op], beneficiary).then(async r => r.wait())
|
const rcpt = await entryPoint.handleOps([op], beneficiary).then(async r => r.wait())
|
||||||
console.log('gasUsed=', rcpt.gasUsed, rcpt.transactionHash)
|
console.log('gasUsed=', rcpt.gasUsed, rcpt.transactionHash)
|
||||||
|
|||||||
@@ -2,31 +2,36 @@ import { Wallet } from 'ethers'
|
|||||||
import { ethers } from 'hardhat'
|
import { ethers } from 'hardhat'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import {
|
import {
|
||||||
|
ERC1967Proxy__factory,
|
||||||
SimpleAccount,
|
SimpleAccount,
|
||||||
SimpleAccountFactory__factory,
|
SimpleAccountFactory__factory,
|
||||||
|
SimpleAccount__factory,
|
||||||
TestUtil,
|
TestUtil,
|
||||||
TestUtil__factory
|
TestUtil__factory
|
||||||
} from '../typechain'
|
} from '../typechain'
|
||||||
import {
|
import {
|
||||||
|
createAccount,
|
||||||
createAddress,
|
createAddress,
|
||||||
createAccountOwner,
|
createAccountOwner,
|
||||||
|
deployEntryPoint,
|
||||||
getBalance,
|
getBalance,
|
||||||
isDeployed,
|
isDeployed,
|
||||||
ONE_ETH,
|
ONE_ETH,
|
||||||
createAccount, HashZero
|
HashZero
|
||||||
} from './testutils'
|
} from './testutils'
|
||||||
import { fillUserOpDefaults, getUserOpHash, packUserOp, signUserOp } from './UserOp'
|
import { fillUserOpDefaults, getUserOpHash, packUserOp, signUserOp } from './UserOp'
|
||||||
import { parseEther } from 'ethers/lib/utils'
|
import { parseEther } from 'ethers/lib/utils'
|
||||||
import { UserOperation } from './UserOperation'
|
import { UserOperation } from './UserOperation'
|
||||||
|
|
||||||
describe('SimpleAccount', function () {
|
describe('SimpleAccount', function () {
|
||||||
const entryPoint = '0x'.padEnd(42, '2')
|
let entryPoint: string
|
||||||
let accounts: string[]
|
let accounts: string[]
|
||||||
let testUtil: TestUtil
|
let testUtil: TestUtil
|
||||||
let accountOwner: Wallet
|
let accountOwner: Wallet
|
||||||
const ethersSigner = ethers.provider.getSigner()
|
const ethersSigner = ethers.provider.getSigner()
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
|
entryPoint = await deployEntryPoint().then(e => e.address)
|
||||||
accounts = await ethers.provider.listAccounts()
|
accounts = await ethers.provider.listAccounts()
|
||||||
// ignore in geth.. this is just a sanity test. should be refactored to use a single-account mode..
|
// ignore in geth.. this is just a sanity test. should be refactored to use a single-account mode..
|
||||||
if (accounts.length < 2) this.skip()
|
if (accounts.length < 2) this.skip()
|
||||||
@@ -59,11 +64,18 @@ describe('SimpleAccount', function () {
|
|||||||
let expectedPay: number
|
let expectedPay: number
|
||||||
|
|
||||||
const actualGasPrice = 1e9
|
const actualGasPrice = 1e9
|
||||||
|
// for testing directly validateUserOp, we initialize the account with EOA as entryPoint.
|
||||||
|
let entryPointEoa: string
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
// that's the account of ethersSigner
|
entryPointEoa = accounts[2]
|
||||||
const entryPoint = accounts[2];
|
const epAsSigner = await ethers.getSigner(entryPointEoa)
|
||||||
({ proxy: account } = await createAccount(await ethers.getSigner(entryPoint), accountOwner.address, entryPoint))
|
|
||||||
|
// 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') })
|
await ethersSigner.sendTransaction({ from: accounts[0], to: account.address, value: parseEther('0.2') })
|
||||||
const callGasLimit = 200000
|
const callGasLimit = 200000
|
||||||
const verificationGasLimit = 100000
|
const verificationGasLimit = 100000
|
||||||
@@ -75,9 +87,9 @@ describe('SimpleAccount', function () {
|
|||||||
callGasLimit,
|
callGasLimit,
|
||||||
verificationGasLimit,
|
verificationGasLimit,
|
||||||
maxFeePerGas
|
maxFeePerGas
|
||||||
}), accountOwner, entryPoint, chainId)
|
}), accountOwner, entryPointEoa, chainId)
|
||||||
|
|
||||||
userOpHash = await getUserOpHash(userOp, entryPoint, chainId)
|
userOpHash = await getUserOpHash(userOp, entryPointEoa, chainId)
|
||||||
|
|
||||||
expectedPay = actualGasPrice * (callGasLimit + verificationGasLimit)
|
expectedPay = actualGasPrice * (callGasLimit + verificationGasLimit)
|
||||||
|
|
||||||
@@ -91,20 +103,13 @@ describe('SimpleAccount', function () {
|
|||||||
expect(preBalance - postBalance).to.eql(expectedPay)
|
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 () => {
|
it('should return NO_SIG_VALIDATION on wrong signature', async () => {
|
||||||
const userOpHash = HashZero
|
const userOpHash = HashZero
|
||||||
const deadline = await account.callStatic.validateUserOp({ ...userOp, nonce: 1 }, userOpHash, 0)
|
const deadline = await account.callStatic.validateUserOp({ ...userOp, nonce: 1 }, userOpHash, 0)
|
||||||
expect(deadline).to.eq(1)
|
expect(deadline).to.eq(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
context('SimpleAccountFactory', () => {
|
context('SimpleAccountFactory', () => {
|
||||||
it('sanity: check deployer', async () => {
|
it('sanity: check deployer', async () => {
|
||||||
const ownerAddr = createAddress()
|
const ownerAddr = createAddress()
|
||||||
|
|||||||
@@ -184,8 +184,7 @@ describe('bls account', function () {
|
|||||||
await fund(senderAddress, '0.01')
|
await fund(senderAddress, '0.01')
|
||||||
const userOp = await fillUserOp({
|
const userOp = await fillUserOp({
|
||||||
sender: senderAddress,
|
sender: senderAddress,
|
||||||
initCode,
|
initCode
|
||||||
nonce: 2
|
|
||||||
}, entrypoint)
|
}, entrypoint)
|
||||||
const requestHash = await blsAgg.getUserOpHash(userOp)
|
const requestHash = await blsAgg.getUserOpHash(userOp)
|
||||||
const sigParts = signer3.sign(requestHash)
|
const sigParts = signer3.sign(requestHash)
|
||||||
|
|||||||
Reference in New Issue
Block a user