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-173: Remove simulation and view-only methods from the EntryPoint contract #321

Merged
merged 42 commits into from
Sep 12, 2023

Conversation

forshtat
Copy link
Collaborator

@forshtat forshtat commented Aug 1, 2023

No description provided.

@forshtat forshtat changed the title WIP: Separate simulation entry point AA-173: Remove simulation and view-only methods from the EntryPoint contract Aug 1, 2023
.github/workflows/build.yml Outdated Show resolved Hide resolved
test/anvil/entrypointsimulations.test.ts Outdated Show resolved Hide resolved
maxFeePerGas: 0
}, accountOwner1, entryPoint)
// must succeed with enough verification gas.
await simulateValidation(op0, entryPoint.address, { gas: '0xF4240' })
Copy link
Contributor

Choose a reason for hiding this comment

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

why hex?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The third parameter of simulateValidation is a transaction details object and is fed directly to the eth_call RPC method as {...tx}. It does not pass through the Ethers.js where gasLimit?: string | number | BigNumber; is translated into a hex gas field.

expect(error.message).to.match(/initCode failed or OOG/, error)
})

// TODO: this test is impossible to do with the "state override" approach
Copy link
Contributor

Choose a reason for hiding this comment

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

since anvil does support debug_traceCall, it is possible to write it better.
but this test is very partial.
should instead say:
for actual opcode and storage rule restrictions, see the reference bundler ValidationManager

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added comment.

})
})

// describe('#simulateHandleOp', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

the one thing missing from simulateHandleOp (which is why we need this "Simulation" contract) is to return the actualGasUsed and success/revert reason of actual call.
they are dumped in an event deep inside innerHandleOp, and the only simple way to propagate them out to the calling "simulateHandleOp" is using storage - a big no-no for a real production code, but good enough for simulate function.

@@ -87,57 +87,17 @@ interface IEntryPoint is IStakeManager, INonceManager {
* so a failure can be attributed to the correct entity.
*/
error FailedOp(uint256 opIndex, string reason);
error FailedOp2(address opIndex, string reason);
Copy link
Contributor

Choose a reason for hiding this comment

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

what is this for?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed.

@drortirosh
Copy link
Contributor

also, to fix the code-coverage, check the COVERAGE env. variable to skip the failed test

Base automatically changed from next_v0.7 to tmpdev August 15, 2023 11:44
Base automatically changed from tmpdev to develop August 15, 2023 12:07
await expect(entryPoint.simulateValidation(userOp, { gasLimit: 1e6 }))
.to.revertedWith('ValidationResult')
const snapshot = await ethers.provider.send('evm_snapshot', [])
await entryPoint.simulateValidation(userOp, { gasLimit: 1e6 })
Copy link
Contributor

Choose a reason for hiding this comment

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

use callStatic, and no need for snapshot/revert

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

}

export async function deployEntryPoint (provider = ethers.provider): Promise<EntryPoint> {
export async function deployEntryPointSimulations (provider = ethers.provider): Promise<EntryPointSimulations> {
Copy link
Contributor

Choose a reason for hiding this comment

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

why rename the method? leave the name "deployEntryPoint", and it would avoid a lot of source modifications...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

await expect(entryPoint.simulateValidation(badOp, { gasLimit: 3e5 }))
.to.revertedWith('ValidationResult')
const snapshot = await ethers.provider.send('evm_snapshot', [])
await entryPoint.simulateValidation(badOp, { gasLimit: 3e5 })
Copy link
Contributor

Choose a reason for hiding this comment

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

use callStatic, and no need for snapshot/revert

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

aggregator,
_getStakeInfo(aggregator)
);
return ValidationResult(
Copy link
Contributor

Choose a reason for hiding this comment

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

better "merge" the 2 separate struct returns.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.


function _simulationOnlyValidations(
UserOperation calldata userOp
) private view {
Copy link
Contributor

Choose a reason for hiding this comment

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

internal

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Changed.

@drortirosh
Copy link
Contributor

Comments by @yoavw :

A couple of questions:

  1. I don't see any gas measurements in the EntryPointSimulations so I'm not sure I understand how it works. I understand it's now an eth_call rather than traceCall, so how are the different gas parameters passed to the caller?
  2. Did everyone review it and look for redpill attacks? Let's all make sure there's no way to detect simulation due to this change.
  3. In simulateHandleOp there are numberMarker() calls. I thought we removed tracing for this, and just use eth_call. Are these markers still used for something?
  4. Part of our censorship resistance is that if bundlers are censoring a user, the user can always submit a single-op bundle directly. Does this method make it possible for wallets to estimate gas without a running a full bundler? Do public nodes allow the required state injection for this simulation method?
  5. We to doc the new method in the EIP, and maybe also a separate doc about simulation to communicate the change to all bundler devs.

@drortirosh
Copy link
Contributor

  1. gas measurements: this PR is a first step: it moves the view functions into a separate contract, and remove the need for "Revert" structure, but otherwise, doesn't change their calculation. a separate PR should change simulateHandleOp so that it could return out the actualGasUsed and success (which are currently only emitted in a log)
  2. @shahafn - please also review the code.
  3. numberMarkers will go away, in a next PR (as explained above)
  4. stateOverride is a parameter of eth_call, and do not require tracing (so in a way, this change actually makes it easier for wallets to estimate gas, since for actual callGasLimit, calculation they currently require traceCall - at least for estimating first UserOp, since you can "hack" if the account is already deployed
  5. right. need to update the EIP to accomodate with this change. @forshtat ?

@shahafn
Copy link
Contributor

shahafn commented Aug 17, 2023

There is one way that is a potential exploit:

The new approach means that there are two different EntryPoint contracts, one for simulation and one for execution.
This means that calls to the very same function (e.g. depositTo()) may have slightly different gas costs (by a diff of ~30 gas).
This depends on the code diff between EntryPoint and EntryPointSimulations, and it may also change between each modification we make to either of them.

In the case where EntryPointSimulations.depositTo() costs 30 gas less than EntryPoint.depositTo(), for instance, an account can call depositTo() during validation 20 times, amplifying the diff to 600 gas, then emit an event. This will pass in simulation since it's cheaper, but fail on actual EntryPoint.

A possible mitigation is to allow only one call to depositTo() during validation, but I think that with gas calculation precise enough, even 30 gas diff can be abused (e.g. through emitting events).

Wdyt?

@yoavw
Copy link
Contributor

yoavw commented Aug 17, 2023

@shahafn , @drortirosh and I discussed this a couple of days ago and I believe the conclusion was that every function in EntryPointSimulations must take at least the same amount of gas as its EntryPoint implementation. If it's marginal, add an extra SSTORE and deduct this fixed cost from the reported gas cost. This way there are no cases where something results in OOG on chain after passing simulation successfully. We're ok if OOG happens during simulation - we just increase the gas limit. What we're trying to prevent is on-chain OOG.

@drortirosh drortirosh force-pushed the separate_simulation_entry_point branch from fb0261d to e2f4703 Compare August 21, 2023 14:59
@drortirosh drortirosh force-pushed the separate_simulation_entry_point branch from f43a535 to c254faf Compare August 29, 2023 09:34
@yoavw yoavw mentioned this pull request Sep 2, 2023
@drortirosh drortirosh force-pushed the separate_simulation_entry_point branch from a9caa9c to ac5a4c9 Compare September 6, 2023 13:03
@drortirosh drortirosh merged commit e996139 into develop Sep 12, 2023
8 checks passed
@drortirosh drortirosh deleted the separate_simulation_entry_point branch January 28, 2024 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants