The Illusion of Sight: Gas Traps in EVM CALL Opcode and Optimism Bedrock's Edge Cases

·

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)

  1. Transaction Initiation: Users submit withdrawals via L2's L2ToL1MessagePasser contract, specifying target, value, gasLimit, and data.
  2. Storage Commitment: Optimism stores the transaction hash in a mapping and saves the storage root to L1's L2OutputOracle.
  3. Execution Phase: After a 7-day challenge period, anyone calls OptimismPortal.finalizeWithdrawalTransaction(), which forwards the transaction using CALL(gas, target, value, ...).

    • Critical Detail: The forwarded gas must be ≥ the user's gasLimit.

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.

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:

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,336

Despite 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

  1. Gas Limits Are Tricky: EVM's CALL opcode doesn’t always honor the specified gasLimit due to EIP-150.
  2. Edge Cases Matter: Systems assuming exact gas forwarding (e.g., non-retryable transactions) must account for the 63/64 rule.
  3. 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.

👉 Explore Optimism’s latest security patches