ROOTVIELROOTVIEL
← Academy

Module 8 · Ethereum, the EVM & on-chain zero-knowledge

Gas, opcodes and the cost model

Gas is the EVM resource meter: every opcode has a price, paid by the transaction sender, that bounds computation and prices scarce resources like persistent state. Understanding the opcode families and their costs — especially the cold/warm distinction, the 63/64 forwarding rule, and the precompiles for cryptographic operations — is what separates reading Solidity from reasoning about denial-of-service, griefing and the feasibility of on-chain cryptography.

Learning objective

Explain the EVM opcode families (arithmetic, comparison, bitwise, control flow), the gas metering model and why it exists, cold versus warm state access introduced by EIP-2929, the 63/64 rule that caps gas forwarded to sub-calls and why it matters for external calls, gas griefing as an attack class, and the precompiled contracts at fixed addresses — including the BN254 pairing precompiles that make on-chain ZK verification feasible.

Track
Engineering
Module
8 · Ethereum, the EVM & on-chain zero-knowledge
Prerequisites
The EVM execution model

Conceptual introduction

Gas is the EVM unit of computational cost. Every opcode the machine executes has a gas price, and the sender of a transaction pays for the total consumed. If a transaction runs out of the gas it was allotted, it reverts — all state changes are undone, but the gas already spent is not refunded. This single mechanism does two jobs at once: it makes denial-of-service attacks economically impossible (there is no free infinite loop) and it prices scarce shared resources, so that touching persistent storage costs far more than adding two numbers.

Gas costs are not arbitrary. They are calibrated to the real burden an operation places on every node that must re-execute it: arithmetic is cheap because it is pure CPU, while reading and writing persistent storage is expensive because it grows the global state that every node stores forever. An auditor who internalizes the cost model can predict where denial-of-service and griefing bugs will appear long before reading the business logic.

Note

Gas cost is not fixed history. Several network upgrades (EIPs) have re-priced opcodes to reflect their true cost or to close attack vectors. Code written against old assumptions about gas can break — or become vulnerable — after a re-pricing. Treat exact numbers as protocol-version-specific and reason about their shape and relative magnitude.

The opcode families

The EVM has on the order of 140 opcodes. For security work they group into a handful of families. The gas figures below are illustrative magnitudes under a recent protocol version, included to show relative cost, not as a benchmark to quote.

Arithmetic
ADD, SUB (cheap); MUL, DIV, MOD (slightly dearer); EXP (variable, growing with the size of the exponent); ADDMOD and MULMOD (modular combine-and-reduce). Division and modulo come in unsigned (DIV, MOD) and signed (SDIV, SMOD) forms.
Comparison
LT and GT (unsigned), SLT and SGT (signed), EQ (equality), ISZERO (is the value zero). The signed/unsigned split is the source of subtle bugs: as an unsigned value, "all ones" is the maximum integer; as a signed value, the same bits are -1.
Bitwise
AND, OR, XOR, NOT, BYTE (extract the i-th byte), and the shifts SHL, SHR (logical) and SAR (arithmetic, sign-preserving). Bit manipulation underlies packing, masking and selector extraction.
Control flow
JUMP and JUMPI (conditional), with destinations restricted to positions marked JUMPDEST; PC (current program counter); STOP (halt successfully); RETURN (return data and halt); REVERT (undo and return data, refunding remaining gas); INVALID (consume ALL remaining gas and fail).
illustrative — signed vs unsigned comparison of the same bits (Yul-style)
let a := sub(0, 1) // a = 2^256 - 1 (all bits set) let r1 := lt(a, 100) // UNSIGNED: a is the largest number -> r1 = 0 (false) let r2 := slt(a, 100) // SIGNED: a is -1 -> r2 = 1 (true) // choosing the wrong comparison opcode inverts the logic completely
Note

Two control-flow details matter for security. First, JUMPDEST: the EVM only permits jumps to positions explicitly marked as valid destinations, which prevents jumping into the middle of a multi-byte instruction. Second, INVALID consumes all remaining gas and does not refund, unlike REVERT — older compilers used it for failed assertions, which is why a failing assert could burn the whole gas budget.

Arithmetic overflow deserves special mention. Before Solidity 0.8 the arithmetic opcodes wrapped silently: an 8-bit value at its maximum plus one became zero, with no error. From 0.8 the compiler inserts checks that revert on overflow. In hand-written assembly the raw opcodes still wrap, so the responsibility returns to the author.

The cost model and cold vs warm access

The cheapest opcodes (ADD, comparison, bitwise) cost a few units. Memory access adds a small base cost plus the expansion charge from the previous lesson. The expensive region is state: reading a storage slot, writing a storage slot, and reaching into another account. Here the pivotal idea, introduced by EIP-2929, is the cold/warm distinction.

