Skip to content

Commit

Permalink
AA-173: Remove simulation and view-only methods from the EntryPoint c…
Browse files Browse the repository at this point in the history
…ontract (#321)

* All simulation functions are in EntryPointSimulations contract
* This contract is never deployed. it is applied as "state-override" when doing simulation with eth_call/debug_traceCall
* update EIP to allowed calls back into the EntryPoint
* make sure entryPoint methods callable from validation (deposit) always cost the same or more during simulation.
Co-authored-by: Dror Tirosh <[email protected]>
  • Loading branch information
forshtat authored Sep 12, 2023
1 parent 54da211 commit e996139
Show file tree
Hide file tree
Showing 20 changed files with 934 additions and 644 deletions.
149 changes: 6 additions & 143 deletions contracts/core/EntryPoint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard,

using UserOperationLib for UserOperation;

SenderCreator private immutable senderCreator = new SenderCreator();

// Internal value used during simulation: need to query aggregator.
address private constant SIMULATE_FIND_AGGREGATOR = address(1);
SenderCreator private senderCreator = new SenderCreator();

// Marker for inner call revert on out of gas
bytes32 private constant INNER_OUT_OF_GAS = hex"deaddead";
Expand Down Expand Up @@ -74,7 +71,10 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard,
uint256 opIndex,
UserOperation calldata userOp,
UserOpInfo memory opInfo
) private returns (uint256 collected) {
)
internal
returns
(uint256 collected) {
uint256 preGas = gasleft();
bytes memory context = getMemoryBytesFromOffset(opInfo.contextOffset);

Expand Down Expand Up @@ -217,41 +217,6 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard,
_compensate(beneficiary, collected);
}

/// @inheritdoc IEntryPoint
function simulateHandleOp(
UserOperation calldata op,
address target,
bytes calldata targetCallData
) external override {
UserOpInfo memory opInfo;
_simulationOnlyValidations(op);
(
uint256 validationData,
uint256 paymasterValidationData
) = _validatePrepayment(0, op, opInfo);
ValidationData memory data = _intersectTimeRange(
validationData,
paymasterValidationData
);

numberMarker();
uint256 paid = _executeUserOp(0, op, opInfo);
numberMarker();
bool targetSuccess;
bytes memory targetResult;
if (target != address(0)) {
(targetSuccess, targetResult) = target.call(targetCallData);
}
revert ExecutionResult(
opInfo.preOpGas,
paid,
data.validAfter,
data.validUntil,
targetSuccess,
targetResult
);
}

