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

Binary search verification gas limit #8

Merged
merged 7 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ for pair in "${pairs[@]}"; do
if [[ -n "$ETHERSCAN_KEY" && -n "$ETHERSCAN_URL" ]]; then
echo "Verifying with $ETHERSCAN_URL using key $ETHERSCAN_KEY"

forge verify-contract 0x74Cb5e4eE81b86e70f9045036a1C5477de69eE87 src/PimlicoEntryPointSimulations.sol:PimlicoEntryPointSimulations \
forge verify-contract 0xe1b9bcD4DbfAE61585691bdB9A100fbaAF6C8dB0 src/v07/PimlicoEntryPointSimulations.sol:PimlicoEntryPointSimulations \
--verifier-url "$ETHERSCAN_URL" \
--etherscan-api-key "$ETHERSCAN_KEY"

forge verify-contract 0xf384fddcaf70336dca46404d809153a0029a0253 src/EntryPointSimulations.sol:EntryPointSimulations \
forge verify-contract 0x29C6bBdd6F4433f8d188616ADd22F842740ee982 src/v07/EntryPointSimulations.sol:EntryPointSimulations \
--verifier-url "$ETHERSCAN_URL" \
--etherscan-api-key "$ETHERSCAN_KEY"

Expand Down
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[profile.default]
viaIr = true
evm_version = "london"
solc_version = "0.8.23"
7 changes: 7 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
account-abstraction-v6/=lib/account-abstraction-v6/contracts/
account-abstraction/=lib/account-abstraction/contracts/
ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/
erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/
forge-std/=lib/forge-std/src/
openzeppelin-contracts/=lib/openzeppelin-contracts/
63 changes: 59 additions & 4 deletions src/v07/EntryPoint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,8 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard
uint256 opIndex,
PackedUserOperation calldata op,
UserOpInfo memory opInfo,
uint256 requiredPreFund
uint256 requiredPreFund,
uint256 pmVerificationGasLimit
) internal returns (bytes memory context, uint256 validationData) {
unchecked {
uint256 preGas = gasleft();
Expand All @@ -450,7 +451,6 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard
revert FailedOp(opIndex, "AA31 paymaster deposit too low");
}
paymasterInfo.deposit = deposit - requiredPreFund;
uint256 pmVerificationGasLimit = mUserOp.paymasterVerificationGasLimit;
try IPaymaster(paymaster).validatePaymasterUserOp{gas: pmVerificationGasLimit}(
op, opInfo.userOpHash, requiredPreFund
) returns (bytes memory _context, uint256 _validationData) {
Expand Down Expand Up @@ -519,6 +519,60 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard
aggregator = data.aggregator;
}

function _accountValidation(uint256 opIndex, PackedUserOperation calldata userOp, UserOpInfo memory outOpInfo)
public
returns (uint256 validationData, uint256 paymasterValidationData, uint256 paymasterVerificationGasLimit)
{
uint256 preGas = gasleft();
MemoryUserOp memory mUserOp = outOpInfo.mUserOp;
_copyUserOpToMemory(userOp, mUserOp);
outOpInfo.userOpHash = getUserOpHash(userOp);

// Validate all numeric values in userOp are well below 128 bit, so they can safely be added
// and multiplied without causing overflow.
uint256 verificationGasLimit = mUserOp.verificationGasLimit;
uint256 maxGasValues = mUserOp.preVerificationGas | verificationGasLimit | mUserOp.callGasLimit
| mUserOp.paymasterVerificationGasLimit | mUserOp.paymasterPostOpGasLimit | mUserOp.maxFeePerGas
| mUserOp.maxPriorityFeePerGas;
require(maxGasValues <= type(uint120).max, "AA94 gas values overflow");

uint256 requiredPreFund = _getRequiredPrefund(mUserOp);
validationData = _validateAccountPrepayment(opIndex, userOp, outOpInfo, requiredPreFund, gasleft());

if (!_validateAndUpdateNonce(mUserOp.sender, mUserOp.nonce)) {
revert FailedOp(opIndex, "AA25 invalid account nonce");
}

unchecked {
if (preGas - gasleft() > verificationGasLimit) {
revert FailedOp(opIndex, "AA26 over verificationGasLimit");
}
}

bytes memory context;
uint256 remainingGas = gasleft();
if (mUserOp.paymaster != address(0)) {}
unchecked {
outOpInfo.prefund = requiredPreFund;
outOpInfo.contextOffset = getOffsetOfMemoryBytes(context);
outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas;
}
paymasterVerificationGasLimit = ((remainingGas - gasleft()) * 115) / 100;
}

function _paymasterValidation(uint256 opIndex, PackedUserOperation calldata userOp, UserOpInfo memory outOpInfo)
public
returns (uint256 validationData, uint256 paymasterValidationData, uint256 paymasterVerificationGasLimit)
{
MemoryUserOp memory mUserOp = outOpInfo.mUserOp;
bytes memory context;
uint256 requiredPreFund = _getRequiredPrefund(mUserOp);
if (mUserOp.paymaster != address(0)) {
(context, paymasterValidationData) =
_validatePaymasterPrepayment(opIndex, userOp, outOpInfo, requiredPreFund, gasleft());
}
}

/**
* Validate account and paymaster (if defined) and
* also make sure total validation doesn't exceed verificationGasLimit.
Expand All @@ -527,7 +581,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard
* @param userOp - The userOp to validate.
*/
function _validatePrepayment(uint256 opIndex, PackedUserOperation calldata userOp, UserOpInfo memory outOpInfo)
internal
public
returns (uint256 validationData, uint256 paymasterValidationData, uint256 paymasterVerificationGasLimit)
{
uint256 preGas = gasleft();
Expand Down Expand Up @@ -559,8 +613,9 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard
bytes memory context;
uint256 remainingGas = gasleft();
if (mUserOp.paymaster != address(0)) {
uint256 pmVerificationGasLimit = outOpInfo.mUserOp.paymasterVerificationGasLimit;
(context, paymasterValidationData) =
_validatePaymasterPrepayment(opIndex, userOp, outOpInfo, requiredPreFund);
_validatePaymasterPrepayment(opIndex, userOp, outOpInfo, requiredPreFund, pmVerificationGasLimit);
}
unchecked {
outOpInfo.prefund = requiredPreFund;
Expand Down
151 changes: 133 additions & 18 deletions src/v07/EntryPointSimulations.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations {
UserOpInfo memory outOpInfo;

_simulationOnlyValidations(userOp);
(uint256 validationData, uint256 paymasterValidationData,) = // uint256 paymasterVerificationGasLimit
_validatePrepayment(0, userOp, outOpInfo);
(
uint256 validationData,
uint256 paymasterValidationData, // uint256 paymasterVerificationGasLimit
) = _validatePrepayment(0, userOp, outOpInfo);

_validateAccountAndPaymasterValidationData(0, validationData, paymasterValidationData, address(0));

Expand Down Expand Up @@ -117,12 +119,11 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations {
}

// Helper function to perform the simulation and capture results from revert bytes.
function simulateCall(address entryPoint, address target, bytes calldata data, uint256 gas)
function simulateCall(address entryPoint, bytes calldata payload, uint256 gas)
external
returns (bool success, bytes memory result)
{
bytes memory payload = abi.encodeWithSelector(this.simulateCallAndRevert.selector, target, data, gas);
try EP(payable(entryPoint)).delegateAndRevert(address(thisContract), payload) {}
try EP(payable(entryPoint)).delegateAndRevert{gas: gas}(address(thisContract), payload) {}
catch (bytes memory reason) {
bytes memory reasonData = new bytes(reason.length - 4);
for (uint256 i = 4; i < reason.length; i++) {
Expand All @@ -132,18 +133,128 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations {
}
}

function processQueuedUserOps(SimulationArgs[] calldata queuedUserOps) internal {
// Run all queued userOps to ensure that state is valid for the target userOp.
for (uint256 i = 0; i < queuedUserOps.length; i++) {
UserOpInfo memory queuedOpInfo;
SimulationArgs calldata args = queuedUserOps[i];
_simulationOnlyValidations(args.op);
_validatePrepayment(0, args.op, queuedOpInfo);

if (args.target == address(0)) {
continue;
}

args.target.call(args.targetCallData);
}
}

function binarySearchGasLimit(
SimulationArgs[] calldata queuedUserOps,
SimulationArgs calldata targetUserOp,
address entryPoint,
uint256 initialMinGas,
uint256 toleranceDelta,
uint256 gasAllowance,
bytes memory payload
) internal returns (TargetCallResult memory) {
processQueuedUserOps(queuedUserOps);
// Extract out the target userOperation info.

// Run our target userOperation.
_simulationOnlyValidations(targetUserOp.op);

uint256 minGas;
bool targetSuccess;
bytes memory targetResult;

if (initialMinGas > 0) {
targetSuccess = true;
targetResult = hex"";
minGas = initialMinGas;
} else {
// Find the minGas (reduces number of iterations + checks if the call reverts).
uint256 remainingGas = gasleft();

(targetSuccess, targetResult) = thisContract.simulateCall(entryPoint, payload, gasleft());
minGas = remainingGas - gasleft();

// If the call reverts then don't binary search.
if (!targetSuccess) {
return TargetCallResult(0, targetSuccess, targetResult);
}
}

uint256 maxGas = minGas + gasAllowance;
uint256 optimalGas = maxGas;

while ((maxGas - minGas) >= toleranceDelta) {
// Check that we can do one more run.
if (gasleft() < minGas + 5_000) {
revert SimulationOutOfGas(optimalGas, minGas, maxGas);
}

uint256 midGas = (minGas + maxGas) / 2;

(bool success, bytes memory result) = thisContract.simulateCall(entryPoint, payload, midGas);

if (success) {
// If the call is successful, reduce the maxGas and store this as the candidate
optimalGas = midGas;
maxGas = midGas - 1;
targetResult = result;
} else {
// If it fails, we need more gas, so increase the minGas
minGas = midGas + 1;
}
}

return TargetCallResult(optimalGas, targetSuccess, targetResult);
}

function binarySearchPaymasterVerificationGasLimit(
SimulationArgs[] calldata queuedUserOps,
SimulationArgs calldata targetUserOp,
address entryPoint,
uint256 initialMinGas,
uint256 toleranceDelta,
uint256 gasAllowance
) public returns (TargetCallResult memory) {
UserOpInfo memory opInfo;
bytes memory payload = abi.encodeWithSelector(this._paymasterValidation.selector, 0, targetUserOp.op, opInfo);

return binarySearchGasLimit(
queuedUserOps, targetUserOp, entryPoint, initialMinGas, toleranceDelta, gasAllowance, payload
);
}

function binarySearchVerificationGasLimit(
SimulationArgs[] calldata queuedUserOps,
SimulationArgs calldata targetUserOp,
address entryPoint,
uint256 initialMinGas,
uint256 toleranceDelta,
uint256 gasAllowance
) public returns (TargetCallResult memory) {
UserOpInfo memory opInfo;
bytes memory payload = abi.encodeWithSelector(this._accountValidation.selector, 0, targetUserOp.op, opInfo);
return binarySearchGasLimit(
queuedUserOps, targetUserOp, entryPoint, initialMinGas, toleranceDelta, gasAllowance, payload
);
}

/*
* Helper function to estimate the call gas limit for a given userOperation.
* The userOperation's callGasLimit is found by performing a onchain binary search.
*
* @param queuedUserOps - The userOperations that should be simulated before the targetUserOperation.
* @param targetUserOp - The userOperation to simulate.
* @param entryPoint - The address of the entryPoint contract.
* @param toleranceDelta - The maximum difference between the estimated gas and the actual gas.
* @param initialMinGas - The initial gas value to start the binary search with.
* @param gasAllowance - The margin to add to the binary search to account for overhead.
* @return optimalGas - The estimated gas limit for the call.
*/
* Helper function to estimate the call gas limit for a given userOperation.
* The userOperation's callGasLimit is found by performing a onchain binary search.
*
* @param queuedUserOps - The userOperations that should be simulated before the targetUserOperation.
* @param targetUserOp - The userOperation to simulate.
* @param entryPoint - The address of the entryPoint contract.
* @param toleranceDelta - The maximum difference between the estimated gas and the actual gas.
* @param initialMinGas - The initial gas value to start the binary search with.
* @param gasAllowance - The margin to add to the binary search to account for overhead.
* @return optimalGas - The estimated gas limit for the call.
*/
function simulateCallData(
SimulationArgs[] calldata queuedUserOps,
SimulationArgs calldata targetUserOp,
Expand Down Expand Up @@ -191,7 +302,9 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations {
} else {
// Find the minGas (reduces number of iterations + checks if the call reverts).
uint256 remainingGas = gasleft();
(targetSuccess, targetResult) = thisContract.simulateCall(entryPoint, target, targetCallData, gasleft());
bytes memory payload =
abi.encodeWithSelector(this.simulateCallAndRevert.selector, target, targetCallData, gasleft());
(targetSuccess, targetResult) = thisContract.simulateCall(entryPoint, payload, gasleft());
minGas = remainingGas - gasleft();

// If the call reverts then don't binary search.
Expand All @@ -212,7 +325,9 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations {

uint256 midGas = (minGas + maxGas) / 2;

(bool success, bytes memory result) = thisContract.simulateCall(entryPoint, target, targetCallData, midGas);
bytes memory payload =
abi.encodeWithSelector(this.simulateCallAndRevert.selector, target, targetCallData, midGas);
(bool success, bytes memory result) = thisContract.simulateCall(entryPoint, payload, gasleft());

if (success) {
// If the call is successful, reduce the maxGas and store this as the candidate
Expand Down
Loading