Opcode Summary

PropertyValue
Opcode0x47
MnemonicSELFBALANCE
Gas5
Stack Input(none)
Stack Outputaddress(this).balance (balance of the executing contract in wei)
BehaviorPushes the ETH balance (in wei) of the currently executing contract onto the stack. Introduced in EIP-1884 (Istanbul, December 2019) as a gas-efficient alternative to BALANCE(ADDRESS()), which costs 100-2600 gas depending on warm/cold access. SELFBALANCE always costs 5 gas because it reads from the current execution context — no state trie lookup is needed for the executing contract’s own balance. Solidity automatically compiles address(this).balance to SELFBALANCE since the Istanbul fork. In a DELEGATECALL context, SELFBALANCE returns the balance of the proxy (the contract whose storage is being used), not the implementation contract whose code is executing.

Threat Surface

SELFBALANCE is the gas-optimized self-referential balance query introduced by EIP-1884 to avoid the expensive BALANCE(ADDRESS()) pattern. While it costs only 5 gas (compared to 100-2600 gas for BALANCE), it returns the exact same value and inherits every security vulnerability that applies to reading address(this).balance.

The threat surface centers on four properties:

  1. SELFBALANCE is externally manipulable. ETH can be forced into any contract via SELFDESTRUCT (bypasses receive/fallback), via coinbase rewards (block proposer directs fees to the contract’s address), and via pre-funding a CREATE2 address before deployment. SELFBALANCE faithfully reflects this manipulated balance. The 5-gas cost encourages frequent balance reads, but cheaper reads don’t make the underlying value any more trustworthy — they just make it easier to build logic around an unreliable input.

  2. SELFBALANCE in DELEGATECALL returns the proxy’s balance. When contract A delegatecalls contract B, B’s code executes with A’s storage and A’s balance. SELFBALANCE inside B returns address(A).balance, not address(B).balance. This is correct for proxy patterns but creates confusion when implementation contracts are called both directly and via delegatecall — the same code path sees different balances depending on the call context. Libraries or shared implementations that make decisions based on SELFBALANCE can behave inconsistently or be exploited when the proxy’s balance differs from what the implementation logic expects.

  3. Lower gas cost encourages adoption of a fragile pattern. Before EIP-1884, checking one’s own balance cost 400-700 gas (BALANCE + ADDRESS), discouraging frequent use. SELFBALANCE at 5 gas removed this friction, making it cheap to embed balance reads in hot paths, loop iterations, and modifier checks. The result is more contracts using address(this).balance for business logic — the exact anti-pattern that leads to forced-ETH vulnerabilities, strict-equality DoS, and share dilution attacks.

  4. SELFBALANCE inherits all BALANCE vulnerabilities. Every threat documented for the BALANCE opcode (0x31) — forced ETH via SELFDESTRUCT, strict equality DoS, balance-as-state-proxy, front-running balance checks — applies identically to SELFBALANCE. The only difference is gas cost and the absence of warm/cold pricing. SELFBALANCE is not “safer” than BALANCE; it is merely cheaper.


Smart Contract Threats

T1: Forced ETH via SELFDESTRUCT/Coinbase Breaking Balance Logic (Critical)

SELFBALANCE reflects the contract’s true ETH balance, which can be inflated by external parties without any interaction with the contract’s code:

  • SELFDESTRUCT forced transfer. An attacker deploys a contract loaded with ETH and calls SELFDESTRUCT(targetAddress). The target receives the ETH without its receive() or fallback() executing. Post-Dencun (EIP-6780), SELFDESTRUCT still transfers ETH even though contract deletion is restricted to same-transaction creation. The forced-ETH vector is fully operational.

  • Coinbase rewards. A block proposer (validator) can direct priority fees and MEV tips to any address, including a contract. This is a protocol-level transfer that bypasses all Solidity-level guards.

  • CREATE2 pre-funding. The deployment address of a contract is deterministic via CREATE2. An attacker can compute the address and send ETH to it before the contract is deployed. The constructor sees a non-zero starting SELFBALANCE.