Cold access
The first time within a transaction that a given storage slot or account address is touched. It carries a large surcharge because the node may have to fetch it from the global state tree.
Warm access
Any subsequent access to a slot or address already touched in the same transaction. It is an order of magnitude cheaper because the value is already in hand.
SLOAD
Read a storage slot: cold roughly 2100, warm roughly 100 (illustrative).
SSTORE
Write a storage slot: cost depends on the transition (zero to non-zero is the most expensive, overwriting an existing non-zero value is cheaper, clearing to zero can earn a bounded refund).
illustrative — the cold/warm gap in practice (synthetic)
function readTwice() external view { uint x = a; // first touch of slot(a): COLD (~2100) uint y = a; // same slot again: WARM (~100) } function readBoth() external view { uint x = a; // slot(a): COLD (~2100) uint y = b; // slot(b): different slot, also COLD (~2100) } // lesson: cache a storage value in a local variable instead of // re-reading it; and expect two distinct slots to both pay cold cost
Note

EIP-2929 re-priced state access upward and split it into cold/warm. Contracts written before it, assuming a single flat SLOAD price, could suddenly cost far more or hit gas limits. This is the canonical example of why "the exact gas number" is a moving target, and why optimization patterns tuned to old prices can quietly become anti-patterns.

A later related idea is transient storage (EIP-1153): a storage-like region (TSTORE/TLOAD) that persists only for the duration of a transaction and is discarded afterward, priced like warm access. It gives contracts a cheap scratch area for cross-call state within one transaction — useful for things like reentrancy locks — without paying for permanent storage.

The 63/64 rule and forwarded gas

When a contract makes an external call it must decide how much gas to forward to the callee. Since EIP-150 the EVM enforces a ceiling: a call can forward at most 63/64 of the gas currently remaining, reserving at least 1/64 for the caller to continue after the call returns. Even if you write "forward all remaining gas", the machine quietly keeps that 1/64 slice.

  1. 1A frame has some gas remaining when it reaches an external call.
  2. 2At most 63/64 of that amount is forwarded to the callee; the callee cannot consume the reserved 1/64.
  3. 3When the callee returns (or reverts), control comes back to the caller with at least that reserved slice available.
  4. 4The caller can therefore always run a little code after the call — crucially, it can check the success flag and react, rather than dying with the callee.

The original purpose was to defuse the stack-depth attack: before the rule, an attacker could arrange for a deep sub-call to fail from exhaustion while the caller, also near empty, could not even detect it. By guaranteeing the caller a reserved slice, the EVM ensures a frame always survives its own sub-calls enough to handle their outcome. The rule also means that in deeply nested call chains the forwardable gas shrinks geometrically — each level keeps 1/64 — so very deep chains starve naturally.

Note

A practical consequence: you cannot rely on a sub-call receiving literally all your gas, and you can rely on having a little left afterward. Both facts shape correct call handling — always check the returned success boolean and handle failure explicitly, because the surrounding frame keeps running regardless.

Gas griefing

Gas griefing is an attack in which a malicious actor forces another party to spend more gas than necessary, or makes an operation fail outright by consuming or withholding gas. It is not about stealing funds directly; it is about weaponizing the cost model to deny service or extract a cost from the victim.

illustrative — a loop that one hostile recipient can break (synthetic)
// VULNERABLE: pays a list of users in one loop function refund(address[] calldata users) external { for (uint i = 0; i < users.length; i++) { (bool ok,) = users[i].call{value: owed[users[i]]}(""); require(ok); // ONE failing recipient reverts the WHOLE batch owed[users[i]] = 0; } } // A recipient that is a contract reverting (or burning gas) on receipt // blocks every other user's refund. FIX below.
illustrative — pull-over-push removes the shared failure point (synthetic)
// SAFE: each user withdraws their own balance function withdraw() external { uint amount = owed[msg.sender]; owed[msg.sender] = 0; // effects before interaction (bool ok,) = msg.sender.call{value: amount}(""); require(ok); } // one user's hostile fallback can only harm that user, not the group

The general defence is the pull-over-push pattern: rather than the contract pushing value to many recipients in a single transaction (where any one recipient can sabotage the batch), let each recipient pull their own share in a separate transaction. This isolates failure to the party that caused it and removes the shared gas dependency a griefer exploits.

Precompiled contracts

Some cryptographic operations are too expensive to implement opcode-by-opcode but are needed often enough to deserve native support. The EVM exposes these as precompiled contracts: special addresses (0x01, 0x02, and upward) that behave like contracts you CALL, but whose behaviour is implemented natively by every client at a fixed, metered price. You pass input as calldata and receive a result, just like a normal call.

0x01 — ecrecover
Recovers the signer address from an ECDSA signature over a hash. Returns the zero address on an invalid signature rather than reverting, so callers must check the result is non-zero and matches the expected signer.
0x02 — sha256
Computes a SHA-256 hash, distinct from the EVM native keccak256.
0x05 — modexp
Modular exponentiation on big integers, used by RSA-style and other number-theoretic constructions.
0x06, 0x07, 0x08 — BN254 curve
Point addition (ECADD), scalar multiplication (ECMUL) and the pairing check (ECPAIRING) on the BN254 elliptic curve. The pairing precompile is what makes verifying succinct zero-knowledge proofs on-chain economically feasible.
Note

The BN254 precompiles (0x06-0x08) are the bridge between the ZK material earlier in the Academy and real deployment. A SNARK verifier contract reduces proof verification to a pairing equation; without a native pairing precompile, checking that equation in plain EVM opcodes would be far too expensive to be practical. This is the concrete reason on-chain ZK verification exists at all.

illustrative — ecrecover must be checked, it does not revert (synthetic)
address signer = ecrecover(hash, v, r, s); // precompile at 0x01 require(signer != address(0), "invalid signature"); // zero = recovery failed require(signer == expectedSigner, "wrong signer"); // forgetting the zero-address check treats a bad signature as "signed by 0x0"

Application to security and agents

For an auditor, the cost model is a map of where attacks hide. Unbounded loops over user-controlled arrays invite griefing; a batch payout that reverts on any single recipient has a built-in failure point; a contract that assumes a flat storage price can be pushed past a gas limit after a re-pricing. Reading a contract with the cost model in mind surfaces these before the business logic even matters.

For autonomous agents that transact on-chain, gas is both a budget and a safety boundary. An agent must estimate gas before acting, handle the case where a call returns failure (the 63/64 rule guarantees it gets to), and understand that a hostile counterparty can grief it into wasted fees or stuck batches. An agent that verifies zero-knowledge proofs on-chain is leaning directly on the BN254 precompiles, and its economics depend on their fixed native cost.

Risks and limitations

  • ▸Exact gas numbers are protocol-version-specific and have changed across upgrades; reason about relative magnitude and shape, not quoted constants.
  • ▸The cold/warm distinction (EIP-2929) means the first touch of a slot or address is far dearer than later ones; code tuned to old flat prices may now hit gas limits.
  • ▸The 63/64 rule means you cannot forward literally all gas to a sub-call, and you always retain a slice to handle the result — ignoring the returned success flag is a bug, not a convenience.
  • ▸Loops over externally supplied lists, and push-style batch payouts, are gas-griefing magnets; prefer pull-over-push.
  • ▸ecrecover returns the zero address on failure instead of reverting; an unchecked result silently authorizes the zero address.
  • ▸Precompiles have fixed native costs but are still CALLs with their own input encoding; mis-encoding input or ignoring their return conventions produces subtle, silent failures.

Exercise

Note

A staking contract distributes rewards by looping over all stakers in one transaction and sending each their reward with a low-level call, requiring success on every send. Identify the attack an adversary can mount, explain how the 63/64 rule and the cost model enable or constrain it, and describe the redesign that removes the vulnerability.

Expected direction: An adversary stakes with a contract address whose receive/fallback reverts (or deliberately burns gas). When the distribution loop reaches that address, the low-level call fails; because the loop requires success, the whole transaction reverts and nobody is paid — a gas-griefing denial of service. The 63/64 rule guarantees the distributing contract retains a slice of gas and so could detect the failed call, but the vulnerability is the design choice to require success inside a shared loop, which couples every honest staker to the worst recipient. The cost model also means a griefer can make each iteration expensive. The fix is pull-over-push: record each staker reward and let them withdraw individually, so a hostile fallback only harms its own owner and the shared failure point disappears. Each withdrawal should apply effects (zero the balance) before the external interaction.

Check your understanding

What did EIP-2929 change about state access, and why does it matter for old code?
It raised state-access costs and split them into cold (first touch in a transaction) and warm (subsequent) tiers. Code that assumed a single flat SLOAD/account price can suddenly cost far more and may exceed gas limits or become DoS-prone.
State the 63/64 rule and why it exists.
An external call can forward at most 63/64 of the remaining gas, reserving at least 1/64 for the caller to continue afterward. It originated to defuse stack-depth attacks by guaranteeing a frame always has gas left to handle its sub-call outcome, and it also makes deep call chains starve geometrically.
Which precompiles make on-chain ZK verification feasible, and why?
The BN254 curve precompiles at 0x06 (ECADD), 0x07 (ECMUL) and 0x08 (ECPAIRING). SNARK verification reduces to a pairing check; a native pairing precompile makes that check cheap enough to run on-chain, which plain EVM opcodes could not afford.
Why must every ecrecover result be checked?
Because ecrecover returns the zero address on an invalid signature instead of reverting; an unchecked result would treat a bad signature as a valid signature by address(0).

References

  1. [1]Ethereum Yellow Paper (Wood) — gas and the opcode fee schedule
  2. [2]EIP-150 — gas cost changes and the 63/64 rule for calls
  3. [3]EIP-2929 — gas cost increases for state access (cold/warm)
  4. [4]EIP-1153 — transient storage opcodes (TSTORE/TLOAD)
  5. [5]evm.codes — interactive opcode, gas and precompile reference

References point to public, well-known technical sources for further study.

Related articles