Unknown Escrow Contract Drain via Integer Overflow in Deposit Function (Ethereum, 2026-03-22)
An unverified escrow-like contract at 0xf0a105d93eec8781e15222ad754fcf1264568c97 on Ethereum Mainnet was fully drained in block 24,707,679 (timestamp 2026-03-22 UTC) through an integer overflow in its deposit function 0x317de4f6. The deposit function accumulates entry amounts into a running total using unchecked arithmetic before passing that total to transferFrom. By supplying two entries whose amounts sum to 1 mod 2^256, the attacker caused only 1 raw USDT unit to be pulled from them via transferFrom, while the per-entry amounts stored in the claim mapping remained uninflated — giving the attacker a stored claimable balance of 97,812,920,709 raw USDT (the full pool) at a cost of 1 raw unit. The attacker then called claim(), which faithfully transferred the stored amount, draining 97,812.92 USDT. The stolen USDT was swapped to approximately 45.34 ETH ($97,500 USD) via the Uniswap V3 USDT/WETH pool, and sent to the attacker EOA 0x7bd736631afbe1d3795a94f60574f7fa0ae89347 through two self-destructing disposable contracts.
Root Cause
Vulnerable Contract
- Name: Unknown (no verified source; version string
0.2.15embedded in bytecode) - Address:
0xf0a105d93eec8781e15222ad754fcf1264568c97 - Proxy: No
- Source type: Recovered [approximation] — runtime bytecode decompiled; confidence
medium
Vulnerable Function
- Function name:
0x317de4f6(deposit) - Signature:
0x317de4f6(address token, (address beneficiary, uint256 amount)[] entries) - Selector:
0x317de4f6 - File:
recovered.sol(decompiled approximation; not verified source)
Vulnerable Code
// [recovered — approximation, confirmed against decompiled source]
// Contract: 0xf0a105d93eec8781e15222ad754fcf1264568c97
// Confidence: high
// Storage layout (inferred):
// slot 0: address _owner; bool _paused
// slot 1: mapping(address => struct{address token; uint256 amount}[]) _claim
function deposit(address token, Entry[] calldata entries) external {
require(!_paused);
// Accumulate total to pull via transferFrom — NO OVERFLOW CHECK
uint256 total = 0;
for (uint256 i = 0; i < entries.length; i++) {
total += entries[i].amount; // <-- VULNERABILITY: unchecked addition
}
require(total > 0, "BADINPUT");
// Pull `total` from caller — wraps silently if overflow occurred
token.transferFrom(msg.sender, address(this), total); // <-- only 1 unit pulled when total wraps to 1
// Store each entry's full (uninflated) amount in the claim mapping
for (uint256 i = 0; i < entries.length; i++) {
if (entries[i].beneficiary != address(0) && entries[i].amount > 0) {
_storeClaimEntry(entries[i].amount, token, entries[i].beneficiary);
// _storeClaimEntry has SafeMath — but per-entry amounts are never overflowed
}
}
}
Note on claim(): claim() is NOT vulnerable. It correctly reads each entry’s stored amount from _claim[msg.sender] and calls token.transfer(msg.sender, amount). It transfers the stored value, not balanceOf(this). The function behaves as designed — the inflated stored amount is the result of the deposit overflow, not a flaw in claim itself.
Why It’s Vulnerable
Expected behavior: total accumulates the sum of all entry amounts and transferFrom pulls exactly that many tokens from the caller. Each entry’s amount is then stored in the claim mapping. A user who deposits 500 USDT can later claim 500 USDT.
Actual behavior: The accumulation of total uses unchecked addition (compiled with Solidity 0.7.6, where overflow wraps silently). An attacker can supply two entries with different beneficiary addresses whose amounts sum to 1 mod 2^256:
| Entry | Beneficiary | Amount |
|---|---|---|
| 0 | garbage address | 2^256 - 97,812,920,708 (≈ 2^256) |
| 1 | AttackerExploitContract | 97,812,920,709 |
total = (2^256 - 97,812,920,708) + 97,812,920,709 = 1(mod 2^256)transferFrompulls 1 raw unit from the attacker_storeClaimEntrystores each entry independently (different beneficiaries, no cross-entry SafeMath triggered):_claim[garbageAddr] = [{USDT, 2^256 - 97,812,920,708}](uncollectable — no key)_claim[attackerContract] = [{USDT, 97,812,920,709}]← full pool balance stored
- Attacker calls
claim()→ receives97,812,920,709raw USDT
Why the beneficiary split is necessary: _storeClaimEntry (0xb38) accumulates amounts for duplicate (beneficiary, token) pairs using SafeMath. If both entries shared the same beneficiary, the stored total would also overflow to 1, yielding no profit. Using distinct beneficiaries keeps the entries in separate mapping slots, bypassing the SafeMath guard in storage.
Missing fix: The accumulation loop must use SafeMath (or Solidity ≥ 0.8) to revert on overflow:
require(total + entries[i].amount >= total, "SafeMath: addition overflow");
total += entries[i].amount;
Attack Execution
High-Level Flow
- Attacker EOA (
0x7bd736631afbe1d3795a94f60574f7fa0ae89347) deploys AttackerFactory via a contract-creation transaction. The factory’s constructor contains the entire exploit logic. - AttackerFactory deploys AttackerExploitContract (a disposable contract containing the exploit steps).
- AttackerFactory calls
func_0xb79113c6()on AttackerExploitContract to trigger the exploit. - AttackerExploitContract calls
unlock()on the Uniswap v4 PoolManager (0x000000000004444c5dc75cb358380d2e3de08a90), entering a callback context. - Inside the
unlockCallbackcallback: the PoolManager’stake()function transfers 1 raw USDT unit to AttackerExploitContract, with the outstanding debt settled later viasettle(). - AttackerExploitContract approves the victim contract for unlimited USDT spending, then calls
func_0x317de4f6()with two crafted entries whose amounts sum to1 mod 2^256: Entry[0]{beneficiary=garbageAddr, amount=2^256−97,812,920,708}and Entry[1]{beneficiary=AttackerExploitContract, amount=97,812,920,709}. The overflowed total causestransferFromto pull only 1 raw USDT unit, while_claim[AttackerExploitContract]is stored with97,812,920,709as the claimable amount. - AttackerExploitContract calls
claim()on the victim contract.claim()reads the stored amount97,812,920,709from_claim[msg.sender]and transfers it — draining ~97,812.92 USDT to AttackerExploitContract. - AttackerExploitContract repays the 1-unit USDT flash-borrow to the PoolManager via
settle(), retaining ~97,812.92 USDT net. - AttackerExploitContract approves the DEX router and swaps all USDT to WETH (via Uniswap V3 USDT/WETH 0.3% pool), receiving ~45.34 WETH.
- AttackerExploitContract calls
WETH.withdraw()to unwrap WETH to ETH, then SELFDESTRUCTs — forwarding all ETH to AttackerFactory, which in turn SELFDESTRUCTs to the attacker EOA.
Detailed Call Trace
All calls derived from trace_callTracer.json and decoded_calls.json.
- [depth 0] EOA
0x7bd7...→ CREATE0x328a0b37f5b26cc8101b2eed064bb93866762dee(AttackerFactory)- [depth 1] AttackerFactory constructor → CREATE
0x0ed84fce07933675974eb9406f8af8b708301902(AttackerExploitContract) - [depth 1] AttackerFactory → AttackerExploitContract: CALL
0xb79113c6(trigger)- [depth 2] AttackerExploitContract → Uniswap v4 PoolManager (
0x000000000004444c5dc75cb358380d2e3de08a90): CALL0x48c89491unlock(bytes)with empty data- [depth 3] PoolManager → AttackerExploitContract: CALL
0x91dd7346unlockCallback(bytes)(callback)- [depth 4] AttackerExploitContract → PoolManager: CALL
0x0b0d9c09take(address,address,uint256)— args:(USDT, AttackerExploitContract, 1)- [depth 5] PoolManager → USDT: CALL
0xa9059cbbtransfer(address,uint256)— transfers 1 raw unit USDT to AttackerExploitContract
- [depth 5] PoolManager → USDT: CALL
- [depth 4] AttackerExploitContract → USDT: STATICCALL
0x70a08231balanceOf(address)— queries victim contract USDT balance (returns ~97,812.92e6) - [depth 4] AttackerExploitContract → USDT: CALL
0x095ea7b3approve(address,uint256)— approves VulnerableTarget (0xf0a105...) foruint256.max - [depth 4] AttackerExploitContract → VulnerableTarget (
0xf0a105...): CALL0x317de4f6(deposit) — two-entry overflow calldata: Entry[0]{garbageAddr, 2^256−97,812,920,708}+ Entry[1]{AttackerExploitContract, 97,812,920,709}; overflowed total = 1- [depth 5] VulnerableTarget → USDT: CALL
0x23b872ddtransferFrom(address,address,uint256)— pulls 1 raw unit USDT from AttackerExploitContract (overflowed total); stores97,812,920,709in_claim[AttackerExploitContract]
- [depth 5] VulnerableTarget → USDT: CALL
- [depth 4] AttackerExploitContract → VulnerableTarget: CALL
0x4e71d92dclaim()— triggers the vulnerable drain- [depth 5] VulnerableTarget → USDT: CALL
0xa9059cbbtransfer(address,uint256)— transfers 97,812,920,709 raw units (~97,812.92 USDT) to AttackerExploitContract
- [depth 5] VulnerableTarget → USDT: CALL
- [depth 4] AttackerExploitContract → PoolManager: CALL
0xa5841194sync(address)— syncs PoolManager’s internal USDT accounting (arg: USDT)- [depth 5] PoolManager → USDT: STATICCALL
0x70a08231balanceOf(address)— reads PoolManager’s own USDT balance
- [depth 5] PoolManager → USDT: STATICCALL
- [depth 4] AttackerExploitContract → USDT: CALL
0xa9059cbbtransfer(address,uint256)— repays 1 raw unit USDT to PoolManager (flash repayment) - [depth 4] AttackerExploitContract → PoolManager: CALL
0x11da60b4settle()— finalizes PoolManager debt accounting- [depth 5] PoolManager → USDT: STATICCALL
0x70a08231balanceOf(address)— confirms PoolManager balance restored
- [depth 5] PoolManager → USDT: STATICCALL
- [depth 4] AttackerExploitContract → USDT: CALL
0x095ea7b3approve(address,uint256)— approves DEXRouter (0x00000000008892d0...) foruint256.max - [depth 4] AttackerExploitContract → USDT: STATICCALL
0x70a08231balanceOf(address)— reads own USDT balance (~97,812.92e6) - [depth 4] AttackerExploitContract → DEXRouter (
0x00000000008892d0...): CALL0xafeae12bswapV3(...)— swaps ~97,812.92 USDT → ~45.34 WETH via UniV3 0.3% pool- [depth 5] DEXRouter → UniV3 USDT/WETH Pool (
0x4e68ccd3...): CALL0x128acb08swap(address,bool,int256,uint160,bytes)- [depth 6] Pool → WETH: CALL
0xa9059cbbtransfer(address,uint256)— transfers 45,341,889,877,898,810,010 wei WETH to AttackerExploitContract - [depth 6] Pool → DEXRouter: CALL
0xfa461e33uniswapV3SwapCallback(int256,int256,bytes)— requests USDT payment- [depth 7] DEXRouter → USDT: CALL
0x23b872ddtransferFrom(address,address,uint256)— pulls 97,812,920,708 raw USDT from AttackerExploitContract to pool
- [depth 7] DEXRouter → USDT: CALL
- [depth 6] Pool → WETH: CALL
- [depth 5] DEXRouter → UniV3 USDT/WETH Pool (
- [depth 4] AttackerExploitContract → WETH: STATICCALL
0x70a08231balanceOf(address)— reads WETH balance - [depth 4] AttackerExploitContract → WETH: CALL
0x2e1a7d4dwithdraw(uint256)— unwraps 45,341,889,877,898,810,010 wei WETH to ETH- [depth 5] WETH → AttackerExploitContract: CALL (value transfer, 45.34 ETH)
- [depth 4] AttackerExploitContract → PoolManager: CALL
- [depth 3] PoolManager → AttackerExploitContract: CALL
- [depth 2] AttackerExploitContract → Uniswap v4 PoolManager (
- [depth 2] AttackerExploitContract → AttackerFactory: SELFDESTRUCT (forwards 45.34 ETH)
- [depth 1] AttackerFactory → EOA (
0x7bd7...): SELFDESTRUCT (forwards 45.34 ETH)
- [depth 1] AttackerFactory constructor → CREATE
Financial Impact
Primary source: funds_flow.json (attacker_gains, net_changes).
| Asset | Amount | Direction |
|---|---|---|
USDT drained from victim (0xf0a105...) | 97,812.920709 USDT (raw: 97,812,920,709; log index 3) | VulnerableTarget → AttackerExploitContract |
| USDT used as initial deposit | 0.000001 USDT (1 raw unit) | PoolManager → AttackerExploitContract → VulnerableTarget |
| USDT repaid to PoolManager (flash repayment) | 0.000001 USDT (1 raw unit) | AttackerExploitContract → PoolManager |
| WETH received from UniV3 swap | 45.341889877898810010 WETH | UniV3 Pool → AttackerExploitContract |
| ETH received by attacker EOA | 45.341889877898810010 ETH | AttackerExploitContract → Factory → EOA |
Net attacker gain: ~45.34 ETH (approximately $97,500 USD at ~$2,150/ETH at time of attack).
Victim loss: The VulnerableTarget contract (0xf0a105...) lost its entire USDT balance. The claim() transfer was 97,812.920709 USDT (raw: 97,812,920,709; log index 3). The victim’s net USDT change is −97,812.920708 USDT (raw: −97,812,920,708; confirmed by funds_flow.json net_changes). The 1-raw-unit difference reflects that 1 unit was first deposited into the victim before claim() drained 97,812,920,709 units; the victim’s pre-attack balance was 97,812,920,708 units (~$97,812.92 at peg).
Flash-borrow cost: 1 raw USDT unit (negligible; no fee charged for the PoolManager unlock/take/settle pattern).
Protocol solvency: The vulnerable contract was completely drained. Its USDT balance is zero after this transaction. The attacker’s self-destructing contract design leaves no on-chain evidence of the exploit contract code post-execution.
Evidence
| Item | Value |
|---|---|
| Transaction status | 0x1 (success) — confirmed by receipt.json |
| Block number | 24,707,679 (0x179025f) |
| Block timestamp | 0x69bee377 = 2026-03-22 UTC |
| TX index in block | 0x0 (first transaction in block) |
Selector 0x317de4f6 (deposit) | Unresolved from 4byte; identified via trace and decompilation |
Selector 0x4e71d92d (claim) | Resolved: claim() via 4byte database |
| USDT Transfer (claim payout) log index 3 | From 0xf0a105... to 0x0ed84fce..., raw amount 97812920709 = 97,812.920709 USDT |
| Deposit Transfer log index 2 | From 0x0ed84fce... to 0xf0a105..., raw amount 1 (1 raw unit USDT) |
| WETH Transfer log index 6 | From UniV3 Pool to 0x0ed84fce..., raw amount 45341889877898810010 wei |
| Victim USDT net change | -97812920708 raw units = -97,812.920708 USDT (from funds_flow.json) |
| Attacker ETH gain | 45341889877898810010 wei = 45.34188987789881001 ETH (from funds_flow.json attacker_gains) |
| Uniswap v4 PoolManager address | 0x000000000004444c5dc75cb358380d2e3de08a90 (Etherscan returns ContractName: PoolManager; not traditional Permit2) |
| Approve log (exploit → victim) log index 1 | 0x0ed84fce... approves 0xf0a105... for uint256.max USDT |
| Approve log (exploit → DEXRouter) log index 5 | 0x0ed84fce... approves 0x00000000008892d0... for uint256.max USDT |
Related URLs
- Transaction: https://etherscan.io/tx/0x73bd1384e7b628a29542239be4bc96af0871f7aa22d410c0b38d62367630b053
- Victim contract: https://etherscan.io/address/0xf0a105d93eec8781e15222ad754fcf1264568c97
- Attacker EOA: https://etherscan.io/address/0x7bd736631afbe1d3795a94f60574f7fa0ae89347
- Uniswap v4 PoolManager: https://etherscan.io/address/0x000000000004444c5dc75cb358380d2e3de08a90