Any contract that uses address(this).balance (compiled to SELFBALANCE) to determine when a funding goal is reached, to calculate share prices, or to gate state transitions is vulnerable. The attacker can inflate or deflate the contract’s effective balance at will.

Why it matters: This is the most common and most dangerous misuse of SELFBALANCE. Forced ETH breaks every assumption about “my balance reflects only legitimate deposits.”

T2: Strict Balance Equality Checks — Permanent DoS (High)

Using == on address(this).balance creates a trivially breakable invariant. An attacker who forces even 1 wei into the contract via SELFDESTRUCT permanently breaks any require(address(this).balance == expectedAmount) check:

contract VulnerableGame {
    uint256 public constant TARGET = 7 ether;
 
    function deposit() external payable {
        require(msg.value == 1 ether);
        require(address(this).balance <= TARGET);
        if (address(this).balance == TARGET) {
            payable(msg.sender).transfer(address(this).balance);
        }
    }
}

Forcing 1 wei into the contract means address(this).balance can never exactly equal TARGET after legitimate deposits. The contract is permanently bricked — no player can ever win.

This pattern also affects assertion-based invariants like assert(address(lockAddr).balance == msg.value) in lockdrop contracts, where pre-funding the deterministic deployment address breaks the assertion for all future callers.

Why it matters: Strict equality on balance is a well-known anti-pattern, yet it continues to appear in production code, CTF challenges, and tutorial contracts. The Gridlock bug (see BALANCE threat model) threatened $900M in locked ETH with this exact vector.

T3: SELFBALANCE in DELEGATECALL — Context Confusion (High)

When a proxy contract delegatecalls an implementation, the implementation’s code sees the proxy’s balance via SELFBALANCE, not its own. This is by design (the implementation executes in the proxy’s context), but it creates exploitable confusion:

  • Implementation logic assumes its own balance. If the implementation contract has logic like “if my balance exceeds X, do Y,” and it’s called via delegatecall from a proxy with a different balance, the logic triggers at the wrong threshold. A proxy with a large balance may satisfy conditions the implementation was never designed to handle, or a proxy with a small balance may fail conditions that pass when the implementation is called directly.

  • Shared implementations across multiple proxies. A single implementation contract may serve dozens of proxy instances (e.g., a factory pattern). Each proxy has a different balance. The implementation’s SELFBALANCE-dependent logic behaves differently for each proxy, and an attacker who can control one proxy’s balance (via forced ETH) can trigger unintended behavior.

  • Direct calls to the implementation. If the implementation is callable directly (not just via delegatecall), SELFBALANCE returns the implementation’s own balance. Access control or logic checks that pass on the proxy (high balance) may fail on the implementation (low/zero balance), or vice versa. This inconsistency is exploitable when implementation contracts are not protected against direct calls.

Why it matters: Proxy patterns (UUPS, Transparent, Diamond) secure billions in TVL. Any SELFBALANCE-dependent logic in implementation contracts must account for the delegatecall context or risk inconsistent behavior across proxy instances.

T4: Using Self-Balance for Access Control or State Gating (High)

Some contracts use address(this).balance as a proxy for state, assuming that balance changes only through their own deposit/withdraw logic:

  • Balance-gated functions. A contract that gates withdrawal with require(address(this).balance >= threshold) can have that threshold prematurely satisfied by forced ETH, enabling early or unauthorized withdrawals.

  • Share/ratio calculations. DeFi pools that calculate user shares as (userDeposit * totalShares) / address(this).balance are vulnerable to share dilution. Forcing ETH into the pool inflates the denominator, reducing the value of all existing shares.

  • Balance as access control. Any pattern like require(address(this).balance > 0, "contract not funded") as a prerequisite for privileged operations can be triggered by anyone sending 1 wei via SELFDESTRUCT.

Why it matters: Balance is an externally manipulable value masquerading as internal state. Using it for authorization or state transitions grants any third party the ability to influence contract behavior.

T5: Race Conditions Between Balance Check and Use (Medium)

SELFBALANCE reads a point-in-time snapshot of the contract’s balance. Between reading the balance and acting on it, the balance can change:

  • Within the same transaction. A contract reads its balance, performs an external call (CALL, DELEGATECALL), and then reads its balance again, assuming it changed by a known amount. The external call could trigger callbacks that send or receive additional ETH, making the second read unpredictable.

  • Check-then-act patterns. A function checks address(this).balance >= amount, then transfers amount to a recipient. If the function is called reentrantly, the balance check may pass twice but the contract only has enough for one transfer, leading to a failed second transfer or an underflow in internal accounting.

  • Cross-function inconsistency. One function reads SELFBALANCE and stores it in a local variable. Another function is invoked (via reentrancy or in the same transaction) and modifies the actual balance. The first function continues with a stale value.

Why it matters: SELFBALANCE is cheap enough to use frequently, but each read is a snapshot. Reentrancy and cross-function interactions make it unreliable as a consistency anchor across operations.


Protocol-Level Threats

P1: No Gas DoS Vector (Low)

SELFBALANCE costs a fixed 5 gas with no dynamic component and no warm/cold distinction. It reads from the current execution context (the account state is already loaded for the executing contract), so there is no additional disk I/O. It cannot be used for gas griefing or state-access DoS. This is a strict improvement over BALANCE(ADDRESS()), which is subject to EIP-2929 warm/cold pricing.

P2: Consensus Safety (Low)

SELFBALANCE is deterministic — it returns the current account balance from the execution context, which is committed in the state trie. All conformant client implementations agree on the result. No consensus bugs have been attributed to SELFBALANCE. The opcode was introduced cleanly in Istanbul with no ambiguity in its specification.

P3: EIP-1884 Gas Repricing Impact — Broken Gas Assumptions (Medium)

EIP-1884 (Istanbul) introduced SELFBALANCE alongside gas increases for SLOAD (200→800), BALANCE (400→700), and EXTCODEHASH (400→700). Contracts deployed before Istanbul that hardcoded gas estimates for address(this).balance (which compiled to BALANCE(ADDRESS())) saw their gas costs change. While SELFBALANCE itself is cheaper, the surrounding gas landscape shifted:

  • Pre-Istanbul contracts calling other contracts with fixed gas stipends may have broken when SLOAD and BALANCE became more expensive.
  • The Aragon voting contract was identified as vulnerable to the gas repricing — its gasleft() check could fail under the new costs, potentially blocking governance votes.
  • Contracts that previously used balance(address()) in inline assembly (not optimized to SELFBALANCE by the compiler) continued paying the higher BALANCE cost.

P4: SELFBALANCE Across Hard Forks (Low)

SELFBALANCE was introduced in EIP-1884 (Istanbul, December 2019) and has not changed semantics since. Key timeline:

  • Pre-Istanbul: No SELFBALANCE opcode; address(this).balance compiled to BALANCE(ADDRESS()) at 400 gas
  • Istanbul (EIP-1884): SELFBALANCE introduced at 5 gas; BALANCE raised to 700 gas
  • Berlin (EIP-2929): BALANCE moved to 100/2600 warm/cold model; SELFBALANCE unchanged at 5 gas
  • Dencun (EIP-6780): SELFDESTRUCT neutered (no code deletion except same-tx), but forced ETH transfer preserved — SELFBALANCE still reflects forced ETH

No upcoming EIPs alter SELFBALANCE’s semantics or gas cost.


Edge Cases

