All incidents

Access-control bug draining 5 ETH from token contract

Share
Sep 11, 2024 19:46 UTCAttackLoss: 5 ETHManually checked1 exploit txWindow: Atomic

Root Cause Analysis

Access-control bug draining 5 ETH from token contract

1. Incident Overview TL;DR

In Ethereum mainnet block 20729549, transaction 0xbeef352f716973043236f73dd5104b9d905fd04b7fc58d9958ac5462e7e3dbc1 was sent by EOA 0xd215ffaf0f85fb6f93f11e49bd6175ad58af0dfd to helper contract 0xd129d8c12f0e7aa51157d9e6cc3f7ece2dc84ecd. The helper first called ERC20-like token contract 0xdb27d4ff4be1cd04c34a7cb6f47402c37cb73459 to change its store_h “marketing wallet” field to the helper’s own address via setMarketingWallet(address), then immediately invoked rescueEth() on the same contract. As a result, the victim contract transferred its entire 5 ETH balance to the helper, which forwarded almost all of that ETH back to the originating EOA.

The root cause is an access-control bug in the victim contract: setMarketingWallet(address) is public and unguarded, while rescueEth() authorizes full ETH withdrawal solely by checking msg.sender == store_h. Any unprivileged account can therefore set store_h to an adversary-controlled address and then call rescueEth() (directly or via a helper) to drain all ETH. This incident is an Anyone-Can-Take (ACT) opportunity because any unprivileged EOA could reproduce the same sequence using only public on-chain data and standard transaction submission.

2. Key Background

Contract 0xdb27d4ff4be1cd04c34a7cb6f47402c37cb73459 is an ERC20-like token contract on Ethereum mainnet. Heimdall decompilation and ABI artifacts show it implements standard ERC20 functions as well as several administrative functions, including setMarketingWallet(address) and rescueEth(). A dedicated storage field store_h holds the configured “marketing wallet” or authorized rescuer address.

The decompiled code for setMarketingWallet(address) shows that it is a public function which accepts an address argument, performs only basic type/non-zero checks, and then writes the provided address into store_h. It emits an event (labeled Event_7f645b8b in the decompiler output) that logs the new address. There is no owner or privileged-role check guarding this function, in contrast to other functions in the same contract that explicitly require msg.sender == owner.

The decompiled rescueEth() function checks that address(this).balance > 0, then enforces require(address(msg.sender) == address(store_h)); and finally transfers the full ETH balance of the contract to msg.sender. This means that whichever address is stored in store_h is effectively authorized to withdraw the entire ETH balance of the token contract.

At pre-state σ_B (immediately before inclusion of tx 0xbeef... in block 20729549), the seed balance diff and trace replay artifacts show that the victim contract held exactly 5 ETH and store_h pointed to address 0x95d4dc882738af8760cb48af9fd8e350ff604d6d. The adversary-related cluster consists of:

  • EOA 0xd215ffaf0f85fb6f93f11e49bd6175ad58af0dfd (transaction sender and net ETH beneficiary), and
  • helper contract 0xd129d8c12f0e7aa51157d9e6cc3f7ece2dc84ecd, whose payable fallback enforces msg.sender == tx.origin and orchestrates calls to the victim contract and back to the EOA.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is a classic access-control flaw in administrative fund-withdrawal logic. The victim contract exposes a public, unguarded setMarketingWallet(address) function that allows any caller to overwrite the store_h field with an arbitrary address. At the same time, the rescueEth() function authorizes withdrawals of the contract’s entire ETH balance solely by checking msg.sender == store_h, without any owner or role verification.

This combination breaks the intended invariant that only a trusted administrator may become the authorized rescuer and withdraw ETH. Instead, any unprivileged account can (1) set store_h to an adversary-controlled address and then (2) call rescueEth() from that address, thereby draining all ETH from the contract whenever its balance is non-zero.

In the observed transaction, the adversary uses a helper contract gated by msg.sender == tx.origin to bundle these steps: the EOA calls the helper; the helper calls setMarketingWallet on the victim to point store_h to the helper; then the helper calls rescueEth(), receives 5 ETH, and forwards nearly all of it back to the EOA. The bug is purely within the victim’s access-control design and is reproducible by any unprivileged EOA using public on-chain information, making this an ACT opportunity.

