Opcode Summary

PropertyValue
Opcode0x30
MnemonicADDRESS
Gas2
Stack Input. (none)
Stack Outputaddress(this)
BehaviorPushes the 20-byte address of the currently executing contract onto the stack. In a DELEGATECALL context, returns the address of the calling (proxy) contract, not the contract whose code is executing.

Threat Surface

ADDRESS returns the address of the contract whose storage context is active, not necessarily the contract whose code is running. This distinction is irrelevant in normal CALL execution but becomes critically important under DELEGATECALL, where code from one contract executes against the storage of another.

The threat surface spans three areas:

  1. Delegatecall identity confusion: When implementation contract code executes via DELEGATECALL from a proxy, ADDRESS returns the proxy’s address. Any logic that uses address(this) for self-identification, access control, or token transfers will operate on the proxy’s identity, not the implementation’s. This is by design for proxy patterns, but code that is unaware it may run in a delegatecall context can behave dangerously.

  2. Address-based access control: Contracts that gate functionality on msg.sender == address(this) (self-call checks) or hardcoded address comparisons can be bypassed when the execution context shifts. A contract deployed at address X that checks require(address(this) == X) will fail when called via delegatecall from a proxy at address Y.

  3. Address prediction and front-running: Contract addresses are deterministic — derived from deployer + nonce (CREATE) or deployer + salt + init code hash (CREATE2). Attackers can predict where a contract will be deployed and front-run the deployment, pre-approve tokens to the predicted address, or deploy malicious contracts at collision addresses.


Smart Contract Threats

T1: ADDRESS in Delegatecall Returns Proxy Address (Critical)

When a proxy uses DELEGATECALL to execute implementation code, ADDRESS returns the proxy’s address. This is the foundation of the proxy pattern, but implementation contracts that use address(this) without awareness of the delegatecall context face critical bugs:

contract Implementation {
    function withdraw() external {
        // In delegatecall context, address(this) is the PROXY, not this contract.
        // Tokens held by the implementation contract itself are unreachable.
        uint256 balance = IERC20(token).balanceOf(address(this));
        IERC20(token).transfer(msg.sender, balance);
    }
}

If the implementation contract holds tokens directly (not through the proxy), balanceOf(address(this)) queries the proxy’s balance instead, returning 0 or a different user’s funds. Conversely, if the proxy holds tokens, the implementation code can unknowingly drain them.

This affects every contract that may be used as a delegatecall target: libraries, diamond facets, modular account modules, and upgradeable implementations.

T2: Self-Referential Access Control Bypass via Delegatecall (High)

Contracts commonly use address(this) in access control patterns to verify self-calls or restrict functionality to the contract itself:

contract Wallet {
    modifier onlySelf() {
        require(msg.sender == address(this), "only self-call");
        _;
    }
    
    function executeUpgrade(address newImpl) external onlySelf {
        // Intended: only callable via this contract's own multicall/execute
    }
}

In a delegatecall context, address(this) is the calling contract (proxy). If the proxy has a generic execute() function that performs delegatecall to arbitrary targets, an attacker can craft a call chain where msg.sender == address(this) is satisfied from the proxy’s perspective, bypassing the intended restriction.

This pattern was exploited in modular smart wallet architectures where batch execution functions allowed delegatecall to the wallet’s own address, enabling arbitrary function execution with elevated privileges.

T3: Contract Address Prediction for Front-Running (High)

Contract addresses are fully deterministic:

MethodAddress DerivationPredictable?
CREATEkeccak256(rlp(deployer, nonce))Yes, if nonce is known
CREATE2keccak256(0xff, deployer, salt, keccak256(initCode))Yes, fully deterministic

An attacker who knows the deployment parameters can:

  • Pre-compute the contract address before deployment
  • Send tokens or approvals to the not-yet-deployed address
  • Deploy a malicious contract at the predicted address (for CREATE2, by front-running with the same salt)
  • Exploit time-of-check-to-time-of-use gaps between address computation and deployment

Wallet drainer groups have stolen over $60 million using CREATE2 prediction to bypass wallet security alerts: they get victims to approve transactions targeting a pre-computed address, then deploy a malicious contract at that address afterward.

T4: Hardcoded Address Checks Break Under Proxy Patterns (Medium)

Contracts sometimes hardcode their own expected address or compare address(this) against a constant for anti-clone or licensing enforcement:

contract Licensed {
    address constant AUTHORIZED_DEPLOYMENT = 0x1234...abcd;
    
    modifier onlyAuthorized() {
        require(address(this) == AUTHORIZED_DEPLOYMENT, "unauthorized copy");
        _;
    }
}