/**
* A memory copy of UserOp static fields only.
* Excluding: callData, initCode and signature. Replacing paymasterAndData with paymaster.
Expand Down Expand Up @@ -364,64 +329,6 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard,
}
}

/// @inheritdoc IEntryPoint
function simulateValidation(UserOperation calldata userOp) external {
UserOpInfo memory outOpInfo;

_simulationOnlyValidations(userOp);
(
uint256 validationData,
uint256 paymasterValidationData
) = _validatePrepayment(0, userOp, outOpInfo);
StakeInfo memory paymasterInfo = _getStakeInfo(
outOpInfo.mUserOp.paymaster
);
StakeInfo memory senderInfo = _getStakeInfo(outOpInfo.mUserOp.sender);
StakeInfo memory factoryInfo;
{
bytes calldata initCode = userOp.initCode;
address factory = initCode.length >= 20
? address(bytes20(initCode[0:20]))
: address(0);
factoryInfo = _getStakeInfo(factory);
}

ValidationData memory data = _intersectTimeRange(
validationData,
paymasterValidationData
);
address aggregator = data.aggregator;
bool sigFailed = aggregator == address(1);
ReturnInfo memory returnInfo = ReturnInfo(
outOpInfo.preOpGas,
outOpInfo.prefund,
sigFailed,
data.validAfter,
data.validUntil,
getMemoryBytesFromOffset(outOpInfo.contextOffset)
);

if (aggregator != address(0) && aggregator != address(1)) {
AggregatorStakeInfo memory aggregatorInfo = AggregatorStakeInfo(
aggregator,
_getStakeInfo(aggregator)
);
revert ValidationResultWithAggregation(
returnInfo,
senderInfo,
factoryInfo,
paymasterInfo,
aggregatorInfo
);
}
revert ValidationResult(
returnInfo,
senderInfo,
factoryInfo,
paymasterInfo
);
}

/**
* Get the required prefunded gas fee amount for an operation.
* @param mUserOp - The user operation in memory.
Expand Down Expand Up @@ -482,50 +389,6 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard,
revert SenderAddressResult(sender);
}

function _simulationOnlyValidations(
UserOperation calldata userOp
) internal view {
try
this._validateSenderAndPaymaster(
userOp.initCode,
userOp.sender,
userOp.paymasterAndData
)
// solhint-disable-next-line no-empty-blocks
{} catch Error(string memory revertReason) {
if (bytes(revertReason).length != 0) {
revert FailedOp(0, revertReason);
}
}
}

/**
* Called only during simulation.
* This function always reverts to prevent warm/cold storage differentiation in simulation vs execution.
* @param initCode - The smart account constructor code.
* @param sender - The sender address.
* @param paymasterAndData - The paymaster address followed by the token address to use.
*/
function _validateSenderAndPaymaster(
bytes calldata initCode,
address sender,
bytes calldata paymasterAndData
) external view {
if (initCode.length == 0 && sender.code.length == 0) {
// it would revert anyway. but give a meaningful message
revert("AA20 account not deployed");
}
if (paymasterAndData.length >= 20) {
address paymaster = address(bytes20(paymasterAndData[0:20]));
if (paymaster.code.length == 0) {
// It would revert anyway. but give a meaningful message.
revert("AA30 paymaster not deployed");
}
}
// always revert
revert("");
}

/**
* Call account.validateUserOp.
* Revert (with FailedOp) in case validateUserOp reverts, or account didn't send required prefund.
Expand Down Expand Up @@ -707,7 +570,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard,
UserOperation calldata userOp,
UserOpInfo memory outOpInfo
)
private
internal
returns (uint256 validationData, uint256 paymasterValidationData)
{
uint256 preGas = gasleft();
Expand Down
185 changes: 185 additions & 0 deletions contracts/core/EntryPointSimulations.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;

/* solhint-disable avoid-low-level-calls */
/* solhint-disable no-inline-assembly */

import "./EntryPoint.sol";
import "../interfaces/IEntryPointSimulations.sol";

/*
* This contract inherits the EntryPoint and extends it with the view-only methods that are executed by
* the bundler in order to check UserOperation validity and estimate its gas consumption.
* This contract should never be deployed on-chain and is only used as a parameter for the "eth_call" request.
*/
contract EntryPointSimulations is EntryPoint, IEntryPointSimulations {
// solhint-disable-next-line var-name-mixedcase
AggregatorStakeInfo private NOT_AGGREGATED = AggregatorStakeInfo(address(0), StakeInfo(0, 0));

/**
* simulation contract should not be deployed, and specifically, accounts should not trust
* it as entrypoint, since the simulation functions don't check the signatures
*/
constructor() {
require(block.number < 100, "should not be deployed");
}

/// @inheritdoc IEntryPointSimulations
function simulateValidation(
UserOperation calldata userOp
)
external
returns (
ValidationResult memory
){
UserOpInfo memory outOpInfo;

_simulationOnlyValidations(userOp);
(
uint256 validationData,
uint256 paymasterValidationData
) = _validatePrepayment(0, userOp, outOpInfo);
StakeInfo memory paymasterInfo = _getStakeInfo(
outOpInfo.mUserOp.paymaster
);
StakeInfo memory senderInfo = _getStakeInfo(outOpInfo.mUserOp.sender);
StakeInfo memory factoryInfo;
{
bytes calldata initCode = userOp.initCode;
address factory = initCode.length >= 20
? address(bytes20(initCode[0 : 20]))
: address(0);
factoryInfo = _getStakeInfo(factory);
}

ValidationData memory data = _intersectTimeRange(
validationData,
paymasterValidationData
);
address aggregator = data.aggregator;
bool sigFailed = aggregator == address(1);
ReturnInfo memory returnInfo = ReturnInfo(
outOpInfo.preOpGas,
outOpInfo.prefund,
sigFailed,
data.validAfter,
data.validUntil,
getMemoryBytesFromOffset(outOpInfo.contextOffset)
);

AggregatorStakeInfo memory aggregatorInfo = NOT_AGGREGATED;
if (aggregator != address(0) && aggregator != address(1)) {
aggregatorInfo = AggregatorStakeInfo(
aggregator,
_getStakeInfo(aggregator)
);
}
return ValidationResult(
returnInfo,
senderInfo,
factoryInfo,
paymasterInfo,
aggregatorInfo
);
}

/// @inheritdoc IEntryPointSimulations
function simulateHandleOp(
UserOperation calldata op,
address target,
bytes calldata targetCallData
)
external nonReentrant
returns (
ExecutionResult memory
){
UserOpInfo memory opInfo;
_simulationOnlyValidations(op);
(
uint256 validationData,
uint256 paymasterValidationData
) = _validatePrepayment(0, op, opInfo);
ValidationData memory data = _intersectTimeRange(
validationData,
paymasterValidationData
);

numberMarker();
uint256 paid = _executeUserOp(0, op, opInfo);
numberMarker();
bool targetSuccess;
bytes memory targetResult;
if (target != address(0)) {
(targetSuccess, targetResult) = target.call(targetCallData);
}
return ExecutionResult(
opInfo.preOpGas,
paid,
data.validAfter,
data.validUntil,
targetSuccess,
targetResult
);
}

function _simulationOnlyValidations(
UserOperation calldata userOp
)
internal
view
{
try
this._validateSenderAndPaymaster(
userOp.initCode,
userOp.sender,
userOp.paymasterAndData
)
// solhint-disable-next-line no-empty-blocks
{} catch Error(string memory revertReason) {
if (bytes(revertReason).length != 0) {
revert FailedOp(0, revertReason);
}
}
}

/**
* Called only during simulation.
* This function always reverts to prevent warm/cold storage differentiation in simulation vs execution.
* @param initCode - The smart account constructor code.
* @param sender - The sender address.
* @param paymasterAndData - The paymaster address followed by the token address to use.
*/
function _validateSenderAndPaymaster(
bytes calldata initCode,
address sender,
bytes calldata paymasterAndData
) external view {
if (initCode.length == 0 && sender.code.length == 0) {
// it would revert anyway. but give a meaningful message
revert("AA20 account not deployed");
}
if (paymasterAndData.length >= 20) {
address paymaster = address(bytes20(paymasterAndData[0 : 20]));
if (paymaster.code.length == 0) {
// It would revert anyway. but give a meaningful message.
revert("AA30 paymaster not deployed");
}
}
// always revert
revert("");
}

//make sure depositTo cost is more than normal EntryPoint's cost.
// empiric test showed that without this wrapper, simulation depositTo costs less..
function depositTo(address account) public override(IStakeManager, StakeManager) payable {
uint x;
assembly {
//some silly code to waste ~200 gas
x := exp(mload(0),100)
}
if (x == 123) {
return;
}
StakeManager.depositTo(account);
}
}
2 changes: 1 addition & 1 deletion contracts/core/StakeManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ abstract contract StakeManager is IStakeManager {
* Add to the deposit of the given account.
* @param account - The account to add to.
*/
function depositTo(address account) public payable {
function depositTo(address account) public virtual payable {
_incrementDeposit(account, msg.value);
DepositInfo storage info = deposits[account];
emit Deposited(account, info.deposit);
Expand Down
Loading

0 comments on commit e996139

Please sign in to comment.