All articles
Vaults  ·  Security

The ERC-4626 rounding bug that keeps coming back

Xora Labs Security  ·  8 min read

A vault with no bugs in its logic can still round a depositor's shares to zero. The standard does not stop you, and the fix is not obvious.

The mechanism

An ERC-4626 vault issues shares in proportion to assets held. The calculation is the obvious one:

shares = assets * totalSupply / totalAssets

Integer division rounds down. When totalSupply is very small and totalAssets is very large, that rounding stops being a rounding error and becomes the entire deposit.

The attack, in order

  • The vault is deployed empty. The attacker deposits one wei and receives one share. They now own the entire supply.
  • The attacker transfers a large amount of the underlying asset directly to the vault address. No deposit call, just a plain transfer. totalAssets is now large; totalSupply is still one.
  • A genuine depositor arrives and deposits an amount smaller than what was donated. Their share calculation rounds to zero. They receive nothing.
  • The attacker redeems their single share and takes everything, including the victim's deposit.

Nothing here exploits a coding error. Every line behaves as written. The flaw is that the accounting reads a balance the vault does not control.

Why the obvious fixes are not enough

Tracking deposits internally instead of reading the balance stops the donation, but breaks any strategy that receives yield as a direct transfer, which is most of them.

Requiring a minimum deposit raises the attacker's cost without changing the mechanism. It is a speed bump on a bug that scales.

Seeding the vault at deployment works, and depends entirely on the deployment script running correctly, once, in the right order. It is a process control standing in for a code control.

What does work

Virtual shares and virtual assets, as used in OpenZeppelin's implementation, add a fixed offset to both sides of the ratio. The vault behaves as though it always holds a small phantom balance. That makes the first depositor's share price sane regardless of donations, and the attacker's cost to force a rounding error to zero grows by the offset. The decimals offset is a parameter, and it is worth understanding rather than copying: it trades precision against attack cost.

Rounding direction, everywhere

The inflation attack is the famous case, but the general principle matters more. Every conversion in a vault should round in the protocol's favour:

  • Minting shares for a deposit: round down, so the depositor never receives more than they paid for.
  • Burning shares for a withdrawal: round up, so the withdrawer never takes more than their share.
  • previewDeposit and previewRedeem must round the same direction as the functions they predict, or integrators will build on a lie.

Rounding the wrong way by one wei is not a rounding error when the function can be called in a loop. It is a slow drain with a clean transaction history.

What to check in review

Deploy the vault empty and try the four steps above. Then check totalAssets: if it reads balanceOf(address(this)), anyone can change your accounting with a transfer, and you need to have decided deliberately whether that is acceptable.

Working on something in this area? Tell us about it.