# 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.15` embedded 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`:

EntryBeneficiaryAmount0garbage address1AttackerExploitContract

– `total = (2^256 – 97,812,920,708) + 97,812,920,709 = 1`(mod 2^256)
– `transferFrom` pulls **1 raw unit** from the attacker
– `_storeClaimEntry` stores 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()` → receives `97,812,920,709` raw 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

01. Attacker EOA ( `0x7bd736631afbe1d3795a94f60574f7fa0ae89347`) deploys AttackerFactory via a contract-creation transaction. The factory’s constructor contains the entire exploit logic.
02. AttackerFactory deploys AttackerExploitContract (a disposable contract containing the exploit steps).
03. AttackerFactory calls `func_0xb79113c6()` on AttackerExploitContract to trigger the exploit.
04. AttackerExploitContract calls `unlock()` on the Uniswap v4 PoolManager ( `0x000000000004444c5dc75cb358380d2e3de08a90`), entering a callback context.
05. Inside the `unlockCallback` callback: the PoolManager’s `take()` function transfers 1 raw USDT unit to AttackerExploitContract, with the outstanding debt settled later via `settle()`.
06. AttackerExploitContract approves the victim contract for unlimited USDT spending, then calls `func_0x317de4f6()` with two crafted entries whose amounts sum to `1 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 causes `transferFrom` to pull only 1 raw USDT unit, while `_claim[AttackerExploitContract]` is stored with `97,812,920,709` as the claimable amount.
07. AttackerExploitContract calls `claim()` on the victim contract. `claim()` reads the stored amount `97,812,920,709` from `_claim[msg.sender]` and transfers it — draining ~97,812.92 USDT to AttackerExploitContract.
08. AttackerExploitContract repays the 1-unit USDT flash-borrow to the PoolManager via `settle()`, retaining ~97,812.92 USDT net.
09. AttackerExploitContract approves the DEX router and swaps all USDT to WETH (via Uniswap V3 USDT/WETH 0.3% pool), receiving ~45.34 WETH.
10. 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…` → CREATE `0x328a0b37f5b26cc8101b2eed064bb93866762dee`(AttackerFactory)
– **[depth 1]** AttackerFactory constructor → CREATE `0x0ed84fce07933675974eb9406f8af8b708301902`(AttackerExploitContract)
– **[depth 1]** AttackerFactory → AttackerExploitContract: CALL `0xb79113c6`(trigger)
– **[depth 2]** AttackerExploitContract → Uniswap v4 PoolManager ( `0x000000000004444c5dc75cb358380d2e3de08a90`): CALL `0x48c89491` `unlock(bytes)` with empty data
– **[depth 3]** PoolManager → AttackerExploitContract: CALL `0x91dd7346` `unlockCallback(bytes)`(callback)
– **[depth 4]** AttackerExploitContract → PoolManager: CALL `0x0b0d9c09` `take(address,address,uint256)`— args: `(USDT, AttackerExploitContract, 1)`
– **[depth 5]** PoolManager → USDT: CALL `0xa9059cbb` `transfer(address,uint256)`— transfers 1 raw unit USDT to AttackerExploitContract
– **[depth 4]** AttackerExploitContract → USDT: STATICCALL `0x70a08231` `balanceOf(address)`— queries victim contract USDT balance (returns ~97,812.92e6)
– **[depth 4]** AttackerExploitContract → USDT: CALL `0x095ea7b3` `approve(address,uint256)`— approves VulnerableTarget ( `0xf0a105…`) for `uint256.max`
– **[depth 4]** AttackerExploitContract → VulnerableTarget ( `0xf0a105…`): CALL `0x317de4f6`(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 `0x23b872dd` `transferFrom(address,address,uint256)`— pulls **1 raw unit** USDT from AttackerExploitContract (overflowed total); stores `97,812,920,709` in `_claim[AttackerExploitContract]`
– **[depth 4]** AttackerExploitContract → VulnerableTarget: CALL `0x4e71d92d` `claim()`— triggers the vulnerable drain
– **[depth 5]** VulnerableTarget → USDT: CALL `0xa9059cbb` `transfer(address,uint256)`— transfers 97,812,920,709 raw units (~97,812.92 USDT) to AttackerExploitContract
– **[depth 4]** AttackerExploitContract → PoolManager: CALL `0xa5841194` `sync(address)`— syncs PoolManager’s internal USDT accounting (arg: USDT)
– **[depth 5]** PoolManager → USDT: STATICCALL `0x70a08231` `balanceOf(address)`— reads PoolManager’s own USDT balance
– **[depth 4]** AttackerExploitContract → USDT: CALL `0xa9059cbb` `transfer(address,uint256)`— repays 1 raw unit USDT to PoolManager (flash repayment)
– **[depth 4]** AttackerExploitContract → PoolManager: CALL `0x11da60b4` `settle()`— finalizes PoolManager debt accounting
– **[depth 5]** PoolManager → USDT: STATICCALL `0x70a08231` `balanceOf(address)`— confirms PoolManager balance restored
– **[depth 4]** AttackerExploitContract → USDT: CALL `0x095ea7b3` `approve(address,uint256)`— approves DEXRouter ( `0x00000000008892d0…`) for `uint256.max`
– **[depth 4]** AttackerExploitContract → USDT: STATICCALL `0x70a08231` `balanceOf(address)`— reads own USDT balance (~97,812.92e6)
– **[depth 4]** AttackerExploitContract → DEXRouter ( `0x00000000008892d0…`): CALL `0xafeae12b` `swapV3(…)`— swaps ~97,812.92 USDT → ~45.34 WETH via UniV3 0.3% pool
– **[depth 5]** DEXRouter → UniV3 USDT/WETH Pool ( `0x4e68ccd3…`): CALL `0x128acb08` `swap(address,bool,int256,uint160,bytes)`
– **[depth 6]** Pool → WETH: CALL `0xa9059cbb` `transfer(address,uint256)`— transfers 45,341,889,877,898,810,010 wei WETH to AttackerExploitContract
– **[depth 6]** Pool → DEXRouter: CALL `0xfa461e33` `uniswapV3SwapCallback(int256,int256,bytes)`— requests USDT payment
– **[depth 7]** DEXRouter → USDT: CALL `0x23b872dd` `transferFrom(address,address,uint256)`— pulls 97,812,920,708 raw USDT from AttackerExploitContract to pool
– **[depth 4]** AttackerExploitContract → WETH: STATICCALL `0x70a08231` `balanceOf(address)`— reads WETH balance
– **[depth 4]** AttackerExploitContract → WETH: CALL `0x2e1a7d4d` `withdraw(uint256)`— unwraps 45,341,889,877,898,810,010 wei WETH to ETH
– **[depth 5]** WETH → AttackerExploitContract: CALL (value transfer, 45.34 ETH)
– **[depth 2]** AttackerExploitContract → AttackerFactory: SELFDESTRUCT (forwards 45.34 ETH)
– **[depth 1]** AttackerFactory → EOA ( `0x7bd7…`): SELFDESTRUCT (forwards 45.34 ETH)

## Financial Impact

**Primary source**: `funds_flow.json` ( `attacker_gains`, `net_changes`).

AssetAmountDirectionUSDT drained from victim (97,812.920709 USDT (raw: 97,812,920,709; log index 3)VulnerableTarget → AttackerExploitContractUSDT used as initial deposit0.000001 USDT (1 raw unit)PoolManager → AttackerExploitContract → VulnerableTargetUSDT repaid to PoolManager (flash repayment)0.000001 USDT (1 raw unit)AttackerExploitContract → PoolManagerWETH received from UniV3 swap45.341889877898810010 WETHUniV3 Pool → AttackerExploitContractETH 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

ItemValueTransaction statusBlock number24,707,679 (Block timestampTX index in blockSelector Unresolved from 4byte; identified via trace and decompilationSelector Resolved: USDT Transfer (claim payout) log index 3From Deposit Transfer log index 2From WETH Transfer log index 6From UniV3 Pool to Victim USDT net changeAttacker ETH gainUniswap v4 PoolManager addressApprove log (exploit → victim) log index 1Approve log (exploit → DEXRouter) log index 5

## 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