4. Detailed Root Cause Analysis

4.1 Victim contract code: setMarketingWallet and rescueEth

The relevant decompiled snippets from contract 0xdb27d4ff4be1cd04c34a7cb73459 show:

address store_h;

/// @custom:signature   setMarketingWallet(address arg0) public
function setMarketingWallet(address arg0) public {
    // Basic checks only
    store_h = (address(arg0)) | (uint96(store_h));
    // Event_7f645b8b emitted with arg0
}

/// @custom:signature   rescueEth() public
function rescueEth() public {
    // Requires contract has positive ETH balance
    require(address(msg.sender) == (address(store_h)));
    // Transfers entire ETH balance to msg.sender
    // (implementation transfers address(this).balance)
}

Other administrative functions in the same contract use owner-based checks, but setMarketingWallet does not. This directly violates the intended invariant that only an authorized admin can change the marketing wallet / authorized rescuer.

4.2 Pre-state and invariant

From the seed balance_diff.json and trace replay artifacts, the pre-state σ_B immediately before tx 0xbeef... is:

  • balance(0xdb27...) = 5.000000000000000000 ETH,
  • balance(0xd215...) = 42.074140197177650124 ETH,
  • balance(0xd129...) = 29116 wei,
  • store_h in 0xdb27... points to 0x95d4dc882738af8760cb48af9fd8e350ff604d6d.

The intended safety invariant can be expressed as:

  • Only a trusted admin address is allowed to (a) change store_h and (b) trigger rescueEth(); and
  • No unprivileged account can cause 0xdb27... to transfer its entire ETH balance to an adversary-controlled address.

4.3 Breakpoint in tx 0xbeef...

QuickNode callTracer output for tx 0xbeef... shows the following call tree:

{
  "from": "0xd215ffaf0f85fb6f93f11e49bd6175ad58af0dfd",
  "to":   "0xd129d8c12f0e7aa51157d9e6cc3f7ece2dc84ecd",
  "value": "0x1a321",
  "calls": [
    {
      "from": "0xd129d8c12f0e7aa51157d9e6cc3f7ece2dc84ecd",
      "to":   "0xdb27d4ff4be1cd04c34a7cb6f47402c37cb73459",
      "input": "0x5d098b38...d129d8c12f0e7aa51157d9e6cc3f7ece2dc84ecd",
      "type": "CALL",
      "value": "0x0"
    },
    {
      "from": "0xd129d8c12f0e7aa51157d9e6cc3f7ece2dc84ecd",
      "to":   "0xdb27d4ff4be1cd04c34a7cb6f47402c37cb73459",
      "input": "0xce31a06b",
      "type": "CALL",
      "value": "0x0",
      "calls": [
        {
          "from": "0xdb27d4ff4be1cd04c34a7cb6f47402c37cb73459",
          "to":   "0xd129d8c12f0e7aa51157d9e6cc3f7ece2dc84ecd",
          "type": "CALL",
          "value": "0x4563918244f40000"
        }
      ]
    },
    {
      "from": "0xd129d8c12f0e7aa51157d9e6cc3f7ece2dc84ecd",
      "to":   "0xd215ffaf0f85fb6f93f11e49bd6175ad58af0dfd",
      "type": "CALL",
      "value": "0x4563918244f471bc"
    }
  ]
}

The corresponding balance_diff.json confirms:

  • 0xdb27... balance: 5_000000000000000000 wei → 0 (delta -5 ETH),
  • 0xd215... balance: 42.074140197177650124 ETH47.066568236521447555 ETH (delta +4.992428039343797431 ETH),
  • 0xd129... balance: 29116 wei107297 wei (delta +78181 wei),
  • 0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97 balance: +7305300000000000 wei (miner/fee recipient).

The breakpoint is the pair of calls to the victim contract:

  1. setMarketingWallet(0xd129...): overwrites store_h from 0x95d4dc... to 0xd129....
  2. rescueEth(): checks msg.sender == store_h (now 0xd129...) and transfers the entire 5 ETH balance of 0xdb27... to 0xd129....

