Opcode Summary

PropertyValue
Opcode0x14
MnemonicEQ
Gas3
Stack Inputa, b
Stack Outputa == b (1 if true, 0 if false)
Behavior256-bit equality comparison. Returns 1 if both operands are bitwise identical, 0 otherwise. Sign-agnostic (same result for signed and unsigned interpretation).

Threat Surface

EQ tests whether two 256-bit stack values are bitwise identical. Unlike LT/GT/SLT/SGT, EQ has no signed/unsigned ambiguity — bitwise equality is the same regardless of interpretation. Its threat surface is different from the comparison opcodes:

  1. Strict equality is fragile: EQ returns true only for an exact match. In a 256-bit space, the probability of two independently derived values being equal is astronomically low. When contracts use EQ to enforce invariants on values that can be externally influenced (balances, timestamps, states), an attacker who can shift the value by even 1 wei breaks the invariant. This is the “strict equality” vulnerability class.

  2. EQ is the wrong tool for range checks: DeFi calculations involving token amounts, prices, and interest rates produce values with rounding error. Using EQ to validate these values (instead of checking whether they fall within an acceptable range) creates brittle logic that fails on legitimate rounding differences and can be deliberately broken by attackers.

  3. EQ on externally manipulable state: Contract balances (address(this).balance), token balances, storage slots, and block properties can all be externally influenced. Any strict equality check on these values is vulnerable to manipulation: an attacker sends 1 wei of ETH to break a balance == expectedValue assertion.

  4. EQ in function dispatch: The EVM uses EQ to match function selectors in the dispatch table (selector == 0xABCDEF12). Selector collisions (different functions with the same 4-byte selector) can cause incorrect dispatch, though this is primarily a tooling/compiler concern.


Smart Contract Threats

T1: Strict Equality on Contract Balance — Denial of Service (Critical)

Using address(this).balance == expectedValue (or any strict equality check on contract balance) creates a denial-of-service vector. Ether can be forcibly sent to any contract address through:

  • SELFDESTRUCT from another contract: Forces ETH to the target with no way to reject it
  • Mining/validator rewards: Block rewards can be directed to any address
  • Pre-deployment sends: ETH sent to a CREATE2-deterministic address before the contract is deployed

Once the balance deviates from the expected value, every function that includes the strict equality check reverts permanently.

// VULNERABLE: strict equality on balance
function deposit() external payable {
    require(address(this).balance == totalDeposits + msg.value);
    // Attacker sends 1 wei via selfdestruct → balance is off by 1
    // All future deposits revert forever
    totalDeposits += msg.value;
}

T2: Strict Equality in DeFi Price/Amount Validation (High)

DeFi calculations involve integer division, which truncates. The result of (a * b) / c depends on rounding, and two independently computed values that “should” be equal may differ by 1 due to rounding direction:

  • AMM: expected output vs. actual output can differ by 1 wei due to rounding in getAmountOut
  • Lending: computed interest may round differently on deposit vs. withdrawal path
  • Staking: reward calculations across different code paths may produce off-by-one results

Using EQ to assert that two independently computed values match fails intermittently on rounding, creating unreliable contract behavior. Attackers can deliberately trigger the rounding direction that breaks the equality.

T3: State Matching Exploits — Manipulating Values to Match EQ (High)

Conversely, if a contract uses EQ to check whether a value matches an expected pattern (for authentication or authorization), and the attacker can influence one side of the comparison, they can craft inputs that match:

  • Hash collision for function selectors: With only 4 bytes, collision probability is ~1 in 2^32. Attackers have generated function signatures that produce specific selectors.
  • Slot collision in mappings: keccak256(key, slot) could theoretically collide for crafted keys, though this is computationally infeasible for 256-bit hashes.

T4: Race Condition / Timing-Dependent Equality (Medium)

Equality checks on values that change between blocks (prices, balances, nonces) create race conditions:

// Check if price hasn't changed since user submitted intent
require(currentPrice == userExpectedPrice);

Between the user’s transaction submission and execution, the price may change, causing legitimate transactions to revert. MEV bots can sandwich these transactions: observe the user’s expected price, manipulate the price away from it, let the user’s tx revert, then revert their manipulation.

T5: Boolean Logic via EQ and ISZERO (Medium)

EQ is frequently used with ISZERO for boolean logic patterns in compiled Solidity:

  • a == true compiles to EQ(a, 1) — but if a is any non-zero value besides 1, this returns false
  • a != b compiles to ISZERO(EQ(a, b))

The danger: if a boolean variable contains a value other than 0 or 1 (possible via assembly manipulation, storage corruption, or ABI decoding of non-standard values), EQ(a, 1) may return false for a “truthy” value. Pre-Solidity 0.8.0 and assembly code can produce non-standard boolean values.


Protocol-Level Threats

P1: No DoS Vector (Low)

EQ costs a fixed 3 gas. Purely stack-based, no dynamic gas component.

