Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AA-161 validate Nonce in EntryPoint #247

Merged
merged 22 commits into from
Apr 9, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 3 additions & 13 deletions contracts/core/BaseAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ abstract contract BaseAccount is IAccount {

/**
* 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)
*/
function nonce() public view virtual returns (uint256);
function nonce() public view virtual returns (uint256) {
return entryPoint().getNonce(address(this), 0);
}

/**
* return the entryPoint used by this account.
Expand All @@ -41,9 +42,6 @@ abstract contract BaseAccount is IAccount {
external override virtual returns (uint256 validationData) {
_requireFromEntryPoint();
validationData = _validateSignature(userOp, userOpHash);
if (userOp.initCode.length == 0) {
_validateAndUpdateNonce(userOp);
}
_payPrefund(missingAccountFunds);
}

Expand All @@ -70,14 +68,6 @@ abstract contract BaseAccount is IAccount {
function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash)
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.
*/
function _validateAndUpdateNonce(UserOperation calldata userOp) internal virtual;

/**
* sends to the entrypoint (msg.sender) the missing funds for this transaction.
* subclass MAY override this method for better funds management
Expand Down
14 changes: 11 additions & 3 deletions contracts/core/EntryPoint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -345,11 +346,13 @@ contract EntryPoint is IEntryPoint, StakeManager {
/**
* Get counterfactual sender address.
* Calculate the sender contract address that will be generated by the initCode and salt in the UserOperation.
* this method always revert, and returns the address in SenderAddressResult error
* this method always revert, and returns the address (and the nonce) in SenderAddressResult error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not returning nonce anymore

* @param initCode the constructor code to be passed into the UserOperation.
*/
function getSenderAddress(bytes calldata initCode) public {
drortirosh marked this conversation as resolved.
Show resolved Hide resolved
revert SenderAddressResult(senderCreator.createSender(initCode));
address sender = senderCreator.createSender(initCode);
uint256 nonce = getNonce(sender, 0);
revert SenderAddressResult(sender, nonce);
}

function _simulationOnlyValidations(UserOperation calldata userOp) internal view {
Expand Down Expand Up @@ -511,6 +514,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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed some pattern on old sample accounts that it skip nonce increment when initCode.length > 0.
I assume it's for gas savings, but I don't see it here. Is it intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was an internal optimization of SimpleWallet.

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();
Expand Down
37 changes: 37 additions & 0 deletions contracts/core/NonceManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;

import "../interfaces/IEntryPoint.sol";

/**
* nonce management functionality
*/
contract NonceManager is INonceManager {

mapping(address => mapping(uint192 => uint256)) public nonces;

function getNonce(address sender, uint192 key)
public view override returns (uint256 nonce) {
return nonces[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 {
drortirosh marked this conversation as resolved.
Show resolved Hide resolved
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wallet can call incrementNonce in _validateSignature, just before checking in _validateAndUpdateNonce, which makes it feasible to execute OP with nonce 3 when the next nonce is 2. So I suggest to check nonce _validateAndUpdateNonce before _validateSignature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that in make sense to modify the nonce before calling a user-provided method.
Still, our opcode rules prevent the account from calling the entryPoint (including incrementNonce) from within validation.

nonces[msg.sender][key]++;
drortirosh marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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 nonces[sender][key]++ == seq;
}

}
7 changes: 4 additions & 3 deletions contracts/interfaces/IEntryPoint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -93,7 +94,7 @@ interface IEntryPoint is IStakeManager {
/**
* return value of getSenderAddress
*/
error SenderAddressResult(address sender);
error SenderAddressResult(address sender, uint nonce);

/**
* return value of simulateHandleOp
Expand Down Expand Up @@ -174,7 +175,7 @@ interface IEntryPoint is IStakeManager {
/**
* Get counterfactual sender address.
* Calculate the sender contract address that will be generated by the initCode and salt in the UserOperation.
* this method always revert, and returns the address in SenderAddressResult error
* this method always revert, and returns the address (and nonce) in SenderAddressResult error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't return nonce

* @param initCode the constructor code to be passed into the UserOperation.
*/
function getSenderAddress(bytes memory initCode) external;
Expand Down
16 changes: 16 additions & 0 deletions contracts/interfaces/INonceManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;

interface INonceManager {

/**
* return the next nonce for this sender
* @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);

function incrementNonce(uint192 key) external;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add documentation, explaining that this function doesn't need to be called by the account?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, it is not needed at all. I just wonder if we should keep it for completeness, or remove it altogether..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was needed for the "bypass" case where an account wants to support non-EntryPoint interactions and still use the same nonce. Is that no longer the case?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think one side-effect of this function is breaking continuity of nonce, that lose the 'one nonce corresponding to one OP' property.

For example, wallet can call _validateSignature in OP calldata to increase nonce by 2 in one single OP.

Also, wallet can call incrementNonce in _validateSignature, just before checking in _validateAndUpdateNonce, which makes it feasible to execute OP with nonce 3 when the next nonce is 2. So I suggest to check nonce _validateAndUpdateNonce before _validateSignature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The match of "one nonce to one OP" exists, but in any case, it can't be trusted as a counter: e.g. "current nonce is # of past transactions" is not true, because:

  • you can incrementNonce(), but also:
  • you can as many different key values, each with another sent of transactions.
  • accounts can support "batch", which means a single UserOp can map to multiple transactions.

In short, at the system level, nonces are nothing but replay protection..

}
12 changes: 0 additions & 12 deletions contracts/samples/SimpleAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ contract SimpleAccount is BaseAccount, UUPSUpgradeable, Initializable {
// 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;
Expand All @@ -37,11 +35,6 @@ contract SimpleAccount is BaseAccount, UUPSUpgradeable, Initializable {
_;
}

/// @inheritdoc BaseAccount
function nonce() public view virtual override returns (uint256) {
return _nonce;
}

/// @inheritdoc BaseAccount
function entryPoint() public view virtual override returns (IEntryPoint) {
return _entryPoint;
Expand Down Expand Up @@ -99,11 +92,6 @@ contract SimpleAccount is BaseAccount, UUPSUpgradeable, Initializable {
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) {
Expand Down
12 changes: 11 additions & 1 deletion contracts/samples/gnosis/EIP4337Fallback.sol
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ contract EIP4337Fallback is DefaultCallbackHandler, IAccount, IERC1271 {
return abi.decode(ret, (uint256));
}

/**
* Helper for wallet to get the next nonce.
* (NOTE: can't be named "nonce()", since it overrides the GnosisSafe internal "nonce" function, which we ignore,
* as we use the EntryPoint's 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
*/
Expand Down Expand Up @@ -78,4 +88,4 @@ contract EIP4337Fallback is DefaultCallbackHandler, IAccount, IERC1271 {
return 0xffffffff;
}
}
}
}
16 changes: 10 additions & 6 deletions contracts/samples/gnosis/EIP4337Manager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,6 @@ contract EIP4337Manager is IAccount, GnosisSafeStorage, Executor {
validationData = SIG_VALIDATION_FAILED;
}

drortirosh marked this conversation as resolved.
Show resolved Hide resolved
if (userOp.initCode.length == 0) {
require(uint256(nonce) == userOp.nonce, "account: invalid nonce");
nonce = bytes32(uint256(nonce) + 1);
}

if (missingAccountFunds > 0) {
//Note: MAY pay more than the minimum, to deposit for future transactions
(bool success,) = payable(msgSender).call{value : missingAccountFunds}("");
Expand Down Expand Up @@ -105,6 +100,14 @@ contract EIP4337Manager is IAccount, GnosisSafeStorage, Executor {
}
}

/**
* Helper for wallet to get the next nonce.
* (NOTE: can't be named "nonce()", since it overrides the GnosisSafe internal "nonce" function, which we ignore,
* as we use the EntryPoint's nonce)
*/
function getNonce() public view returns (uint256) {
return IEntryPoint(entryPoint).getNonce(address(this), 0);
}

/**
* set up a safe as EIP-4337 enabled.
Expand Down Expand Up @@ -158,7 +161,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);
uint nonce = uint256(IEntryPoint(manager.entryPoint()).getNonce(address(safe), 0));
drortirosh marked this conversation as resolved.
Show resolved Hide resolved
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()));
Expand Down
5 changes: 3 additions & 2 deletions contracts/test/MaliciousAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
1 change: 1 addition & 0 deletions deploy/2_deploy_SimpleAccountFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
30 changes: 28 additions & 2 deletions gascalc/GasChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -111,6 +112,8 @@ export class GasChecker {
])
}

createdAccounts = new Set<string>()

/**
* create accounts up to this counter.
* make sure they all have balance.
Expand All @@ -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)
Expand All @@ -140,6 +161,9 @@ export class GasChecker {
await GasCheckCollector.inst.entryPoint.depositTo(addr, { value: minDepositOrBalance.mul(5) })
}
}
console.log('ops=', creationOps)
await this.entryPoint().handleOps(creationOps, ethersSigner.getAddress())
console.log('post-op')
}

/**
Expand Down Expand Up @@ -238,7 +262,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
}
Expand Down
Loading