Skip to content
48 changes: 48 additions & 0 deletions contracts/core/EntryPoint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import "../interfaces/IAccount.sol";
import "../interfaces/IAccountExecute.sol";
import "../interfaces/IEntryPoint.sol";
import "../interfaces/IPaymaster.sol";
import "../interfaces/IReserveBalance.sol";

import "./UserOperationLib.sol";
import "./StakeManager.sol";
Expand Down Expand Up @@ -43,6 +44,8 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ERC165, EIP712 {
// Marker for inner call revert on out of gas
bytes32 private constant INNER_OUT_OF_GAS = hex"deaddead";
bytes32 private constant INNER_REVERT_LOW_PREFUND = hex"deadaa51";
// we pick ba to distinguish from the aa series of revert codes, to avoid confusion with actual prefund-related reverts (e.g. AA31, AA32, AA36)
bytes32 private constant INNER_REVERT_DIPPED_INTO_RESERVE = hex"deadba51";

uint256 private constant REVERT_REASON_MAX_LEN = 2048;
// Penalty charged for either unused execution gas or postOp gas
Expand All @@ -60,6 +63,8 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ERC165, EIP712 {

bytes32 transient private currentUserOpHash;

address private constant RESERVE_BALANCE = 0x0000000000000000000000000000000000001001;

error Reentrancy();

constructor() EIP712(DOMAIN_NAME, DOMAIN_VERSION) {
Expand Down Expand Up @@ -279,6 +284,13 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ERC165, EIP712 {
_emitPrefundTooLow(opInfo);
_emitUserOperationEvent(opInfo, false, actualGasCost, actualGas);
collected = actualGasCost;
} else if (innerRevertCode == INNER_REVERT_DIPPED_INTO_RESERVE) {
// innerCall reverted after execution due to reserve balance violation.
uint256 actualGas = preGas - gasleft() + opInfo.preOpGas;
uint256 actualGasCost = opInfo.prefund;
_emitReserveBalanceViolatedEvent(opInfo);
_emitUserOperationEvent(opInfo, false, actualGasCost, actualGas);
collected = actualGasCost;
} else {
uint256 freePtr = _getFreePtr();
emit PostOpRevertReason(
Expand Down Expand Up @@ -333,6 +345,19 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ERC165, EIP712 {
);
}

/**
* Emit the UserOperationReserveBalanceViolated event for the given UserOperation.
*
* @param opInfo - The details of the current UserOperation.
*/
function _emitReserveBalanceViolatedEvent(UserOpInfo memory opInfo) internal virtual {
emit UserOperationReserveBalanceViolated(
opInfo.userOpHash,
opInfo.mUserOp.sender,
opInfo.mUserOp.nonce
);
}

/**
* Iterate over calldata PackedUserOperation array and perform account and paymaster validation.
* @notice UserOpInfo is a global array of all UserOps while PackedUserOperation is grouped per aggregator.
Expand Down Expand Up @@ -444,12 +469,35 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ERC165, EIP712 {
}
}

if (_dippedIntoReserve()) {
// if a userop dips into the reserve balance, abort bundle exec
assembly ("memory-safe") {
mstore(0, INNER_REVERT_DIPPED_INTO_RESERVE)
revert(0, 32)
}
}

unchecked {
uint256 actualGas = preGas - gasleft() + opInfo.preOpGas;
return _postExecution(mode, opInfo, context, actualGas);
}
}

/**
* Check if the reserve balance was dipped into by making a call to the reserve balance precompile.
* If the call fails or returns an invalid value, we assume the worst and treat it as if the reserve balance was dipped into.
* @dev The precompile only supports CALL on this specific selector
*/
function _dippedIntoReserve() internal returns (bool dipped) {
(bool success, bytes memory ret) = RESERVE_BALANCE.call(
abi.encodeWithSelector(IReserveBalance.dippedIntoReserve.selector)
);
if (!success || ret.length != 32) {
return true;

Choose a reason for hiding this comment

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

As a design point: if this is instead return false this becomes chain independent and, when run on non-monad chains that do not have reserve balance, will correctly return that the contract is not in a reverting state.

Copy link
Member Author

Choose a reason for hiding this comment

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

we did consider it but it's highly unlikely that this would be deployed on other chains due to the difference in bytecode (and thus, address) and also the side-effect in case a different precompile was to be made available at the same address. i'm erring on the side on caution here but we can consider a chain ID-based condition if we do need it to be agnostic (just that we haven't found a good reason yet).

}
dipped = abi.decode(ret, (bool));
}

/**
* Copy general fields from userOp into the memory opInfo structure.
* @param userOp - The user operation.
Expand Down
12 changes: 12 additions & 0 deletions contracts/interfaces/IEntryPoint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@ interface IEntryPoint is IStakeManager, INonceManager {
uint256 nonce
);

/**
* UserOp violated reserve balance constraints. The UserOperation is reverted, and no refund is made.
* @param userOpHash - The request unique identifier.
* @param sender - The sender of this request.
* @param nonce - The nonce used in the request.
*/
event UserOperationReserveBalanceViolated(
bytes32 indexed userOpHash,
address indexed sender,
uint256 nonce
);

/**
* An event emitted by handleOps() and handleAggregatedOps(), before starting the execution loop.
* Any event emitted before this event, is part of the validation.
Expand Down
6 changes: 6 additions & 0 deletions contracts/interfaces/IReserveBalance.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

interface IReserveBalance {
function dippedIntoReserve() external returns (bool);
}
29 changes: 26 additions & 3 deletions gascalc/GasChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,13 +272,19 @@ export class GasChecker {
}
const rcpt = await ret.wait()
const gasUsed = rcpt.gasUsed.toNumber()
const countSuccessOps = rcpt.events?.filter(e => e.event === 'UserOperationEvent' && e.args?.success).length
const countSuccessOps = rcpt.events?.filter(e => e.event === 'UserOperationEvent' && e.args?.success === true).length ?? 0
const reserveViolationEvents = rcpt.events?.filter(e => e.event === 'UserOperationReserveBalanceViolated').length ?? 0

rcpt.events?.filter(e => e.event?.match(/PostOpRevertReason|UserOperationRevertReason/)).find(e => {
throw new Error(`${e.event}(${decodeRevertReason(e.args?.revertReason)})`)
})
// check for failure with no revert reason (e.g. OOG)
expect(countSuccessOps).to.eq(userOps.length, 'Some UserOps failed to execute (with no revert reason)')
const failedOps = userOps.length - countSuccessOps
if (!(failedOps > 0 && !GasCheckCollector.inst.isReserveBalancePrecompileSimulated && reserveViolationEvents === failedOps)) {
expect(countSuccessOps).to.eq(userOps.length, 'Some UserOps failed to execute (with no revert reason)')
} else {
debug(`reserve precompile violations detected on ${failedOps}/${userOps.length} ops; continuing gascalc on this network`)
}

debug('count', info.count, 'gasUsed', gasUsed)
const gasDiff = gasUsed - lastGasUsed
Expand Down Expand Up @@ -320,6 +326,7 @@ export class GasCheckCollector {
static initPromise?: Promise<GasCheckCollector>

entryPoint: EntryPoint
isReserveBalancePrecompileSimulated = false

static async init (): Promise<void> {
if (this.inst == null) {
Expand All @@ -334,6 +341,20 @@ export class GasCheckCollector {
debug('signer=', await globalSigner.getAddress())
DefaultGasTestInfo.beneficiary = createAddress()

const reservePrecompile = '0x0000000000000000000000000000000000001001'
const returnFalseCode = '0x600060005260206000F3'
try {
await provider.send('hardhat_setCode', [reservePrecompile, returnFalseCode])
this.isReserveBalancePrecompileSimulated = true
} catch (e: any) {
const message = String(e?.message ?? e)
if (message.includes('hardhat_setCode') && message.includes('does not exist')) {
debug('hardhat_setCode is unavailable on this network; using native reserve precompile behavior')
} else {
throw e
}
}

if (entryPointAddressOrTest === 'test') {
this.entryPoint = await deployEntryPoint(provider)
} else {
Expand Down Expand Up @@ -416,5 +437,7 @@ export class GasCheckCollector {
}

after(() => {
GasCheckCollector.inst.doneTable()
if (GasCheckCollector.inst != null) {
GasCheckCollector.inst.doneTable()
}
})
32 changes: 16 additions & 16 deletions reports/gas-checker.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,36 @@
║ │ │ │ (delta for │ (compared to ║
║ │ │ │ one UserOp) │ account.exec()) ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple │ 1 │ 77742 │ │ ║
║ simple │ 1 │ 81827 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple - diff from previous │ 2 │ │ 4188210761
║ simple - diff from previous │ 2 │ │ 4596714846
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple │ 10 │ 454776 │ │ ║
║ simple │ 10 │ 495638 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple - diff from previous │ 11 │ │ 4192710806
║ simple - diff from previous │ 11 │ │ 4600014879
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple paymaster │ 1 │ 83434 │ │ ║
║ simple paymaster │ 1 │ 87485 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple paymaster with diff │ 2 │ │ 40274 9153
║ simple paymaster with diff │ 2 │ │ 4432513204
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple paymaster │ 10 │ 446011 │ │ ║
║ simple paymaster │ 10 │ 486521 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ simple paymaster with diff │ 11 │ │ 40323 9202
║ simple paymaster with diff │ 11 │ │ 4438613265
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ big tx 5k │ 1 │ 167520 │ │ ║
║ big tx 5k │ 1 │ 171585 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ big tx - diff from previous │ 2 │ │ 13110914543
║ big tx - diff from previous │ 2 │ │ 13518618620
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ big tx 5k │ 10 │ 1347572 │ │ ║
║ big tx 5k │ 10 │ 1388222 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ big tx - diff from previous │ 11 │ │ 13114214576
║ big tx - diff from previous │ 11 │ │ 13515918593
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ paymaster+postOp │ 1 │ 84782 │ │ ║
║ paymaster+postOp │ 1 │ 87506 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ paymaster+postOp with diff │ 2 │ │ 4165910538
║ paymaster+postOp with diff │ 2 │ │ 4438313262
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ paymaster+postOp │ 10 │ 459644 │ │ ║
║ paymaster+postOp │ 10 │ 486860 │ │ ║
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
║ paymaster+postOp with diff │ 11 │ │ 4163710516
║ paymaster+postOp with diff │ 11 │ │ 4439713276
╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝

Loading