ERC-4626 is the standard interface for yield-bearing vaults: you deposit an asset and receive shares that grow in value. This implementation pays particular attention to two things custom vaults often get wrong, rounding direction and the inflation attack.
Every conversion rounds in the vault's favor:
| function | rounds | why |
|---|---|---|
deposit / previewDeposit |
shares down | a depositor can't get more shares than they paid for |
mint / previewMint |
assets up | a minter pays at least the fair price |
withdraw / previewWithdraw |
shares up | a withdrawal burns enough shares |
redeem / previewRedeem |
assets down | a redeemer can't pull out extra dust |
In an empty vault, an attacker can deposit 1 wei and then donate a large amount directly to the vault,
so that the next depositor's shares round down to almost nothing. The conversions here use a virtual
offset like OpenZeppelin's _decimalsOffset: totalSupply + 10^3 virtual shares and
totalAssets + 1. Most of the donation then goes to the virtual shares, the attacker loses money and
the victim loses only a rounding error. The tests run the attack. The attacker donates 10,000 tokens
and gets about 5,000 back, and the victim who deposited 20,000 withdraws more than 19,990.
An offset of 1 is not enough. The attacker still loses money, but in the same scenario the victim loses about 10%. That's why shares have 3 extra decimals (21 for an 18-decimal asset).
In the tests, yield is simulated by transferring assets to the vault, which raises the value of every share.
forge build
forge test -vv