P2: Consensus Safety (Low)

Bitwise equality on 256-bit values is trivially deterministic. All EVM implementations agree. No known consensus divergence.

P3: No State Impact (None)

EQ modifies only the stack.

P4: Function Selector Dispatch (Low)

The Solidity compiler generates a selector dispatch table using EQ: EQ(selector, 0x70a08231) for each function. If two functions in the same contract have colliding 4-byte selectors (impossible in Solidity which checks at compile time, but possible in hand-crafted bytecode), the first matching EQ would route to the wrong function. This is primarily a concern for proxy contracts where selectors from different facets/implementations could collide.


Edge Cases

Edge CaseBehaviorSecurity Implication
EQ(0, 0)Returns 1 (true)Correct
EQ(MAX_UINT256, MAX_UINT256)Returns 1 (true)Correct; same value regardless of signed interpretation
EQ(0, 1)Returns 0 (false)Correct
EQ(int256(-1), uint256(MAX))Returns 1 (true)Same bit pattern: 0xFF...FF. EQ is sign-agnostic
EQ(a, a) for any aReturns 1 (true)Reflexive; always true
EQ(2^255, 2^255)Returns 1 (true)Same bits, whether interpreted as -2^255 or 2^255 unsigned
EQ(result, expectedResult) after roundingMay return 0Off-by-one from integer truncation breaks strict equality
EQ(address(this).balance, expectedBalance)Fragile1 wei of forced ETH breaks it permanently
EQ(bool_var, 1) where bool_var = 2Returns 0 (false)Non-canonical boolean: “truthy” value fails strict equality to 1

Real-World Exploits

Exploit 1: Edgeware Lockdrop “Gridlock” — $900M at Risk (July 2019)

Root cause: Strict equality assertion on newly-created contract balance, where the balance could be pre-loaded by an attacker.

Details: The Edgeware Lockdrop contract used assert(address(lockAddr).balance == msg.value) to verify that a newly created Lock contract received exactly the deposited amount. Since Ethereum addresses are deterministic (derived from sender address and nonce), an attacker could pre-calculate the Lock contract’s address and send 1 wei to it before the lock() call. The assertion then failed because the balance included the pre-sent wei: msg.value + 1 != msg.value.

The contract managed over $900M in ETH at the time. The Gridlock attack would have permanently prevented any new lock operations, effectively bricking the lockdrop.

EQ’s role: The == operator compiled to EQ. The fix replaced it with >=, demonstrating that comparison operators (LT/GT family) are the correct choice for balance validation, not EQ.

References:


Exploit 2: Strict Balance Equality DoS — Generic Pattern (Multiple Incidents)

Root cause: Using this.balance == expectedAmount as an invariant, which can be broken by forcibly sending ETH.

Details: Multiple contracts have used strict equality on address(this).balance for accounting invariants. A widely-documented pattern:

function gameRound() external payable {
    require(msg.value == 1 ether);
    players.push(msg.sender);
    if (address(this).balance == 10 ether) {  // EQ: exactly 10 ETH
        _payWinner();
    }
}

An attacker uses SELFDESTRUCT from another contract to force-send ETH, making the balance 10.000…001 ether. The == 10 ether check never passes again, and the game is permanently stuck. This pattern has been documented in CTF challenges, audit findings, and educational materials as a canonical smart contract vulnerability.

EQ’s role: EQ is the direct cause: the strict equality check cannot tolerate the forced balance deviation. Using >= (compiled to ISZERO(LT(...))) eliminates the vulnerability.

References:


Exploit 3: zkLend Rounding Exploit — $9.5M Stolen (February 2025)

Root cause: Rounding errors in integer division produced values that deviated from expected amounts, breaking invariants that depended on exact equality or precise balance tracking.

Details: The zkLend protocol on StarkNet was exploited for $9.5M through a rounding error in the mint() function. The safe_decimal_math::div() function rounded down, and attackers exploited this by making tiny deposits that rounded the minted share amount to zero or near-zero, inflating the exchange rate. Over multiple iterations, the attacker accumulated a massive share imbalance.

While this exploit occurred on StarkNet (not EVM), the underlying principle applies directly to EVM contracts: integer division truncation means computed values are not exactly what mathematical formulas predict. Any contract that uses strict equality to cross-check values derived from division-based calculations is vulnerable to deliberate rounding manipulation.

EQ’s role: The general principle — strict equality breaks on rounding. Any protocol that validated share calculations with == instead of accepting a small tolerance would be vulnerable to this class of attack.

References:


Attack Scenarios

Scenario A: Selfdestruct Breaks Balance Invariant

contract VulnerableGame {
    uint256 public targetBalance = 10 ether;
    address[] public players;
    
    function play() external payable {
        require(msg.value == 1 ether);
        players.push(msg.sender);
        
        // EQ check: exactly 10 ether means game ends
        if (address(this).balance == targetBalance) {
            _selectWinner();
        }
        // Attack: another contract selfdestructs and sends 0.5 ether here
        // Balance is now 10.5 ether at the 10-player mark
        // Game never ends; funds locked forever
    }
}
 