This pattern breaks completely when the contract is used behind a proxy (address changes to the proxy’s address) or redeployed to a new address. It also provides no real protection since bytecode can be deployed at any address, and the check can be patched out in a copy.

T5: Uninitialized Implementation Contract Exploitation (Critical)

Implementation contracts behind proxies are deployed as standalone contracts with their own address and storage. If the implementation is not properly initialized (locked), an attacker can call initialization functions directly on the implementation, taking ownership of its storage context:

contract ImplementationV1 {
    address public owner;
    
    function initialize(address _owner) external {
        require(owner == address(0), "already initialized");
        owner = _owner;
    }
    
    function destroy() external {
        require(msg.sender == owner);
        selfdestruct(payable(msg.sender));
    }
}

If initialize() is never called on the implementation itself (only via proxy), an attacker can call initialize() directly, become owner, then call destroy(). If the implementation is self-destructed, all proxies delegating to it become non-functional.

This is exactly what happened in the Parity Wallet hack (November 2017), freezing ~$287 million.


Protocol-Level Threats

P1: ADDRESS in Cross-Chain Bridge Verification (Medium)

Bridge contracts use address(this) to identify themselves in cross-chain message verification. If a bridge contract is deployed behind a proxy and upgraded, the address remains stable (the proxy address). However, if a bridge component uses delegatecall to a shared library and that library references address(this), the returned address depends on which contract initiated the delegatecall — potentially allowing message spoofing if the verification assumes a specific address.

P2: No Gas-Based DoS Vector (Low)

ADDRESS costs a fixed 2 gas with no dynamic component. It reads only the execution context, accesses no storage, and cannot be used for gas griefing.

P3: Consensus Safety (Low)

ADDRESS is trivially deterministic: it returns the address of the current execution context. All EVM implementations agree on its behavior in both normal call and delegatecall contexts. No known consensus divergence has occurred due to ADDRESS.

P4: EVM Equivalence Across L2s (Low)

ADDRESS behaves identically across all EVM-compatible L2s (Optimism, Arbitrum, zkSync, etc.). However, L2s that use system contracts or precompiles at specific addresses may have different address spaces, and contracts that compare address(this) against L1-specific addresses will fail on L2 deployments.


Edge Cases

Edge CaseBehaviorSecurity Implication
ADDRESS in DELEGATECALLReturns the caller’s (proxy’s) address, not the implementation’sCore proxy pattern behavior; implementation code must be delegatecall-aware
ADDRESS in constructorReturns the address being deployed to (computed from deployer + nonce or CREATE2 params)Address is valid even before deployment completes; can be used for self-registration
ADDRESS in STATICCALLReturns the callee’s address (same as normal CALL)No difference from regular call; read-only restriction doesn’t affect ADDRESS
ADDRESS after CREATEParent’s ADDRESS unchanged; child gets keccak256(rlp(parent, nonce))Child address is deterministic from parent address and nonce
ADDRESS after CREATE2Parent’s ADDRESS unchanged; child gets keccak256(0xff, parent, salt, keccak256(initCode))Fully predictable; enables front-running and address collision attacks
ADDRESS in CALLCODE (deprecated)Returns the caller’s address (like DELEGATECALL)Legacy behavior; CALLCODE is deprecated but still functional in EVM
ADDRESS with zero-address contractReturns 0x0000...0000Contracts at the zero address are theoretical; ADDRESS would return zero
ADDRESS across nested DELEGATECALLsReturns the outermost caller’s address (the contract with the actual storage)Chain of delegatecalls all share the same address(this)

Real-World Exploits

Exploit 1: Parity Wallet Library Self-Destruct — $287M Frozen (November 2017)

Root cause: Uninitialized implementation contract (library) that could be taken over and self-destructed, bricking all proxies that depended on it.

Details: The Parity multi-signature wallet used a proxy pattern where thin wallet contracts delegated all calls to a shared library contract at a known address. The library itself was a standalone contract with its own storage. After the first Parity hack (July 2017, $30M stolen via unprotected initWallet()), a fix added an only_uninitialized modifier. However, the library contract itself was never initialized — it was only meant to be called via delegatecall from proxies.

An attacker called initWallet() directly on the library contract (not through a proxy), becoming its owner. They then called kill() (which invoked selfdestruct), permanently destroying the library. Since all 587 Parity wallets depended on delegatecalling this library, they all became non-functional, freezing 513,774 ETH (~$287M).

ADDRESS’s role: The address(this) in the library code was designed to return the proxy’s address during normal delegatecall operation. But when called directly, address(this) returned the library’s own address. The library had no check to detect whether it was being called directly vs via delegatecall — a check like require(address(this) != LIBRARY_ADDRESS) would have prevented the attack.

