-
Notifications
You must be signed in to change notification settings - Fork 624
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
Conversation
merge v0.5 to main
- NonceManager to handle 2d nonces - _validateAndUpdateNonce() called after validateUserOp - getNonce(sender, key) - remove nonce checking from BaseAccount - BaseAccount implements nonce() using ep.getNonce
@@ -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)) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
contracts/samples/SimpleAccount.sol
Outdated
//increment nonce during construction. | ||
// This way, the cost of moving from "0" to "1" is absorbed | ||
// by first (construction) transaction, instead of during the 2nd transaction | ||
entryPoint().incrementNonce(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know that this part is just a sample but just out of curiosity, since this will be called on construction and userOp.nonce will start with 1 as far as i know since validateUserOp will check for nonce after initialize. Is this a best practice?
And on my understanding, this nonce will be increased to 1 after validateUserOp even without this. Why bother going 0 -> 1 -> 2 for first userOp? instead of going 0 -> 1?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was done so that even wallets that are not created via a dedicated UserOp, will have their next operation gas "clean".
A wallet can be created by execution its initCode either by the UserOp, or by other means.
Calling "incrementNonce" the first time will cost 20000 gas, and any further call 5000 (I ignore for now explicitly setting to a new key, which is will cost 20000 again, but that an explicit operation a wallet owner does)
Not incrementing it during construction will mean first TX after construction will cost 20000 gas more than any further TX. We prefer to make gas-per-transaction as consistent as possible, and thus make only the construction transaction different.
Calling "incrementNonce" from the factory, indeed causes double-increment of the nonce, but the overhead is minimal (~400 gas)
The side-effect is indeed an extra increment of the nonce.
create account doesn't increment nonce automatically. The nonce is incremented if it is created through a UserOp Side-effect: if account is created NOT through AA, then first UserOp will cost 20000 gas more. (if created as a userop, then this extra cost is part of the "creation" and not on a separate transaction.
function getNonce(address sender, uint192 key) | ||
external view returns (uint256 nonce); | ||
|
||
function incrementNonce(uint192 key) external; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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..
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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..
contracts/core/EntryPoint.sol
Outdated
@@ -345,11 +346,12 @@ 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not returning nonce anymore
contracts/interfaces/IEntryPoint.sol
Outdated
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't return nonce
@@ -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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we changed EIP to ERC
nonce() is no longer "do-nothing" method. use instead account()
function getNonce(address sender, uint192 key) | ||
external view returns (uint256 nonce); | ||
|
||
function incrementNonce(uint192 key) external; |
There was a problem hiding this comment.
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
.
// (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 { |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
* 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
* 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
* 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
account doesn't check nonce. instead, entryPoint validate and increment nonce.
nonces are two-dimensional, with 192 bit of "key" and 64 bit of sequence.
For description about this change, please see this doc