pETH Pair Drain via Synthetic balanceOf Inflation
Exploit Transactions
0x4ab68b21799828a57ea99c1288036889b39bf85785240576e697ebff524b3930Victim Addresses
0x62abdd605e710cc80a52062a8cc7c5d659dddbe7Ethereum0x2033b54b6789a963a02bfcbd40a46816770f1161EthereumLoss Breakdown
Similar Incidents
TomInu Reflection Pair Inflation Flashloan Exploit
39%QUATERNION Pair-Rebase Accounting Drift Enables Permissionless Drain
33%VINU Reserve Drain
33%NOON Pool Drain via Public transfer
33%Curve Vyper Lock Reentrancy
33%MahaLend Liquidity Index Inflation
32%Root Cause Analysis
pETH Pair Drain via Synthetic balanceOf Inflation
1. Incident Overview TL;DR
In Ethereum transaction 0x4ab68b21799828a57ea99c1288036889b39bf85785240576e697ebff524b3930 at block 18305132, an unprivileged adversary flash-loaned 51970.861731879316502999 WETH from Balancer Vault 0xBA12222222228d8Ba445958a75a0704d566BF2C8, bought pETH from the pETH/WETH Uniswap V2 pair 0x2033B54B6789a963A02BfCbd40A46816770f1161, and then used repeated permissionless skim(address) calls to make token 0x62aBdd605E710Cc80a52062a8cC7c5d659dDDbE7 report a synthetic pair balance that exceeded its own total supply. The attacker then sold the remaining pETH back into the pair, repaid the flash loan, and forwarded 1.437545289007891970 WETH profit to 0x3eb469163892ac241661dc5f3c5114be9a72cb21.
The root cause is an accounting flaw in the unverified pETH token, not in Uniswap V2 or Balancer. pETH keeps a conserved base balance at keccak256(address,1) but also keeps a separate pair-specific bonus bucket at keccak256(address,3), and balanceOf(address) reports the sum of both values. Repeated skim(pair) self-transfers let any caller grow that bonus bucket by 10e18 per call until a 100e18 cap is reached, so Uniswap V2 consumes fake token input and releases real WETH.
2. Key Background
Uniswap V2 pairs compute amountIn from IERC20(token).balanceOf(address(this)) inside swap(uint256,uint256,address,bytes). That design assumes the token's balanceOf is conservative, supply-consistent, and reflects real spendable inventory. If a token reports a larger balance than the pair truly owns, the pair will mis-price swaps and release real reserves against synthetic input.
pETH is unverified, so the incident must be reconstructed from public traces, selector recovery, storage deltas, and live historical RPC queries. The victim bytecode analysis shows that balanceOf(address) is backed by both a base mapping and an auxiliary pair bonus mapping, while selector 0x7c3a00fd returns 10 and selector 0xee10640d returns the 100 ether cap used by the exploit path.
Balancer flash loans are permissionless. That matters because the adversary does not rely on any privileged funding source, secret, or private orderflow. Every call used in the transaction is a public interface reachable by an arbitrary contract cluster operating under the ACT adversary model.
3. Vulnerability Analysis & Root Cause Summary
This incident is an ATTACK case caused by non-conservative token accounting in pETH. The token's real spendable balances live in keccak256(address,1), but balanceOf(address) also includes a second address-indexed value at keccak256(address,3). During normal transfers, only the base mapping moves. During the special self-transfer path reached when the pair executes skim(pair), the token increments the bonus bucket by 10e18, records the current block at keccak256(pair,3)+1, and leaves totalSupply() unchanged. After ten successful skim(pair) calls, the pair reports 101.537965509184617860 pETH even though total supply remains only 3.412000000000000001 pETH. Uniswap V2 then interprets that synthetic delta as real input and pays out almost all of the pair's WETH reserve.
Victim bytecode reconstruction:
balanceOf(account):
return baseBalance[account] + bonusBalance[account]
transfer(to, amount):
if msg.sender == pair and to == pair:
bonusBalance[pair] += cap / stepCount // +10e18 each skim(pair)
bonusBlock[pair] = block.number
emit CustomPairEvent(pair, 1e18)
return true
baseBalance[msg.sender] -= amount
baseBalance[to] += amount
emit Transfer(msg.sender, to, amount)
4. Detailed Root Cause Analysis
At the start of block 18305132, the pair's real pETH base balance at keccak256(pair,1) was 1537965509184617860, the auxiliary bonus bucket at keccak256(pair,3) was 0, and balanceOf(pair) therefore matched the real balance at 1.537965509184617860 pETH. Historical RPC queries also show totalSupply() was 3412000000000000001 and remained unchanged across the exploit.
The attacker first borrowed 51970.861731879316502999 WETH from Balancer and sent that WETH into the pair. The first swap(...) bought 1.537922181095525778 pETH, which moved the pair's real base balance down to 43328089092082 wei of pETH and raised the WETH side to 51972.321521432081735476 WETH. That phase is ordinary AMM behavior: only the base-balance mapping changes.
Next, the attacker transferred 90% of the purchased pETH, 1.384129962985973200 pETH, back to the pair. That restored the pair's real base balance to 1.384173291075065282 pETH but did not yet create any synthetic input. The exploit begins only when the attacker repeatedly calls skim(pair), which makes the pair attempt to transfer any apparent excess pETH back to itself.
Seed transaction trace excerpt:
0x2033...1161::skim(0x2033...1161)
0x62aB...DbE7::transfer(0x2033...1161, 91384129962985973200)
storage @ keccak256(pair,3): 0x4563918244f400000 -> 0x4e1003b28d9280000
0x2033...1161::skim(0x2033...1161)
0x62aB...DbE7::transfer(0x2033...1161, 101384129962985973200)
-> Revert: You have reached maximum pre-loaded Pseudo
0x2033...1161::swap(0, 51972299277168324394969, 0xf88D...36F8, 0x)
The same trace and the victim bytecode analysis show why those calls matter. Each successful skim(pair) self-transfer increments keccak256(pair,3) by exactly 10e18, derived from 0xee10640d() / 0x7c3a00fd() = 100e18 / 10. After ten successful iterations, keccak256(pair,3) reaches 100e18 and keccak256(pair,3)+1 stores block 18305132. An eleventh attempt fails at the cap, which confirms the growth rule is bounded but still attacker-controlled.
After the bonus bucket is full, the attacker transfers the remaining 10% pETH, 0.153792218109552578, back to the pair. That returns the real base-balance slot to its original pre-attack value of 1.537965509184617860, but the bonus slot stays at 100e18. At that moment:
slot1(pair) = 1.537965509184617860 pETH
slot3(pair) = 100.000000000000000000 pETH
balanceOf(pair) = 101.537965509184617860 pETH
totalSupply() = 3.412000000000000001 pETH
Uniswap V2 then evaluates amount0In = balance0 - reserve0 during the final swap(...). Because balanceOf(pair) is synthetic, the pair sees 100.153792218109552578 pETH of apparent input and pays out 51972.299277168324394969 WETH. The pair is left with only 0.022244263757340507 WETH and a distorted pETH reserve that reflects attacker-inflated accounting rather than conserved token inventory.
The explicit invariant violation is: for an ERC20 paired in an AMM, balanceOf(pair) must remain equal to the pair's conserved spendable balance and remain consistent with totalSupply(). pETH breaks that invariant by exposing slot1(pair) + slot3(pair) as spendable balance even though slot3(pair) is synthetic and can be grown permissionlessly via skim(pair).
5. Adversary Flow Analysis
The seed EOA 0xea75aec151f968b8de3789ca201a2a3a7faeefba sends the exploit transaction to attacker contract 0xf88d1d6d9db9a39dbbfc4b101cecc495bb0636f8. Stakeholder bytecode analysis shows that this outer contract is the execution wrapper. It immediately calls helper contract 0xc71e35ecdca07553b936a4716b8a5c1c3ac6ccb6, whose recovered interface includes receiveFlashLoan(address[],uint256[],uint256[],bytes). The helper requests the Balancer flash loan and hands borrowed WETH back into the outer exploit wrapper.
The outer contract then performs the exploit in three stages:
- Funding and buy phase: flash-loan WETH is delivered to the pair and swapped into pETH.
- Synthetic balance inflation:
90%of the bought pETH is transferred back to the pair, thenskim(pair)is called ten times to populate the pair-specific bonus bucket from0to100e18. - Exit and cleanup: the remaining
10%pETH is transferred to the pair, the final swap drains WETH, Balancer is repaid, and the leftover WETH is forwarded to0x3eb469163892ac241661dc5f3c5114be9a72cb21.
Every stage is permissionless. The helper contracts are adversary-side orchestration only; they do not introduce any privileged dependency that would break ACT feasibility.
6. Impact & Losses
The pETH/WETH Uniswap V2 pair lost 1437545289007891970 wei of WETH, or 1.437545289007891970 WETH, net to the adversary cluster in the exploit transaction. The profit recipient's WETH balance increased from 0.000018438034170325 to 1.437563727042062295, while the seed EOA paid 0.003804485827706025 ETH in gas. On a net ETH-equivalent basis, the adversary cluster realized 1.433740803180185945 ETH after fees.
The post-attack pool state is also materially corrupted. The pair ends with only 0.022244263757340507 WETH while reporting 101.537965509184617860 pETH, even though the token's total supply remains only 3.412000000000000001. That distorted reserve picture is not a harmless display bug; it is the direct accounting condition that let the attacker extract real WETH.
7. References
- Seed transaction:
0x4ab68b21799828a57ea99c1288036889b39bf85785240576e697ebff524b3930 - Victim token:
0x62aBdd605E710Cc80a52062a8cC7c5d659dDDbE7 - Victim pair:
0x2033B54B6789a963A02BfCbd40A46816770f1161 - Balancer Vault:
0xBA12222222228d8Ba445958a75a0704d566BF2C8 - Collector trace and state artifacts for the seed transaction
- Victim bytecode analysis:
/workspace/session/artifacts/auditor/iter_1/peth_bytecode_analysis.md - Stakeholder bytecode analysis:
/workspace/session/artifacts/auditor/iter_1/attacker_stakeholder_analysis.md - Auditor forked PoC execution log:
/workspace/session/artifacts/auditor/forge-test.log