Reproduced Exploit

PoolTogether: a 1-wei deposit plus an aToken donation gives the next depositor 0 shares

Apr 2022•Other•arithmetic•2 min read

Chain

Other

Category

arithmetic

Date

Apr 2022

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 24991-h-01-a-malicious-early-userattacker-can-manipulate-the-vault. Standalone Foundry PoC and full write-up: 24991-h-01-a-malicious-early-userattacker-can-manipulate-the-vault_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/arithmetic/precision-loss

Reproduction: local synthetic, no fork. _tokenToShares keeps the audited formula. The second depositor's 9,999 underlying mints 0 shares and the attacker keeps it. Trace: output.txt.

Key info#

ImpactHIGH — the second depositor receives 0 shares and loses 9,999 underlying
ProtocolPoolTogether — AaveV3YieldSource._tokenToShares
Vulnerable codetokens * totalSupply / aToken.balanceOf(this) with no virtual offset
Bug classFirst-depositor inflation
FindingCode4rena 2022-04 PoolTogether · #24991 · WatchPug, unforgiven
Reportcode4rena.com/reports/2022-04-pooltogether

Root cause#

When totalSupply is not zero, shares are:

SOLIDITY
return _tokens.mul(_supply).div(aToken.balanceOf(address(this)));

There is no virtual offset. The first depositor supplies 1 wei and receives 1 share. They then transfer 10,000 aTokens directly to the yield source. That donation increases aToken.balanceOf(vault) without minting shares, so one share now claims about 10,000 underlying.

The next depositor supplies 9,999 underlying:

TEXT
shares = 9_999e18 * 1 / (10_000e18 + 1) = 0

The vault still pulls the underlying and supplies it to Aave. The depositor's share balance stays 0. The attacker redeems the only share, receives the whole aToken balance, and keeps the 9,999.

Attack path#

flowchart TD A["Attacker deposits 1 wei and receives 1 share"] --> B["Attacker donates 10,000 aTokens to the vault"] B --> C["Second depositor sends 9,999 underlying"] C --> D["Share formula divides by the inflated aToken balance"] D --> E["9,999e18 times 1 share over 10,000e18 rounds to 0"] E --> F["Vault still pulls the 9,999 and mints nothing"] F --> G["Attacker redeems the only share and keeps the 9,999"]

PoC#

BASH
cd 24991-h-01-a-malicious-early-userattacker-can-manipulate-the-vault_exp
forge test -vv

The exploit asserts victimShares == 0 and the attacker balance is 9_999 ether. The control vault adds a virtual offset of 1_000_000 on the same formula. The same second deposit then mints a non-zero share balance (greater than 100,000).

Remediation#

Mint shares against a virtual offset so a 1-wei donation cannot dominate the rate, and reject a zero share mint:

SOLIDITY
return _tokens.mul(_supply + OFFSET).div(aToken.balanceOf(address(this)) + OFFSET);

PoolTogether fixed this in pooltogether/aave-v3-yield-source#15.

References#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.