Edge CaseBehaviorSecurity Implication
SELFBALANCE in DELEGATECALLReturns the balance of the delegating (proxy) contract, not the implementationImplementation code sees the proxy’s balance; logic that assumes its own balance will behave incorrectly across different proxy instances
SELFBALANCE in STATICCALLReturns the contract’s balance at the time of the static callBalance is read-only in this context; no state changes possible, but the value may be stale if used across calls
SELFBALANCE in constructorReturns msg.value plus any ETH pre-funded at the deployment address (via CREATE2 pre-calculation)Constructor cannot assume address(this).balance == msg.value; pre-funded ETH inflates the initial balance
SELFBALANCE after receiving forced ETH (same tx)Reflects the updated balance including SELFDESTRUCT transfersWithin a transaction, forced ETH is immediately visible to SELFBALANCE
SELFBALANCE after CALL sending ETH (same tx)Reflects the reduced balance after the outgoing transferBalance decreases are visible immediately; mid-transaction balance checks are consistent with transfers
SELFBALANCE == 0 after SELFDESTRUCT (same tx, pre-Dencun)Returns 0 after the contract self-destructs within the same transactionPost-Dencun: SELFDESTRUCT only transfers ETH; balance goes to 0 if all ETH is sent, but contract persists
SELFBALANCE vs BALANCE(ADDRESS())Identical return value; SELFBALANCE costs 5 gas, BALANCE(ADDRESS()) costs 100-2600 gasSolidity optimizes address(this).balance to SELFBALANCE automatically since Istanbul. Inline assembly balance(address()) does NOT get this optimization
SELFBALANCE in inline assemblyselfbalance() in Yul/inline assembly returns the same valueDevelopers using assembly must use selfbalance() explicitly; balance(address()) uses the more expensive BALANCE opcode

Real-World Exploits

Exploit 1: Gridlock Bug — $900M Lockdrop DoS via Strict Balance Equality (July 2019)

Root cause: Edgeware’s Lockdrop contract used a strict equality check on the balance of newly created lock contracts, which could be broken by pre-funding the deterministic deployment address.

Details: Edgeware’s Lockdrop contract allowed users to lock ETH in individual lock contracts to earn EDG tokens. The lock() function created a new contract via CREATE and validated:

assert(address(lockAddr).balance == msg.value);

Because CREATE addresses are deterministic (based on deployer address + nonce), an attacker could pre-compute the lock contract’s address and send ETH to it before lock() was called. The assertion would permanently fail because the lock contract’s balance would be msg.value + preFundedAmount, never equaling msg.value.

At the time of discovery, the Lockdrop contract held over $900 million in locked ETH. The bug was discovered by community researcher Neil M before it was exploited and confirmed by Runtime Verification’s formal verification team.

SELFBALANCE’s role: The balance read (whether via BALANCE or SELFBALANCE) returned the pre-funded balance plus msg.value, breaking the strict == check. The vulnerability exists regardless of which balance opcode is used — the root cause is trusting balance as a reliable invariant.

Impact: $900M at risk. Discovered before exploitation. Led to widespread awareness of the forced-ETH anti-pattern.

References:


Exploit 2: Akutars NFT — $34M Permanently Locked via Balance Logic Bug (April 2022)

Root cause: The AkuAuction contract’s withdrawal logic depended on a balance-related state condition that could never be satisfied due to a bidding counter mismatch, permanently locking all ETH.

Details: The Akutars NFT project conducted a Dutch Auction in April 2022. The auction smart contract had two critical vulnerabilities:

  1. DoS in refund processing. The processRefunds() function iterated over bidders and sent ETH refunds via external calls. A malicious bidder deployed a contract that reverted on receiving ETH, blocking the entire refund loop. This was a classic “push-over-pull” vulnerability.

  2. Withdrawal permanently blocked. The claimProjectFunds() function required refundProgress >= totalBids before the team could withdraw. However, totalBids was incremented on every bid (including re-bids), reaching 5,495, while refundProgress could only reach the actual number of unique bidders (3,669). This condition could never be satisfied, making withdrawal impossible regardless of the refund DoS.

The contract’s balance — 11,539 ETH worth approximately $34 million — became permanently inaccessible. No SELFDESTRUCT recovery was possible because the withdrawal function’s logic check would still fail.

SELFBALANCE’s role: The contract’s funds were readable via SELFBALANCE (the balance was right there in the contract), but no code path could ever move them out. This is a balance-accessibility failure: the contract can see its own balance but can never spend it, demonstrating that SELFBALANCE as a read operation is useless without correct withdrawal logic.

Impact: $34M permanently locked. The Akutars team had to issue refunds and NFTs via a new contract.

References:


