Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x57 |
| Mnemonic | JUMPI |
| Gas | 10 |
| Stack Input | dst, condition |
| Stack Output | (none) |
| Behavior | Conditional jump: if condition is any nonzero 256-bit value, sets the program counter (PC) to dst, which must point to a valid JUMPDEST (0x5B) instruction. If condition is zero, PC increments by 1 (falls through to the next instruction). If dst is not a valid JUMPDEST, execution reverts with an invalid jump destination error. |
Threat Surface
JUMPI is the sole conditional branching primitive in the EVM. Every if, else, require(), assert(), ternary operator, while loop, for loop, and switch statement in Solidity compiles down to one or more JUMPI instructions. This makes JUMPI the gatekeeper for all validation logic in smart contracts — overflow checks, access control, balance requirements, deadline enforcement, and invariant assertions all ultimately depend on a JUMPI deciding whether to branch or fall through.
The threat surface centers on four properties:
-
Any nonzero value is truthy. The EVM does not have a boolean type at the bytecode level. JUMPI treats
1,2,0xFF, andMAX_UINT256identically — all are “true.” Solidity’s compiler enforces strict boolean semantics (only0x00or0x01), but inline assembly, cross-contract calls returning raw bytes, and precompile return values can produce nonzero values that are truthy but not0x01. This creates a gap between what the developer expects (“true means 1”) and what the EVM executes (“true means anything not zero”). -
Condition values are computed from upstream opcodes. JUMPI itself is not vulnerable — it faithfully evaluates whatever condition the stack provides. The real attack surface is in the opcodes that produce the condition: LT, GT, EQ, ISZERO, AND, OR, and arithmetic opcodes. If an attacker can manipulate the inputs to these upstream operations (via integer overflow, type confusion, or calldata crafting), they control whether JUMPI branches, effectively bypassing every downstream validation check.
-
Compiler-generated JUMPI patterns are bypassable in assembly. Solidity’s
require()compiles toISZERO ... JUMPI ... REVERT.assert()compiles toISZERO ... JUMPI ... INVALID. These patterns provide safety at the Solidity level, but contracts written in raw assembly or Yul can construct arbitrary JUMPI conditions without these safety wrappers. Upgradeable contracts, optimized DEX routers, and EVM puzzles often contain hand-rolled JUMPI logic where the condition computation may be flawed. -
JUMPDEST validation applies identically to JUMPI and JUMP. A conditional jump to an invalid destination (not a JUMPDEST, or a 0x5B byte inside PUSH immediate data) causes an immediate revert. This is a safety property, but it also means an attacker who controls the
dstoperand can force a revert by supplying an invalid target, creating a denial-of-service vector in contracts that compute jump destinations dynamically.
Smart Contract Threats
T1: Condition Manipulation via Integer Overflow/Underflow (Critical)
JUMPI’s condition comes from upstream arithmetic. In Solidity < 0.8.0 (no automatic overflow checks), an attacker can craft inputs that cause intermediate calculations to overflow, producing a zero or nonzero result that inverts the intended branch direction. Every require() compiles to ISZERO(condition) JUMPI(revert_dest) — if the attacker makes condition overflow to zero, the ISZERO produces 1 (truthy), and JUMPI branches to the revert label as expected. But if the overflow occurs in the value being checked rather than the condition itself, the require may pass when it shouldn’t.
-
Arithmetic overflow bypassing balance checks. A
require(balances[msg.sender] >= amount)check compiles toLT(balance, amount) JUMPI(revert). If theamountparameter is crafted to overflow in a preceding multiplication (e.g.,amount = cnt * valuewhere the product wraps to zero), the LT comparison seesbalance >= 0, which is always true. The JUMPI never branches to revert, and the transfer proceeds with an astronomically large token amount. -
Underflow creating false conditions. In
require(a - b > 0), ifa < band subtraction wraps (pre-0.8.0), the result is a huge positive number, causing the GT comparison to produce 1 (truthy). The JUMPI condition is satisfied, and execution continues past the guard. -
Multiplicative overflow in price calculations. DeFi protocols compute prices, fees, and shares using multiplications that can overflow. If the overflow produces a value that satisfies a downstream JUMPI condition, the protocol executes trades, mints, or burns at incorrect prices.
Why it matters: Integer overflow is the most exploited arithmetic vulnerability class in smart contract history. Every overflow that changes a JUMPI outcome is a potential fund drain.
T2: Truthy Value Confusion — Any Nonzero Is True (High)
The EVM’s JUMPI uses a 256-bit condition where any nonzero value means “jump.” Solidity’s type system restricts bool to 0x00/0x01, but this guarantee breaks at contract boundaries:
-
External call return value misinterpretation. When a contract uses
staticcallorcallto invoke another contract and interprets the raw return data as a boolean, any nonzero byte in the return buffer satisfies a JUMPI condition. If the callee returns0x02or0xFF...FFinstead of0x01, the caller treats it astruewithout the compiler’s strict boolean validation. The 0x Protocol vulnerability (2019) exploited exactly this: astaticcallto an EOA returned stale memory (nonzero bytes) instead of a valid boolean, and the JUMPI-based branch treated it as “signature valid.” -
Inline assembly bypassing boolean strictness. In Yul or inline assembly, developers can push arbitrary values as JUMPI conditions. A function that computes
result := sub(a, b)and uses it directly as a condition (if result { ... }) will branch whenevera != b, regardless of which is larger. This is correct for a “not-equal” check but wrong if the developer intended “a > b.” -
Precompile return values. EVM precompiles (ecrecover, SHA-256, etc.) return raw 32-byte words. A contract that checks
require(ecrecover(...) != address(0))is safe because the compiler generates proper comparison opcodes. But a contract that uses raw assembly to check the return value may treat any nonzero return as “valid signature.”
Why it matters: The gap between Solidity’s strict booleans and the EVM’s nonzero-is-truthy semantics creates a class of bugs that only manifest at contract boundaries or in assembly code.
T3: Compiler-Generated Require/Assert Bypass via Assembly (High)
Solidity’s require(condition) compiles to:
ISZERO(condition) // invert: 0 if condition was truthy, 1 if falsy
PUSH revert_dest
JUMPI // jump to revert if condition was falsy
This pattern is safe when generated by the compiler, but contracts that mix Solidity and inline assembly can subvert it:
-
Skipping the JUMPI entirely. A Yul block can manipulate the program counter or stack to skip past a compiler-generated JUMPI. While the EVM prevents arbitrary PC manipulation (all jumps must target JUMPDEST), a carefully constructed assembly block can pop the condition off the stack before the JUMPI executes, or push a zero condition to prevent the branch.
-
Overwriting memory used by require’s error message. In
require(condition, "error message"), the revert data (error string) is stored in memory. An assembly block that writes to the same memory region can corrupt the revert data, causing confusing error messages during debugging (not a direct exploit, but a vector for obfuscation in malicious contracts). -
Assert vs. Require: different failure modes.
assert()compiles toJUMPI ... INVALID(consuming all remaining gas), whilerequire()compiles toJUMPI ... REVERT(refunding unused gas). Malicious contracts can useassert()patterns to grief callers by consuming their entire gas allowance when a condition fails, even though the failure was expected.
Why it matters: Optimized contracts, MEV bots, and protocol routers frequently use inline assembly for gas savings. Every hand-rolled condition check is a potential JUMPI bypass if the condition computation is incorrect.
T4: Short-Circuit Evaluation Bugs in Compound Conditions (Medium)
Solidity implements short-circuit evaluation for && and || operators using nested JUMPI sequences. require(A && B) compiles to:
evaluate A
ISZERO
JUMPI -> revert // if A is false, skip B and revert
evaluate B
ISZERO
JUMPI -> revert // if B is false, revert
This creates subtle vulnerabilities when the evaluation of B has side effects or when the ordering assumption matters:
-
State-dependent condition ordering. If condition A modifies state (e.g., calls an external contract) and condition B reads state, the order matters. Solidity evaluates left-to-right, but developers may not realize that swapping
A && BtoB && Achanges which condition gets short-circuited, potentially skipping a critical external call. -
Gas-dependent branch behavior. If evaluating condition B requires a
CALLorSLOADthat consumes significant gas, and the transaction’s gas limit is tight, condition A might pass but the transaction reverts during B’s evaluation — not because B is false, but because the gas ran out during B’s computation. The caller receives an out-of-gas error, not a “condition failed” revert. -
Reentrancy during condition evaluation. If condition A involves an external call (e.g.,
token.balanceOf(addr) >= threshold && otherCheck()), the external call in A provides a reentrancy window. The re-entrant call executes before condition B is evaluated, potentially changing the state that B checks.
Why it matters: Compound conditions are ubiquitous in DeFi (e.g., “deadline not passed AND sufficient balance AND approved amount”). Incorrect ordering or side effects in JUMPI chains can create exploitable windows.
T5: Branch Timing Side Channels (Low)
In traditional computing, branch prediction timing attacks (Spectre, Meltdown) exploit CPU speculation to leak secrets. In the EVM, this threat is largely theoretical:
-
No speculative execution. The EVM is a strictly sequential virtual machine with no branch prediction or speculative execution. JUMPI either branches or doesn’t; there is no timing difference based on the branch direction.
-
Gas-based inference. An observer can infer which branch was taken by measuring gas consumption. If the “true” branch costs 50,000 gas and the “false” branch costs 10,000 gas, the total gas used reveals the branch outcome. On public blockchains, all execution is transparent anyway, making this moot. However, on private EVM chains or in confidential computing environments (e.g., Oasis, Secret Network’s EVM compatibility layer), gas-based inference could leak branch outcomes.
-
Timing in off-chain simulation. MEV searchers simulate transactions off-chain before submitting them. If a JUMPI branch depends on a private state variable, the gas consumption difference in simulation reveals the branch outcome, potentially leaking information about the private state.
Why it matters: Not practically exploitable on public Ethereum, but relevant for privacy-focused EVM chains and off-chain simulation environments. Included for completeness.
Protocol-Level Threats
P1: Invalid Jump Destination as DoS Vector (Low)
When JUMPI’s dst operand does not point to a valid JUMPDEST, the EVM immediately halts execution with an error, consuming all gas up to that point. This is by design and provides safety (prevents jumping into arbitrary bytecode), but it creates a minor DoS consideration:
- Dynamic jump targets. Contracts that compute JUMPI destinations at runtime (rare in compiled Solidity, more common in hand-written assembly or EVM puzzles) can be forced to revert by supplying inputs that cause the computed destination to miss a JUMPDEST.
- Gas griefing via forced revert. A caller can craft inputs that cause a JUMPI to target an invalid destination, wasting the gas spent up to that point. With 10 gas for JUMPI itself, the griefing cost is low, but the wasted gas from preceding computation can be significant.
P2: Consensus Safety — JUMPDEST Analysis Consistency (Low)
All EVM implementations must agree on which bytecode positions are valid JUMPDESTs. The JUMPDEST analysis pass scans bytecode and excludes 0x5B bytes that fall within PUSH immediate data. If two client implementations disagree on whether a particular 0x5B byte is a valid JUMPDEST, a JUMPI targeting that position would succeed on one client and revert on the other, causing a consensus split.
This has not occurred in practice. The JUMPDEST validation algorithm is well-specified in the Yellow Paper and all major clients (Geth, Nethermind, Besu, Erigon, Reth) produce identical JUMPDEST bitmaps. EIP-7921 (proposed March 2025) would simplify this by allowing jumps to any 0x5B byte regardless of context, but it explicitly warns about backward-compatibility risks for contracts with dynamic JUMPI targets.
P3: EOF and the Deprecation of Dynamic JUMPI (Medium)
The EVM Object Format (EOF, EIP-3540/EIP-4750/EIP-5450) replaces JUMP/JUMPI/JUMPDEST with static relative jumps (RJUMP, RJUMPI, RJUMPV). Under EOF:
- Static jump targets are validated at deploy time. The deployer cannot submit bytecode with invalid relative jump offsets. This eliminates the entire class of invalid-jump-destination bugs.
- No JUMPDEST analysis needed. The runtime overhead of JUMPDEST validation disappears for EOF contracts.
- Legacy JUMP/JUMPI remain valid in non-EOF contracts indefinitely. The dual execution environment means both dynamic and static jumps coexist, and the security properties differ between them.
Why it matters: EOF will not remove JUMPI from the threat model — legacy contracts with JUMPI will persist on-chain forever. But new contracts compiled for EOF will not be vulnerable to JUMPI-specific issues.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
condition = 0 | No jump; PC increments by 1 (falls through) | Normal “false” path. Require/assert checks pass when condition is zero because the Solidity pattern uses ISZERO to invert before JUMPI. |
condition = 1 | Jumps to dst | Standard “true” value from comparison opcodes (LT, GT, EQ, ISZERO). |
condition = MAX_UINT256 | Jumps to dst (any nonzero is truthy) | Identical to condition = 1. Developers who assume boolean returns from external calls may be surprised by non-canonical truthy values. |
condition = 2 | Jumps to dst (still truthy) | Demonstrates that the EVM has no concept of “strict boolean.” Foundry’s testing framework historically only treated 0x01 as true, causing test/production divergence (foundry-rs/foundry#4067). |
Invalid dst with truthy condition | Execution halts with invalid jump destination error | All gas consumed up to this point is lost. Safety feature prevents jumping into PUSH data or non-JUMPDEST bytes. |
dst = valid JUMPDEST inside PUSH data | Invalid jump destination error (0x5B in PUSH immediate is not a valid JUMPDEST) | JUMPDEST analysis correctly excludes 0x5B bytes within PUSH operands. Attempts to jump into data are rejected. |
dst = position beyond bytecode length | Invalid jump destination error | The EVM pads bytecode with implicit STOP (0x00) bytes, none of which are JUMPDEST. |
condition is result of overflowed arithmetic | JUMPI faithfully evaluates whatever value is on the stack | The overflow bug is in the arithmetic opcode, not JUMPI, but JUMPI is the mechanism that converts a corrupted value into a wrong branch decision. |
| JUMPI in STATICCALL context | Behaves identically; STATICCALL restricts state modification, not control flow | JUMPI can branch freely in read-only contexts. The restriction applies to SSTORE, LOG, CREATE, and SELFDESTRUCT within the called code. |
| Back-to-back JUMPI (tight loop) | Each JUMPI costs 10 gas; a loop body consisting only of JUMPI + JUMPDEST costs 11 gas per iteration | Theoretical infinite loop with minimal gas per iteration. Limited by block gas limit (~30M gas = ~2.7M iterations). |
Real-World Exploits
Exploit 1: BEC Token (BeautyChain) — $900M+ Paper Value via Integer Overflow Bypassing Require (April 2018)
Root cause: Integer overflow in a multiplication caused the require() balance check (compiled to JUMPI) to pass with a zero amount while transferring an astronomically large token quantity to recipients.
Details: The BEC token’s batchTransfer() function computed uint256 amount = cnt * _value where cnt was the number of recipients and _value was the per-recipient transfer amount. The function then checked require(balances[msg.sender] >= amount) before subtracting amount from the sender’s balance and adding _value to each recipient.
On April 22, 2018, an attacker called batchTransfer() with two recipients and _value = 0x8000...0000 (2^255). The multiplication 2 * 2^255 overflowed the uint256 boundary, wrapping amount to exactly zero. The compiled bytecode executed:
LT(balance, amount)→LT(balance, 0)→0(balance is not less than zero)JUMPI(revert_label, 0)→ condition is 0, no jump, execution falls throughbalances[sender] -= 0→ no change to sender’s balancebalances[recipient1] += 2^255→ recipient receives 2^255 tokensbalances[recipient2] += 2^255→ recipient receives 2^255 tokens
The JUMPI faithfully executed the “no jump” path because the overflowed amount (zero) made the balance check trivially true. The attacker transferred ~5.79 × 10^57 BEC tokens, collapsing the token’s market and forcing exchanges worldwide to suspend ERC-20 deposits.
JUMPI’s role: JUMPI was the enforcement point for the require() check. The integer overflow corrupted the condition fed to JUMPI, causing it to fall through (no branch to revert) when it should have branched. JUMPI itself worked correctly — the bug was in the arithmetic that computed its condition.
Impact: ~$900M in paper token value destroyed. CVE-2018-10299 was assigned. OKEx, Poloniex, Changelly, and other major exchanges suspended all ERC-20 token services. Led to widespread adoption of SafeMath libraries and eventually Solidity 0.8.0’s built-in overflow checks.
References:
- PeckShield: New batchOverflow Bug in Multiple ERC20 Smart Contracts (CVE-2018-10299)
- Secbit: A Disastrous Vulnerability Found in Smart Contracts of BeautyChain
Exploit 2: 0x Protocol v2.0 — Signature Forgery via Truthy Non-Boolean Return Value (July 2019)
Root cause: A staticcall to an EOA returned stale memory (nonzero bytes) instead of a valid boolean. The JUMPI-based condition check treated the nonzero return as “signature valid,” enabling arbitrary order filling.
Details: The 0x v2.0 Exchange contract’s isValidWalletSignature() function used staticcall to query a wallet contract for signature verification. When the target address was an EOA (no code), the staticcall succeeded immediately (executing no code is equivalent to STOP) but returned zero bytes. The contract then read 32 bytes from the return data buffer, which contained stale memory from a previous operation — a nonzero value. The subsequent check compiled to:
- Load return value from memory (nonzero stale data)
ISZERO(return_value)→0(not zero, since stale data was nonzero)JUMPI(revert_label, 0)→ no jump, execution falls through- Signature treated as valid
A specially crafted signature type (0x04, the “Wallet” type) triggered this code path for any EOA address. An attacker could fill any open order where the maker was an EOA that had approved the 0x exchange for token spending, without possessing the maker’s private key.
JUMPI’s role: The JUMPI condition was a nonzero value that was not a valid boolean (0x01) — it was arbitrary stale memory. Because the EVM treats any nonzero value as truthy, the JUMPI fell through rather than branching to the revert path. If the EVM had strict boolean enforcement at the opcode level, this exploit would not have been possible.
Impact: No funds were stolen — samczsun responsibly disclosed the vulnerability, and the 0x team triggered an emergency shutdown within hours. However, all orders from EOA makers with active token approvals were at risk. The patched v2.1 exchange was deployed the same day.
References:
- samczsun: The 0x Vulnerability, Explained
- 0x Core Team: Post-Mortem
- ConsenSys Diligence: Return Data Length Validation
Exploit 3: Truebit Protocol — $26.6M Drained via Overflow Bypassing Price Validation (January 2026)
Root cause: An integer overflow in a price calculation function caused the JUMPI-enforced require() check on purchase price to pass with a price of zero, allowing free minting of tokens that were immediately sold for ETH.
Details: On January 8, 2026, an attacker exploited the Truebit Protocol’s buyTRU() function. The getPurchasePrice() function, compiled with Solidity 0.6.10, computed intermediate values like 200 × total_supply × amount × reserve without overflow protection. By supplying an extremely large amount parameter, the multiplication overflowed, wrapping the purchase price to zero.
The compiled bytecode for the price validation was:
- Compute
price = getPurchasePrice(amount)→ overflows to0 require(msg.value >= price)→LT(msg.value, 0)→0(nothing is less than zero)JUMPI(revert_label, 0)→ no jump, passes the check
The attacker called buyTRU() with msg.value = 0, minted massive quantities of TRU tokens, then called sellTRU() to redeem them for ETH from the contract’s reserves. This mint-and-burn cycle was repeated within a single atomic transaction, draining 8,535 ETH (~$26.6M).
JUMPI’s role: Identical pattern to the BEC exploit — JUMPI was the enforcement mechanism for a require() check, but the condition it evaluated was corrupted by upstream arithmetic overflow. The JUMPI correctly executed “no branch” because the condition was zero, but the condition should have been nonzero if the arithmetic hadn’t overflowed.
Impact: 0.16 to near zero. The contract was compiled with Solidity 0.6.10 (no automatic overflow checks), had never been publicly audited, and the source code was not verified on Etherscan.
References:
- Olympix: Truebit $26.6M Exploit — Integer Overflow and the Cost of Abandoned Code
- BlockSec: In-Depth Analysis — The Truebit Incident
Exploit 4: FuturXE Token — Boolean Mishandling in Transfer Authorization (2018)
Root cause: A custom authorization function returned a non-boolean uint256 value. The contract used inline assembly to check the return value, and the JUMPI treated any nonzero return as “authorized,” including values that the developer intended to represent error codes.
Details: The FuturXE (FXE) token contract implemented a custom isContract() function that returned a uint256 rather than a bool. The function used EXTCODESIZE to check whether an address had code, returning the raw code size (e.g., 0x1A3 for a 419-byte contract) rather than a normalized true/false. Downstream, this value was used as a JUMPI condition to gate transfer logic.
Because any nonzero EXTCODESIZE value is truthy, the branch was taken for all contracts, regardless of code size. The bug itself was a logic error — the developer confused “nonzero means contract” with “nonzero means authorized” — but the EVM’s truthy semantics amplified the mistake by treating code sizes like 2, 100, and 10000 identically as “true.”
JUMPI’s role: The raw EXTCODESIZE return value (a non-boolean integer) was used directly as a JUMPI condition. The EVM’s “any nonzero is truthy” behavior converted what should have been a size comparison into a binary true/false decision.
Impact: Limited financial impact due to the token’s small market cap, but it was documented as CVE-2018-12025 and became a canonical example of boolean confusion in smart contracts.
References:
Attack Scenarios
Scenario A: Integer Overflow Bypassing a Require Check (Pre-0.8.0)
// Vulnerable: Solidity < 0.8.0, no SafeMath
contract VulnerableToken {
mapping(address => uint256) public balances;
function batchTransfer(address[] calldata receivers, uint256 value) external {
// Compiled to: MUL(receivers.length, value) -> amount
// If receivers.length = 2 and value = 2^255, amount overflows to 0
uint256 amount = receivers.length * value;
// Compiled to: LT(balances[sender], amount) JUMPI(revert)
// Since amount = 0, LT returns 0, JUMPI does NOT branch
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount; // Subtracts 0
for (uint256 i = 0; i < receivers.length; i++) {
balances[receivers[i]] += value; // Adds 2^255 to each
}
}
}
// Attack: call batchTransfer([addr1, addr2], 2^255)
// The overflow makes `amount` = 0, bypassing the balance check.
// Each receiver gets 2^255 tokens for free.Scenario B: Truthy Non-Boolean Return Value Bypassing Validation
contract VulnerableExchange {
function isValidSignature(address signer, bytes32 hash, bytes calldata sig)
internal view returns (bool)
{
// staticcall to signer's isValidSignature function
(bool success, bytes memory result) = signer.staticcall(
abi.encodeWithSelector(IERC1271.isValidSignature.selector, hash, sig)
);
// If signer is an EOA (no code), staticcall succeeds
// but result is empty. Loading from result reads stale memory.
if (success && result.length >= 32) {
// SAFE: checks return data length
return abi.decode(result, (bool));
}
// VULNERABLE alternative (no length check):
// if (success) {
// // Stale memory may contain nonzero bytes
// // JUMPI treats nonzero as "valid signature"
// return abi.decode(result, (bool));
// }
return false;
}
}Scenario C: Inline Assembly Subverting Require Pattern
contract VulnerableVault {
address public owner;
mapping(address => uint256) public deposits;
function withdraw(uint256 amount) external {
require(deposits[msg.sender] >= amount, "insufficient balance");
// Intended: send ETH to msg.sender
// Bug: assembly block corrupts the stack or memory used by
// a subsequent require check
assembly {
let success := call(gas(), caller(), amount, 0, 0, 0, 0)
// Developer forgot to check `success`
// If the call fails, execution continues without reverting
}
// This state update happens regardless of whether the ETH transfer succeeded
deposits[msg.sender] -= amount;
}
}
// Attack: If the low-level call fails (e.g., caller is a contract
// whose receive() reverts), the vault still decrements the depositor's
// balance without sending ETH. Repeated calls drain the accounting
// while ETH stays in the vault, available for the attacker to extract
// via a different path.Scenario D: Short-Circuit Evaluation with Side Effects
contract VulnerableLending {
mapping(address => uint256) public collateral;
mapping(address => uint256) public debt;
IOracle public oracle;
function liquidate(address borrower) external {
// Compiled to two JUMPI sequences (short-circuit &&):
// 1. Evaluate isUndercollateralized() -> JUMPI (if false, skip to revert)
// 2. Evaluate isLiquidationOpen() -> JUMPI (if false, revert)
require(
isUndercollateralized(borrower) && isLiquidationOpen(),
"cannot liquidate"
);
// Problem: isUndercollateralized() calls oracle.getPrice(),
// which is an external call. During this call, the oracle
// contract can reenter this contract and manipulate state
// before isLiquidationOpen() is evaluated.
_executeLiquidation(borrower);
}
function isUndercollateralized(address borrower) internal view returns (bool) {
uint256 price = oracle.getPrice(); // External call -- reentrancy window
return collateral[borrower] * price < debt[borrower] * 1e18;
}
function isLiquidationOpen() internal view returns (bool) {
return block.timestamp >= liquidationStartTime;
}
}Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Integer overflow bypassing JUMPI conditions | Use Solidity >= 0.8.0 with built-in overflow checks | Compiler automatically inserts overflow checks before arithmetic; overflows revert instead of wrapping. For legacy contracts, use OpenZeppelin SafeMath. |
| T1: Overflow in complex expressions | Break compound expressions into checked steps | Replace a * b * c with temp = a * b; result = temp * c; so each multiplication is individually checked. |
| T2: Truthy non-boolean from external calls | Validate return data length and decode strictly | require(result.length >= 32); bool valid = abi.decode(result, (bool)); — Solidity’s ABI decoder rejects non-canonical booleans. |
| T2: Stale memory from empty return data | Check returndatasize() before reading return data | In assembly: if iszero(returndatasize()) { revert(0, 0) } to explicitly handle empty returns from EOAs. |
| T3: Assembly bypassing require patterns | Minimize inline assembly; audit all hand-rolled JUMPI conditions | Use Solidity high-level constructs wherever possible. When assembly is necessary, ensure every code path that should revert has an explicit revert. |
| T3: Assert gas griefing | Prefer require() over assert() for input validation | Reserve assert() for invariant checks that should never fail. Use require() with error messages for user-facing validation. |
| T4: Short-circuit side effects | Place pure/view conditions before external-call conditions | In A && B, ensure A has no side effects or reentrancy risk. Move oracle calls and external interactions after all local state checks. |
| T4: Reentrancy during condition evaluation | Use reentrancy guards around functions with compound conditions | OpenZeppelin ReentrancyGuard prevents re-entrant calls that could manipulate state between JUMPI evaluations. |
| T5: Gas-based branch inference | Not practically mitigable on public chains; for private chains, use constant-gas execution patterns | Design branch paths to consume similar gas amounts if branch privacy is required. |
| General: Dynamic jump destinations | Avoid computing JUMPI destinations at runtime | Use Solidity’s high-level control flow (if/else, loops) which compiles to static JUMPI targets. Avoid raw JUMP/JUMPI in assembly. |
Compiler/EIP-Based Protections
- Solidity >= 0.8.0: All arithmetic operations revert on overflow/underflow by default. The compiler inserts
ADD ... DUP ... LT ... JUMPI(revert)sequences after every addition, multiplication, and subtraction. This eliminates T1 for new contracts but does not protect legacy contracts or unchecked blocks. unchecked { }blocks (Solidity >= 0.8.0): Explicitly opt out of overflow checks for gas optimization. Code insideuncheckedreverts to pre-0.8.0 wrapping behavior. Auditors should scrutinize everyuncheckedblock for potential overflow-based JUMPI bypass.- EOF / EIP-3540 (RJUMPI): Replaces dynamic JUMPI with static relative jumps (
RJUMPI) that have deploy-time validated destinations. Eliminates invalid-jump-destination bugs entirely for EOF contracts. - EIP-7921 (Proposed): Would relax JUMPDEST validation to allow jumps to any 0x5B byte. Security implication: existing contracts with dynamic JUMPI targets could gain unintended jump destinations in PUSH immediate data.
- Solidity ABI Decoder v2: Strictly validates decoded boolean values (only
0x00or0x01), providing JUMPI-level protection against truthy non-boolean returns when usingabi.decode().
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | Critical | High | BEC Token (26.6M, 2026), dozens of overflow exploits |
| T2 | Smart Contract | High | Medium | 0x Protocol signature forgery (2019), FuturXE boolean bug (CVE-2018-12025) |
| T3 | Smart Contract | High | Medium | Inline assembly vulnerabilities in wagmi-leverage (2024), Solidity optimizer bugs |
| T4 | Smart Contract | Medium | Medium | No single large exploit; pattern present in multiple DeFi audit findings |
| T5 | Smart Contract | Low | Low | Theoretical on public chains; relevant for private EVM implementations |
| P1 | Protocol | Low | Low | No known exploit; safety feature working as designed |
| P2 | Protocol | Low | Low | No consensus bugs from JUMPDEST analysis; EIP-7921 acknowledges backward-compatibility risk |
| P3 | Protocol | Medium | Medium | EOF adoption progressing; legacy JUMPI contracts will persist indefinitely |
Related Opcodes
| Opcode | Relationship |
|---|---|
| JUMP (0x56) | Unconditional jump. Same JUMPDEST validation requirements as JUMPI, but no condition evaluation. JUMPI = JUMP + conditional logic. |
| JUMPDEST (0x5B) | Marks valid jump targets. Both JUMP and JUMPI must target a JUMPDEST or execution reverts. JUMPDEST is the gate that prevents jumping into arbitrary bytecode. |
| ISZERO (0x15) | Inverts a boolean: returns 1 if input is 0, else returns 0. Used immediately before JUMPI in the require() compilation pattern: ISZERO(condition) JUMPI(revert). |
| LT (0x10) | Unsigned less-than comparison. Produces the 0/1 condition fed to JUMPI for require(a >= b) checks (compiled as LT(a,b) JUMPI(revert)). |
| GT (0x11) | Unsigned greater-than comparison. Produces conditions for require(a <= b) checks. Like LT, feeds a strict 0/1 value to JUMPI. |
| EQ (0x14) | Equality comparison. Used in require(a == b) and switch statements. Returns 0 or 1, feeding JUMPI for equality-gated branches. |