References:


Exploit 2: Wintermute Profanity Vanity Address Compromise — $160M Stolen (September 2022)

Root cause: Weak entropy in vanity address generation allowed brute-force recovery of the admin private key.

Details: Wintermute used the Profanity tool to generate a vanity admin address (starting with 0x0000000). Profanity used only a 32-bit random seed to derive 256-bit private keys, meaning the entire keyspace was only ~4.3 billion possibilities — trivially brute-forceable with GPUs. The vulnerability was publicly disclosed by 1inch on September 15, 2022. Five days later, an attacker brute-forced Wintermute’s admin key and drained $160M from their vault.

ADDRESS’s role: The exploit is fundamentally about the relationship between an Ethereum address (the output of ADDRESS when executed by that contract) and the private key that controls it. The vanity address generation optimized for a specific address pattern at the cost of key security. The admin address was used in access control checks — once the private key was recovered, the attacker could impersonate the admin. This demonstrates that address-based access control is only as strong as the key generation behind the address.

References:


Exploit 3: CREATE2 Wallet Drainers — $60M+ Stolen (2023-2024)

Root cause: CREATE2 deterministic address prediction used to bypass wallet security alerts and front-run contract deployment.

Details: Attacker groups exploited CREATE2 to pre-compute contract addresses, then tricked users into signing token approvals to those addresses. At signing time, no contract existed at the target address, so wallet security tools couldn’t flag malicious code. After the victim’s approval transaction confirmed, the attacker deployed a malicious drainer contract at the pre-computed address and drained the approved tokens.

One drainer group stole ~3M.

ADDRESS’s role: The deterministic relationship between CREATE2 parameters and the resulting contract address (which ADDRESS returns once deployed) is the core mechanism. The attacker exploits the fact that ADDRESS’s future return value is knowable before deployment. Security tools that check address(this) for code existence at approval time find nothing, because the contract doesn’t exist yet.

References:


Exploit 4: Smart Wallet Account Takeover via Self-Delegatecall (2026)

Root cause: Missing target validation in batch execution allowed delegatecall to the wallet’s own address, bypassing onlySelf access control.

Details: Modular smart wallets (ERC-4337 style) that support batch execution with a list of Call structs allowed users to specify arbitrary target addresses for delegatecall. An attacker passed the wallet’s own address as the delegatecall target within a batch, causing the wallet to execute its own privileged functions (like executeUpgrade or addOwner) with msg.sender == address(this) satisfied — a self-call check bypass.

ADDRESS’s role: The onlySelf modifier checks msg.sender == address(this). In a delegatecall-to-self scenario, this check passes because the delegatecall preserves the execution context’s address. The ADDRESS opcode returns the same value for both the outer call and the inner delegatecall, making the self-call indistinguishable from a legitimate internal call.

References:


Attack Scenarios

Scenario A: Implementation Contract Takeover via Direct Initialization

contract VulnerableImplementation {
    address public owner;
    bool private initialized;
 
    function initialize(address _owner) external {
        require(!initialized, "already initialized");
        owner = _owner;
        initialized = true;
    }
 
    function upgradeToAndCall(address newImpl, bytes calldata data) external {
        require(msg.sender == owner, "not owner");
        // ... upgrade logic ...
    }
 
    function destroy() external {
        require(msg.sender == owner, "not owner");
        selfdestruct(payable(owner));
    }
}
 
// Attack: the implementation is deployed but never initialized directly.
// Attacker calls initialize() on the implementation (not the proxy),
// becomes owner, calls destroy(), bricks all proxies.

Mitigation: Use OpenZeppelin’s _disableInitializers() in the implementation constructor.

Scenario B: Self-Call Check Bypass via Delegatecall

contract ModularWallet {
    struct Call {
        address target;
        uint256 value;
        bytes data;
    }
 
    function executeBatch(Call[] calldata calls) external {
        for (uint i = 0; i < calls.length; i++) {
            // Bug: no check that target != address(this) for delegatecall
            (bool success,) = calls[i].target.delegatecall(calls[i].data);
            require(success);
        }
    }
 
    function addOwner(address newOwner) external {
        require(msg.sender == address(this), "only self-call");
        // ... add owner ...
    }
}
 
// Attack: call executeBatch with target = address(this),
// data = abi.encodeCall(addOwner, attackerAddress).
// delegatecall preserves address(this), so msg.sender == address(this) passes.

Scenario C: Delegatecall-Unaware Library Drains Wrong Contract