Exploit 3: EtherGame-Class Forced ETH Attacks — Recurring Pattern (Ongoing)

Root cause: Contracts that gate functionality on address(this).balance reaching a specific threshold, combined with forced ETH injection via SELFDESTRUCT.

Details: A widely documented and recurring vulnerability pattern in production contracts, CTF challenges, and DeFi protocols. The canonical example is a game contract where the last depositor to reach a target balance wins the pot:

function deposit() external payable {
    require(msg.value == 1 ether);
    require(address(this).balance <= 7 ether);
    if (address(this).balance == 7 ether) {
        payable(msg.sender).transfer(address(this).balance);
    }
}

An attacker deploys a contract with 5+ ETH and calls SELFDESTRUCT(gameAddress). The game contract’s balance jumps past the target. No legitimate depositor can ever trigger the win condition because address(this).balance already exceeds 7 ether, and the <= guard prevents new deposits.

Post-Dencun (EIP-6780), SELFDESTRUCT still transfers ETH even though contract code deletion is restricted to same-transaction creation. The forced-ETH vector remains fully operational.

SELFBALANCE’s role: address(this).balance compiles to SELFBALANCE, which faithfully reflects the forced ETH. The contract has no way to distinguish legitimate deposits from externally injected ETH.

Impact: Recurring pattern across multiple production contracts and educational platforms (OpenZeppelin Ethernaut Level 7, Capture the Ether, Damn Vulnerable DeFi).

References:


Exploit 4: EIP-1884 Gas Repricing — Aragon Governance Voting Disruption (December 2019)

Root cause: The Istanbul hard fork (EIP-1884) changed gas costs for SLOAD and BALANCE, breaking contracts that relied on fixed gas estimates. SELFBALANCE was introduced as part of the same EIP.

Details: Before Istanbul, several contracts hardcoded gas estimates based on the pre-Istanbul cost of BALANCE (400 gas) and SLOAD (200 gas). When EIP-1884 raised SLOAD to 800 gas and BALANCE to 700 gas, these contracts’ gas calculations became incorrect.

The Aragon voting contract was flagged by ChainSecurity as vulnerable: its gasleft() check in the voting function could fail under the new gas costs, potentially blocking governance proposals from executing. The Aragon team had to deploy a workaround before Istanbul activated.

While SELFBALANCE itself was the solution (providing a cheap self-balance check at 5 gas), the EIP that introduced it simultaneously broke contracts relying on the old BALANCE pricing. Contracts using balance(address()) in inline assembly (rather than Solidity’s address(this).balance) did not benefit from the SELFBALANCE optimization and continued paying the elevated BALANCE cost.

SELFBALANCE’s role: SELFBALANCE was introduced specifically to mitigate the gas increase on BALANCE for the common self-balance query pattern. But the transition period created a window where contracts with hardcoded gas assumptions were vulnerable. This demonstrates that gas-cost changes have security implications beyond simple economics.

Impact: Multiple contracts required emergency updates before Istanbul. ChainSecurity’s audit of EIP-1884 identified Aragon and other governance contracts as at-risk.

References:


Attack Scenarios

Scenario A: Forced ETH Breaks Crowdfunding Goal

contract VulnerableCrowdfund {
    uint256 public goal = 100 ether;
    uint256 public deadline;
 
    function contribute() external payable {
        require(block.timestamp < deadline);
    }
 
    function finalize() external {
        require(block.timestamp >= deadline);
        // VULNERABLE: uses SELFBALANCE instead of tracked contributions
        if (address(this).balance >= goal) {
            payable(owner).transfer(address(this).balance);
        } else {
            // Enable refunds
        }
    }
}
 
// Attack: force 100 ETH into the contract via SELFDESTRUCT before the
// deadline. finalize() releases all funds to the owner even though no
// legitimate contributor deposited anything. Alternatively, use it to
// prevent refunds by pushing balance past the goal threshold.
contract ForceFeeder {
    constructor(address payable target) payable {
        selfdestruct(target);
    }
}

Scenario B: SELFBALANCE in DELEGATECALL — Proxy Balance Confusion