Because setMarketingWallet is public and unguarded, any unprivileged caller can perform step 1. Because rescueEth relies solely on store_h for authorization, step 2 is also available to any caller that can make calls from the address stored in store_h. In this incident, the adversary’s helper contract serves as that address.

4.4 Helper contract behavior

Heimdall decompilation for helper contract 0xd129d8c12f0e7aa51157d9e6cc3f7ece2dc84ecd shows a payable fallback function that:

  • Enforces require(msg.sender == tx.origin);, ensuring it is only controlled directly by an EOA.
  • Parses msg.data to construct one or more external calls, including address(...).transfer(...) operations to route ETH.

In the observed transaction, the helper:

  1. Receives the initial call from EOA 0xd215... with 107297 wei and selector-prefixed data.
  2. Calls 0xdb27... with selector 0x5d098b38 and argument 0xd129... (setMarketingWallet).
  3. Calls 0xdb27... again with selector 0xce31a06b (rescueEth), which causes 0xdb27... to send exactly 0x4563918244f40000 (5 ETH) to 0xd129....
  4. Forwards 0x4563918244f471bc wei (5 ETH minus 78181 wei) to tx.origin (0xd215...), retaining 78181 wei.

This behavior is fully consistent with the adversary using 0xd129... as an orchestrator to (a) become the authorized rescuer and (b) receive and forward the drained ETH.

4.5 ACT opportunity conditions

The ACT opportunity is characterized by:

  • Pre-state: 5 ETH held by 0xdb27..., store_h pointing to 0x95d4dc....
  • Strategy: Any unprivileged EOA can (i) deploy or use a helper contract it controls (optionally with tx.origin gating), (ii) call setMarketingWallet(helperAddress) on 0xdb27..., and then (iii) call rescueEth() from helperAddress, causing 5 ETH (or whatever ETH balance is present) to be transferred out and then back to the EOA.
  • Feasibility: Tx 0xbeef... is a standard type-2 transaction with ordinary gas parameters sent from an unprivileged EOA to a public contract, so it is realizable under normal mempool inclusion rules by any actor who observes the contract code and state.
  • Success predicate: The adversary’s net portfolio in ETH increases by 4.992428039343797431 after paying 0.007571960656124388 ETH in gas, as computed from balance_diff.json and gasUsed × effectiveGasPrice.

No private keys, privileged roles, or off-chain coordination are required, satisfying the ACT model’s “anyone-can-take” condition.

5. Adversary Flow Analysis

5.1 Adversary-related cluster

The adversary-related cluster consists of:

  • EOA 0xd215ffaf0f85fb6f93f11e49bd6175ad58af0dfd
    • Chain: Ethereum mainnet (chainid 1).
    • Role: Transaction sender for 0xbeef... and final ETH beneficiary with net gain +4.992428039343797431 ETH.
  • Contract 0xd129d8c12f0e7aa51157d9e6cc3f7ece2dc84ecd
    • Chain: Ethereum mainnet (chainid 1).
    • Role: Helper/orchestrator contract that enforces msg.sender == tx.origin in its fallback, receives 5 ETH from the victim via rescueEth(), and forwards 5 ETH minus 78181 wei to the EOA.

The primary victim is:

  • ERC20-like token contract 0xdb27d4ff4be1cd04c34a7cb6f47402c37cb73459
    • Chain: Ethereum mainnet (chainid 1).
    • Role: Contract that loses its entire 5 ETH balance due to the flawed access-control design.

5.2 Single-transaction exploit sequence (tx 0xbeef...)

The entire exploit is realized in a single transaction:

{
  "chainid": 1,
  "txhash": "0xbeef352f716973043236f73dd5104b9d905fd04b7fc58d9958ac5462e7e3dbc1",
  "block_number": 20729549,
  "from": "0xd215ffaf0f85fb6f93f11e49bd6175ad58af0dfd",
  "to": "0xd129d8c12f0e7aa51157d9e6cc3f7ece2dc84ecd",
  "type": 2
}

