Access-control bug draining 5 ETH from token contract
Exploit Transactions
0xbeef352f716973043236f73dd5104b9d905fd04b7fc58d9958ac5462e7e3dbc1Victim Addresses
0xdb27d4ff4be1cd04c34a7cb6f47402c37cb73459EthereumLoss Breakdown
Similar Incidents
FlippazOne ungated ownerWithdrawAllTo lets attacker drain 1.15 ETH
38%AnyswapV4Router WETH9 permit misuse drains WETH to ETH
35%OMPxContract bonding-curve loop exploit drains ETH reserves
34%SilicaPools decimal-manipulation bug drains WBTC flashloan collateral
34%DeRace vesting proxy ownership takeover and emergency exit
33%TecraCoin TcrToken burnFrom Allowance Bug Exploits
33%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 enforcesmsg.sender == tx.originand 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_hin0xdb27...points to0x95d4dc882738af8760cb48af9fd8e350ff604d6d.
The intended safety invariant can be expressed as:
- Only a trusted admin address is allowed to (a) change
store_hand (b) triggerrescueEth(); 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_000000000000000000wei →0(delta-5 ETH),0xd215...balance:42.074140197177650124 ETH→47.066568236521447555 ETH(delta+4.992428039343797431 ETH),0xd129...balance:29116 wei→107297 wei(delta+78181 wei),0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97balance:+7305300000000000 wei(miner/fee recipient).
The breakpoint is the pair of calls to the victim contract:
setMarketingWallet(0xd129...): overwritesstore_hfrom0x95d4dc...to0xd129....rescueEth(): checksmsg.sender == store_h(now0xd129...) and transfers the entire 5 ETH balance of0xdb27...to0xd129....
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.datato construct one or more external calls, includingaddress(...).transfer(...)operations to route ETH.
In the observed transaction, the helper:
- Receives the initial call from EOA
0xd215...with 107297 wei and selector-prefixed data. - Calls
0xdb27...with selector0x5d098b38and argument0xd129...(setMarketingWallet). - Calls
0xdb27...again with selector0xce31a06b(rescueEth), which causes0xdb27...to send exactly0x4563918244f40000(5 ETH) to0xd129.... - Forwards
0x4563918244f471bcwei (5 ETH minus 78181 wei) totx.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_hpointing to0x95d4dc.... - Strategy: Any unprivileged EOA can (i) deploy or use a helper contract it controls (optionally with tx.origin gating), (ii) call
setMarketingWallet(helperAddress)on0xdb27..., and then (iii) callrescueEth()fromhelperAddress, 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.992428039343797431after paying0.007571960656124388ETH in gas, as computed frombalance_diff.jsonand 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.992428039343797431ETH.
- Contract 0xd129d8c12f0e7aa51157d9e6cc3f7ece2dc84ecd
- Chain: Ethereum mainnet (chainid 1).
- Role: Helper/orchestrator contract that enforces
msg.sender == tx.originin its fallback, receives 5 ETH from the victim viarescueEth(), 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:
- Trigger helper: EOA
0xd215...calls helper0xd129...with carefully crafted calldata. - Reassign marketing wallet: Helper calls
0xdb27...with selector0x5d098b38and argument0xd129..., executingsetMarketingWallet(0xd129...). Storage diffs showstore_hchanging from0x95d4dc...to0xd129..., and the receipt logs an event with both indexed topics equal to0xd129.... - Invoke rescueEth: Helper calls
0xdb27...with selector0xce31a06b, executingrescueEth(). Becausemsg.senderis now0xd129...andstore_h == 0xd129..., the authorization check passes and the contract transfers its entire ETH balance (5 ETH) to0xd129.... - Forward ETH to EOA: Helper’s logic immediately sends
5000000000000029116wei back to0xd215..., keeping78181wei in the helper contract as a residual.
The effect of this sequence is:
0xdb27...: balance 5 ETH → 0 ETH.0xd215...: net gain+4.992428039343797431ETH 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_deltasand the stateDiff intx_trace_replay.json(balance from0x4563918244f40000wei to0x0). - Adversary profit: EOA
0xd215ffaf0f85fb6f93f11e49bd6175ad58af0dfdgains4.992428039343797431ETH net after paying approximately0.007571960656124388ETH 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.solartifacts/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):
0xbeef352f716973043236f73dd5104b9d905fd04b7fc58d9958ac5462e7e3dbc1in block20729549(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
- Seed balance diff:
- 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).