contract VaultImplementation {
    uint256 public minOperatingBalance = 1 ether;
 
    function emergencyWithdraw(address payable to) external onlyOwner {
        // VULNERABLE in delegatecall context: address(this).balance
        // returns the PROXY's balance, not the implementation's balance.
        // Different proxies have different balances, so the same
        // threshold check behaves differently per proxy instance.
        require(
            address(this).balance > minOperatingBalance,
            "Below operating minimum"
        );
        uint256 excess = address(this).balance - minOperatingBalance;
        to.call{value: excess}("");
    }
}
 
// Proxy A holds 10 ETH → emergencyWithdraw sends 9 ETH
// Proxy B holds 0.5 ETH → emergencyWithdraw reverts ("Below operating minimum")
// Proxy C has 100 ETH forced via SELFDESTRUCT → emergencyWithdraw sends 99 ETH
//
// The implementation's logic executes identically across all proxies,
// but SELFBALANCE returns different values for each, creating
// unpredictable behavior that an attacker can exploit by force-feeding
// ETH into a specific proxy.

Scenario C: Share Dilution via Forced ETH in DeFi Pool

contract VulnerablePool {
    mapping(address => uint256) public shares;
    uint256 public totalShares;
 
    function deposit() external payable {
        uint256 newShares;
        if (totalShares == 0) {
            newShares = msg.value;
        } else {
            // VULNERABLE: address(this).balance includes forced ETH
            // SELFBALANCE returns the inflated balance
            newShares = (msg.value * totalShares) / address(this).balance;
        }
        shares[msg.sender] += newShares;
        totalShares += newShares;
    }
 
    function withdraw() external {
        uint256 userShares = shares[msg.sender];
        // User gets proportional share of TOTAL balance (including forced ETH)
        uint256 amount = (userShares * address(this).balance) / totalShares;
        shares[msg.sender] = 0;
        totalShares -= userShares;
        payable(msg.sender).transfer(amount);
    }
}
 
// Attack: Before any deposits, force 1000 ETH via SELFDESTRUCT.
// First depositor sends 1 ETH. totalShares == 0, so they get 1e18 shares.
// address(this).balance is now 1001 ETH.
// On withdraw: amount = (1e18 * 1001 ether) / 1e18 = 1001 ETH.
// The first depositor extracts 1000 ETH of forced funds.
// Alternatively, forcing ETH AFTER deposits dilutes existing holders.

Scenario D: Balance Check-Then-Act Race Condition

contract VulnerableEscrow {
    mapping(address => uint256) public deposits;
 
    function deposit() external payable {
        deposits[msg.sender] += msg.value;
    }
 
    function withdrawAll() external {
        uint256 deposited = deposits[msg.sender];
        require(deposited > 0);
 
        // Check: sufficient balance for this withdrawal
        // SELFBALANCE reads the current balance
        require(address(this).balance >= deposited);
 
        // External call BEFORE state update — classic reentrancy setup
        (bool ok,) = msg.sender.call{value: deposited}("");
        require(ok);
 
        // State update happens after the external call
        deposits[msg.sender] = 0;
    }
}
 
// Reentrancy attack: attacker's receive() calls withdrawAll() again.
// In the re-entrant call, deposits[attacker] is still non-zero
// (state not yet updated), and address(this).balance still has enough
// (SELFBALANCE reflects balance before the first transfer completes
// only if the call hasn't yet returned). Classic check-act race condition
// enabled by reading SELFBALANCE before state changes are committed.

Mitigations

