NM-SAMPLE-01 · Share calculation rounds in the caller’s favour on withdraw
- SeverityMedium (illustrative)
- ImpactMedium: small, repeatable loss to remaining depositors
- LikelihoodMedium: anyone can trigger it; profitable only where gas is cheap
- Location
ExampleVault.sol,withdraw()(fictional) - StatusFixed in
1111111(placeholder)
Description
In withdraw(assets), the vault computes the shares to burn with rounding down (it should round up, per ERC-4626 previewWithdraw). The caller burns slightly fewer shares than the assets they receive are worth, so each call moves a small amount of value from other depositors to the caller.
Severity reasoning
Impact: each call leaks at most one unit of rounding, so total loss is bounded by call count and gas cost, which keeps impact at Medium. Likelihood: anyone can trigger it with no preconditions, but it pays only where gas is cheap relative to the unit, so Medium.
Proof of concept (Foundry, fictional)
function test_withdrawRoundsInCallerFavour() public {
vault.deposit(1_000e18, bob);
vault.deposit(1_000e18, alice);
asset.transfer(address(vault), 1e18); // simulated yield: share price above 1, so withdraw(1) burns 0 shares
uint256 before = vault.convertToAssets(vault.balanceOf(bob));
for (uint256 i; i < 100; ++i) vault.withdraw(1, alice, alice);
assertLt(vault.convertToAssets(vault.balanceOf(bob)), before);
}
Recommendation
Round up when converting assets to shares on withdraw (round against the caller), and add an invariant that total assets cover total shares.
Fix review
Verified at the fix commit: rounding now favours the vault and the new invariant holds under stateful fuzzing.