Within this transaction, the adversary’s logical stages are:

  1. Trigger helper: EOA 0xd215... calls helper 0xd129... with carefully crafted calldata.
  2. Reassign marketing wallet: Helper calls 0xdb27... with selector 0x5d098b38 and argument 0xd129..., executing setMarketingWallet(0xd129...). Storage diffs show store_h changing from 0x95d4dc... to 0xd129..., and the receipt logs an event with both indexed topics equal to 0xd129....
  3. Invoke rescueEth: Helper calls 0xdb27... with selector 0xce31a06b, executing rescueEth(). Because msg.sender is now 0xd129... and store_h == 0xd129..., the authorization check passes and the contract transfers its entire ETH balance (5 ETH) to 0xd129....
  4. Forward ETH to EOA: Helper’s logic immediately sends 5000000000000029116 wei back to 0xd215..., keeping 78181 wei in the helper contract as a residual.

The effect of this sequence is:

  • 0xdb27...: balance 5 ETH → 0 ETH.
  • 0xd215...: net gain +4.992428039343797431 ETH after paying gas.
  • 0xd129...: retains 78181 wei as a small remainder.

6. Impact & Losses

The measurable on-chain impact for this incident is:

  • Asset: ETH (native asset on Ethereum mainnet).
  • Victim: Contract 0xdb27d4ff4be1cd04c34a7cb6f47402c37cb73459.
  • Loss from victim contract: Exactly 5.000000000000000000 ETH, as confirmed by native_balance_deltas and the stateDiff in tx_trace_replay.json (balance from 0x4563918244f40000 wei to 0x0).
  • Adversary profit: EOA 0xd215ffaf0f85fb6f93f11e49bd6175ad58af0dfd gains 4.992428039343797431 ETH net after paying approximately 0.007571960656124388 ETH in gas fees.

No ERC20 token balance changes occur in this transaction, as erc20_balance_deltas is empty. The economic effect is a direct theft of ETH held by the token contract via misconfigured access control on its administrative functions.

7. References

  • Victim contract (ERC20-like token): 0xdb27d4ff4be1cd04c34a7cb6f47402c37cb73459 (Ethereum mainnet, chainid 1). Decompiled source and ABI:
    • artifacts/root_cause/data_collector/iter_1/contract/1/0xdb27d4ff4be1cd04c34a7cb6f47402c37cb73459/decompile/0xdb27d4ff4be1cd04c34a7cb73459-decompiled.sol
    • artifacts/root_cause/data_collector/iter_1/contract/1/0xdb27d4ff4be1cd04c34a7cb73459/decompile/0xdb27d4ff4be1cd04c34a7cb73459-abi.json
  • Helper contract (adversary orchestrator): 0xd129d8c12f0e7aa51157d9e6cc3f7ece2dc84ecd (Ethereum mainnet, chainid 1). Decompiled source:
    • artifacts/root_cause/data_collector/iter_1/contract/1/0xd129d8c12f0e7aa51157d9e6cc3f7ece2dc84ecd/decompile/0xd129d8c12f0e7aa51157d9e6cc3f7ece2dc84ecd-decompiled.sol
  • Adversary EOA: 0xd215ffaf0f85fb6f93f11e49bd6175ad58af0dfd (Ethereum mainnet, chainid 1).
  • Exploit transaction (ACT opportunity): 0xbeef352f716973043236f73dd5104b9d905fd04b7fc58d9958ac5462e7e3dbc1 in block 20729549 (Ethereum mainnet, chainid 1). Supporting artifacts:
    • Seed balance diff: artifacts/root_cause/seed/1/0xbeef352f716973043236f73dd5104b9d905fd04b7fc58d9958ac5462e7e3dbc1/balance_diff.json
    • Tx metadata and receipt: artifacts/root_cause/data_collector/iter_1/tx/1/0xbeef352f716973043236f73dd5104b9d905fd04b7fc58d9958ac5462e7e3dbc1/tx_metadata.json, tx_receipt.json
    • Call tracer and stateDiff traces: artifacts/root_cause/data_collector/iter_1/tx/1/0xbeef352f716973043236f73dd5104b9d905fd04b7fc58d9958ac5462e7e3dbc1/tx_trace_callTracer.json, tx_trace_replay.json
  • Analyzer summaries:
    • artifacts/root_cause/root_cause_analyzer/iter_2/current_analysis_result.json (consolidated root cause and ACT opportunity analysis used as the basis for this report).