contract Attacker {
    constructor(address target) payable {
        selfdestruct(payable(target));  // Force-sends ETH
    }
}

Scenario B: Rounding Breaks Strict Price Check

contract VulnerableOracle {
    function executeSwap(
        uint256 amountIn,
        uint256 expectedOut
    ) external {
        uint256 actualOut = (amountIn * reserveOut) / (reserveIn + amountIn);
        
        // VULNERABLE: strict equality fails on rounding
        require(actualOut == expectedOut, "price changed");
        // actualOut may be expectedOut - 1 due to integer truncation
        // Legitimate swaps fail intermittently
        
        // BETTER: allow slippage tolerance
        // require(actualOut >= expectedOut * 99 / 100, "excessive slippage");
        
        _executeSwap(msg.sender, amountIn, actualOut);
    }
}

Scenario C: Non-Canonical Boolean Fails EQ Check

contract VulnerableAccess {
    mapping(address => uint256) internal _isAdmin;
    
    function setAdmin(address user) external {
        assembly {
            // Incorrectly stores 2 instead of 1
            sstore(/* slot for _isAdmin[user] */, 2)
        }
    }
    
    function requireAdmin() internal view {
        // EQ(2, 1) returns 0 (false) -- admin check fails!
        require(_isAdmin[msg.sender] == true);
        // "true" in Solidity is 1, but storage has 2
        // The admin is locked out despite being set as admin
        
        // BETTER: use != 0 for boolean checks
        // require(_isAdmin[msg.sender] != 0);
    }
}

Scenario D: Selector Collision in Proxy

// Two functions with colliding 4-byte selectors
// func_2093253501() has selector 0x7ce5e31f
// transfer(address,uint256) has selector 0xa9059cbb
// (These don't actually collide -- but crafted signatures can)
 
// In hand-crafted bytecode proxy:
// If implementation A has function X with selector 0xDEADBEEF
// and implementation B has function Y with the same selector 0xDEADBEEF
// the EQ-based dispatch routes to whichever is checked first

Mitigations

ThreatMitigationImplementation
T1: Strict balance equalityNever use == on address(this).balanceUse >= or track internal accounting separately from actual balance
T1: Forced ETHDesign for unexpected balance increasesInternal accounting variable instead of this.balance for invariants
T2: Rounding in DeFiUse tolerance ranges instead of strict equalityrequire(actual >= expected * (1 - slippage))
T2: Price validationAccept slippage parameter from userrequire(amountOut >= minAmountOut) instead of == expectedAmount
T3: Crafted input matchingDon’t rely on EQ of user-controlled values for authorizationUse cryptographic signatures, not value matching
T4: Timing-dependent equalityUse inequalities for time-sensitive checksrequire(block.timestamp <= deadline) not == deadline
T5: Non-canonical booleansUse != 0 for boolean checks instead of == truerequire(isActive != 0) instead of require(isActive == true)

Compiler/Tool-Based Protections

  • Slither: The dangerous-strict-equalities detector specifically flags == comparisons on msg.value, this.balance, and other externally-influenceable values. The incorrect-equality detector catches related patterns.
  • Solidity best practices: The Solidity documentation explicitly warns against using this.balance for accounting logic.
  • OpenZeppelin: Uses internal accounting (_totalSupply, _balances) rather than relying on address(this).balance for ERC-20 and vault patterns.
  • Formal verification: Certora and similar tools can prove that equality checks are not reachable with manipulated values.

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractCriticalHighEdgeware Gridlock ($900M at risk), multiple CTF/audit findings
T2Smart ContractHighHighzkLend ($9.5M), rounding errors across DeFi
T3Smart ContractHighLowSelector collision research
T4Smart ContractMediumMediumMEV sandwich attacks on strict price checks
T5Smart ContractMediumLowAssembly/storage corruption edge cases
P1ProtocolLowN/A
P2ProtocolLowN/A
P4ProtocolLowLowProxy selector collision research

OpcodeRelationship
ISZERO (0x15)ISZERO(a) is equivalent to EQ(a, 0). Used for boolean negation and zero-checking
LT (0x10)Comparison operators are usually the correct alternative to EQ for range/threshold checks
GT (0x11)Greater-than comparison; >= compiles to ISZERO(LT(...)), preferred over ==
BALANCE (0x31)Returns contract balance; EQ on BALANCE is the root of strict-equality DoS
SELFBALANCE (0x47)Returns address(this).balance; same strict-equality risk as BALANCE
SELFDESTRUCT (0xFF)Primary mechanism to force-send ETH to break EQ-based balance checks
KECCAK256 (0x20)Hash comparison via EQ; collision resistance of Keccak-256 makes EQ safe for hash matching