Introduction
While researching Optimism Bedrock's code, I stumbled upon a rarely discussed edge case: EVM's CALL opcode (including STATICCALL and DELEGATECALL) may use a gasLimit different from the expected value. Though generally harmless, this quirk becomes critical in Optimism Bedrock's design—potentially allowing malicious actors to permanently lock users' assets. Surprisingly, even Optimism's team initially overlooked this issue.
Why This Matters
Optimism's cross-chain transactions (Deposits/Withdrawals) are non-retryable. If a transaction fails—especially one transferring ETH (msg.value)—the funds get permanently stuck in the contract. A primary failure mode? Exceeding the user-specified gasLimit during execution.
How Optimism Handles Withdrawals (L2 → L1)
- Transaction Initiation: Users submit withdrawals via L2's
L2ToL1MessagePassercontract, specifyingtarget,value,gasLimit, anddata. - Storage Commitment: Optimism stores the transaction hash in a mapping and saves the storage root to L1's
L2OutputOracle. Execution Phase: After a 7-day challenge period, anyone calls
OptimismPortal.finalizeWithdrawalTransaction(), which forwards the transaction usingCALL(gas, target, value, ...).- Critical Detail: The forwarded
gasmust be ≥ the user'sgasLimit.
- Critical Detail: The forwarded
Code Breakdown
function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) external {
require(
gasleft() >= _tx.gasLimit + FINALIZE_GAS_BUFFER, // Buffer = 20,000 gas
"Insufficient gas to finalize withdrawal"
);
bool success = SafeCall.call(
_tx.target,
gasleft() - FINALIZE_GAS_BUFFER, // Forwarded gas
_tx.value,
_tx.data
);
}Problem: Between the gas check and CALL, operations like SSTORE consume gas, potentially making the actual CALL gas lower than the user's gasLimit.
The Deeper Issue: EIP-150's 63/64 Gas Rule
What Is EIP-150?
Implemented to prevent call-stack attacks, EIP-150 enforces that CALL gasLimit cannot exceed 63/64 of remaining gas.
- Example: If
availableGas = 1000and youCALL(990, ...), EVM caps the gas at1000 * 63/64 = 985—causing potential failure if the callee needs ≥990 gas.
Why Optimism's Fix Falls Short
Even after increasing FINALIZE_GAS_BUFFER to cover pre-CALL operations, the 63/64 rule can still throttle gas. For instance:
- If
_tx.gasLimit > 1,254,878,(gasleft() - buffer)might exceed63/64 * gasleft(), forcing EVM to use the lower cap.
Result: A perfectly valid transaction could fail due to hidden gas constraints.
Proof of Concept
// Test case: User requests 1,368,975 gas for `store(60)`
function testPortalBugWithEnoughGas() public {
_executePortalWithGivenGas(1_368_975 + 30_000); // Should pass but fails
}Log Output:
gas provided to call: 1,369,583 | gasLeft: 1,389,336Despite specifying adequate gas, the 63/64 rule reduces the effective limit below requirements.
Optimism's Solution
The team addressed this by introducing SafeCall.callWithMinGas(), which ensures the forwarded gas accounts for EIP-150's rule:
function callWithMinGas(address _target, uint256 _minGas, bytes memory _calldata) internal returns (bool) {
uint256 gasLeft = gasleft();
// Enforce 63/64 rule while ensuring ≥ _minGas
require(gasLeft >= (_minGas * 64) / 63, "Insufficient gas");
return call(_target, gasLeft - (gasLeft / 64), 0, _calldata);
}Key Takeaways
- Gas Limits Are Tricky: EVM's
CALLopcode doesn’t always honor the specified gasLimit due to EIP-150. - Edge Cases Matter: Systems assuming exact gas forwarding (e.g., non-retryable transactions) must account for the 63/64 rule.
- Audit Thoroughly: Even subtle EVM behaviors can lead to critical vulnerabilities when layered with custom logic (e.g., cross-chain designs).
👉 Learn more about EVM opcodes
FAQ
Q: Why does EIP-150 enforce the 63/64 rule?
A: To prevent call-stack exhaustion attacks by limiting how much gas each nested CALL can consume.
Q: Can this issue affect other L2s?
A: Yes—any system relying on precise gas forwarding (e.g., cross-chain messaging) must consider EIP-150's constraints.
Q: How can developers avoid similar traps?
A: Always test gas limits under worst-case scenarios and use libraries like SafeCall that abstract EIP-150's complexity.