contract TokenVault {
    function rescueTokens(address token, address to) external onlyOwner {
        // When called via delegatecall from a proxy, address(this) is the PROXY.
        // This drains the proxy's tokens, not the implementation's.
        uint256 balance = IERC20(token).balanceOf(address(this));
        IERC20(token).transfer(to, balance);
    }
}
 
// If used as an implementation behind a proxy holding user funds,
// the owner can drain ALL proxy-held tokens via this "rescue" function.

Scenario D: CREATE2 Pre-Deployment Approval Attack

// Attacker pre-computes the address of a contract they will deploy
address predicted = address(uint160(uint256(keccak256(abi.encodePacked(
    bytes1(0xff),
    deployer,
    salt,
    keccak256(maliciousInitCode)
)))));
 
// Step 1: Trick victim into approving tokens to 'predicted' address
// (no contract exists yet -- wallet tools see an EOA-like address)
 
// Step 2: Deploy malicious contract at 'predicted' address
MaliciousDrainer drainer = new MaliciousDrainer{salt: salt}();
// drainer is now at 'predicted', with the victim's token approval
 
// Step 3: Drain approved tokens
drainer.drain(victim, token);

Mitigations

ThreatMitigationImplementation
T1: ADDRESS in delegatecallMake implementation contracts delegatecall-awareDocument and test behavior under both direct call and delegatecall contexts
T1: Proxy identityUse ERC-1967 storage slots for proxy metadataStandardized slots prevent storage collision: bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)
T2: Self-call bypassValidate delegatecall targets in batch executionrequire(target != address(this), "no self-delegatecall") in execution loops
T3: Address predictionDon’t rely on address non-existence for securityCheck extcodesize > 0 at execution time, not just at approval time
T3: CREATE2 front-runningUse commit-reveal schemes for sensitive deploymentsCommit salt hash first, reveal and deploy in second transaction
T4: Hardcoded address checksUse immutable variables set in constructor insteadaddress immutable self = address(this); — set once, works across contexts
T5: Uninitialized implementationLock the implementation in its constructorconstructor() { _disableInitializers(); } (OpenZeppelin pattern)
T5: selfdestruct on implementationRemove selfdestruct from implementation contractsPost-Dencun (EIP-6780): SELFDESTRUCT only works in the same transaction as creation
General: Delegatecall detectionCheck if running in delegatecall contextrequire(address(this) == expectedAddress) to detect unexpected delegatecall

Delegatecall-Awareness Pattern

contract DelegatecallAware {
    address private immutable __self = address(this);
 
    modifier noDelegatecall() {
        require(address(this) == __self, "delegatecall not allowed");
        _;
    }
 
    modifier onlyDelegatecall() {
        require(address(this) != __self, "must be delegatecall");
        _;
    }
}

This pattern (used by Uniswap V3, OpenZeppelin) stores the contract’s own address as an immutable at construction time. During execution, comparing address(this) (the ADDRESS opcode result) against the stored value detects whether the code is running via delegatecall.


Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractCriticalHighParity Wallet ($287M frozen), ubiquitous in proxy patterns
T2Smart ContractHighMediumSmart wallet account takeover (2026)
T3Smart ContractHighHighCREATE2 drainers ($60M+ stolen, 2023-2024)
T4Smart ContractMediumLowAnti-clone checks broken by proxies
T5Smart ContractCriticalMediumParity Wallet library self-destruct ($287M)
P1ProtocolMediumLowBridge verification edge cases
P2ProtocolLowN/A
P3ProtocolLowN/A
P4ProtocolLowLowL1/L2 address space differences

OpcodeRelationship
CALLER (0x33)Returns msg.sender. In delegatecall, CALLER is the original external caller while ADDRESS is the proxy. Together they define the execution identity
ORIGIN (0x32)Returns tx.origin (the EOA that initiated the transaction). Unlike ADDRESS, ORIGIN is constant across all call depths
DELEGATECALL (0xF4)The opcode that creates the context where ADDRESS returns a different contract’s address. The primary source of ADDRESS-related threats
CREATE (0xF0)Creates a new contract whose address is derived from address(this) + nonce. ADDRESS determines the deployer component of the child address
CREATE2 (0xF5)Creates a contract at a deterministic address derived from address(this) + salt + init code hash. Enables address prediction attacks
EXTCODESIZE (0x3B)Used to check if an address has code deployed. Returns 0 during construction and for EOAs — relevant to CREATE2 prediction attacks
CALLCODE (0xF2)Deprecated predecessor to DELEGATECALL. ADDRESS behaves similarly (returns caller’s address), but msg.sender differs
SELFDESTRUCT (0xFF)Post-EIP-6780, only effective in the creation transaction. Previously enabled the Parity library destruction via address(this) takeover