ThreatMitigationImplementation
T1: Forced ETH via SELFDESTRUCT/coinbaseTrack deposits with internal state variables; never rely on address(this).balance for business logicmapping(address => uint256) deposits; uint256 totalDeposited; — update on every deposit/withdrawal
T1: CREATE2 pre-fundingDo not assume constructor balance equals msg.valueRead msg.value directly for initial accounting; ignore any pre-existing balance
T2: Strict equality DoSNever use == on balance; use >= or internal accountingReplace assert(addr.balance == amount) with tracked state variables
T3: DELEGATECALL context confusionDo not use SELFBALANCE for logic in implementation contracts that serve proxiesUse internal accounting variables stored in the proxy’s storage slots; if balance checks are needed, document the delegatecall context explicitly
T3: Direct implementation callsPrevent direct calls to implementation contractsUse OpenZeppelin’s _disableInitializers() and onlyDelegateCall modifier
T4: Balance as state proxyMaintain explicit internal accounting for all ETH flowsUse receive() and fallback() to track incoming ETH; use withdrawal pattern with internal balances
T5: Race conditionsChecks-Effects-Interactions pattern; reentrancy guardsUse OpenZeppelin’s ReentrancyGuard; update state before external calls; use transient storage locks (EIP-1153) for cross-function guards
GeneralUse address(this).balance only for informational purposesBalance reads for display, logging, or sanity checks (assert(address(this).balance >= totalDeposited)) are safe; decision-making based on balance is not

Key Design Principle

Never use address(this).balance for business logic. Maintain internal accounting:

contract SafeVault {
    uint256 private _totalDeposited;
    mapping(address => uint256) private _deposits;
 
    function deposit() external payable {
        _deposits[msg.sender] += msg.value;
        _totalDeposited += msg.value;
    }
 
    function totalDeposited() public view returns (uint256) {
        return _totalDeposited;  // Not address(this).balance
    }
}

Compiler/EIP-Based Protections

  • EIP-1884 (Istanbul, 2019): Introduced SELFBALANCE at 5 gas, providing a gas-efficient self-balance query that avoids the elevated BALANCE cost. Solidity automatically compiles address(this).balance to SELFBALANCE.
  • EIP-6780 (Dencun, 2024): SELFDESTRUCT no longer deletes pre-existing contracts but still transfers ETH. Forced-ETH attacks remain viable; only the code-deletion aspect of SELFDESTRUCT is neutered.
  • EIP-1153 (Transient Storage, Dencun 2024): Enables gas-efficient reentrancy guards using transient storage, reducing the cost of protecting SELFBALANCE-dependent functions from race conditions.
  • Solidity >= 0.8.0: Automatic overflow/underflow checks protect against balance arithmetic errors. address(this).balance compiles to SELFBALANCE since Istanbul, regardless of Solidity version.

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractCriticalHighGridlock ($900M at risk), EtherGame-class exploits (recurring)
T2Smart ContractHighMediumGridlock strict equality; EtherGame-class DoS
T3Smart ContractHighMediumProxy balance confusion in factory patterns
T4Smart ContractHighMediumShare dilution in DeFi pools; balance-gated access
T5Smart ContractMediumMediumReentrancy exploits using balance check-then-act
P1ProtocolLowN/A
P2ProtocolLowN/A
P3ProtocolMediumLowAragon governance disruption (Istanbul, 2019)
P4ProtocolLowLow

OpcodeRelationship
BALANCE (0x31)Returns the balance of any address (100/2600 gas warm/cold). SELFBALANCE is the gas-optimized equivalent for querying the executing contract’s own balance. Both return the same value for address(this) and share identical vulnerability surface — forced ETH, strict equality, balance-as-state. SELFBALANCE exists because BALANCE’s gas cost was raised by EIP-1884, making self-balance queries expensive.
ADDRESS (0x30)Returns the current contract’s own address. Before SELFBALANCE existed, the pattern BALANCE(ADDRESS()) was used for self-balance queries. In a DELEGATECALL, ADDRESS returns the delegating (proxy) contract’s address — consistent with SELFBALANCE returning the proxy’s balance.
SELFDESTRUCT (0xFF)Forcibly sends ETH to any address without triggering recipient code. Primary vector for manipulating SELFBALANCE by injecting ETH into a contract. Post-EIP-6780: still transfers ETH even though contract deletion is restricted to same-transaction creation.
CALL (0xF1)The primary mechanism for legitimate ETH transfers that change a contract’s balance. CALL’s value field is reflected in subsequent SELFBALANCE reads. Unlike SELFDESTRUCT, CALL triggers the recipient’s receive/fallback functions.