Open sherlock-admin4 opened 5 months ago
1 comment(s) were left on this issue during the judging contest.
takarez commented:
this seem valid medium; medium(3)
The protocol team fixed this issue in the following PRs/commits: https://github.com/equilibria-xyz/perennial-v2/pull/304
The Lead Senior Watson signed off on the fix.
panprog
medium
Vault checkpoints slightly incorrect conversion from assets to shares leads to slow loss of funds for long-time vault depositors
Summary
When vault checkpoints convert assets to shares (specifically used to calculate user's shares for their deposit), it uses the following formula:
shares = (assets[before fee] - settlementFee) * checkpoint.shares/checkpoint.assets * (deposit + redeem - tradeFee) / (deposit + redeem)
settlementFee
in this formula is taken into account slightly incorrectly: in actual market collateral calculations, both settlement fee and trade fee are subtracted from collateral, but this formula basically multiplies1 - settlement fee percentage
by1 - trade fee percentage
, which is slightly different and adds the calculation error =settlement fee percentage * trade fee percentage
.This is the scenario to better understand the issue:
This is the calculation for User2 shares:
shares = ($100 - $1) * 100/$97 * ($100 - $2) / $100 = $99 * 100/$97 * $98/$100 = $99 * 98/$97 = 100.02
The extra 0.02 this user has received is because the
tradeFee
is taken from the amount after settlement fee ($99) rather than full amount as it should ($100). This difference (settlementFee * tradeFee = $0.02
) is unfair amount earned by User2 and loss of funds for User1.When redeeming, the formula for shares -> assets vault checkpoint conversion is correct and the correct amount is redeemed.
This issue leads to all vault depositors slowly losing share value with each deposit, and since no value is gained when redeeming, continuous deposits and redeems will lead to all long-time depositors continuously losing their funds.
Vulnerability Detail
This is the formula for vault checkpoint
toSharesGlobal
:This code translates to a formula shown above, i.e. it first subtracts settlement fee from the assets (
withoutSettlementFeeGlobal
), then multiplies this by checkpoint's share value in_toShares
(*checkpoint.shares/checkpoint.assets
), and then multiplies this by trade fee adjustment in_withSpread
(*(deposit+redeem-tradeFee) / (deposit+redeem)
). Here is the formula again:shares = (assets[before fee] - settlementFee) * checkpoint.shares/checkpoint.assets * (deposit + redeem - tradeFee) / (deposit + redeem)
As shown above, the formula is incorrect, because it basically does the following:
user_assets = (deposit - settlementFee) * (deposit - tradeFee)/deposit = deposit * (1 - settlementFeePct) * (1 - tradeFeePct)
But the actual user collateral after fees is calculated as:
user_assets = deposit - settlementFee - tradeFee = deposit * (1 - settlementFeePct - tradeFeePct)
If we subtract the actual collateral from the formula used in checkpoint, we get the error:
error = deposit * ((1 - settlementFeePct) * (1 - tradeFeePct) - (1 - settlementFeePct - tradeFeePct))
error = deposit * settlementFeePct * tradeFeePct
error = settlementFee * tradeFeePct
So this is systematic error, which inflates the shares given to users with any deposit by fixed amount of
settlementFee * tradeFeePct
Impact
Any vault deposit reduces the vault assets by
settlementFee * tradeFeePct
. While this amount is not very large (in the order of $0.1 - $0.001 per deposit transaction), this is amount lost with each deposit, and given that an active vault can easily have 1000s of transactions daily, this will be a loss of $1-$100/day, which is significant enough to make it a valid issue.Code Snippet
SettlementFee subtracted from asset before proceeding in
toSharesGlobal
: https://github.com/sherlock-audit/2024-02-perennial-v2-3/blob/main/perennial-v2/packages/perennial-vault/contracts/types/Checkpoint.sol#L91-L97The result is multiplied by the checkpoint's share to assets ratio in
_toShares
: https://github.com/sherlock-audit/2024-02-perennial-v2-3/blob/main/perennial-v2/packages/perennial-vault/contracts/types/Checkpoint.sol#L153-L156And the final result is multiplied by
tradeFee
-adjusted deposits and redeems in_withSpread
: https://github.com/sherlock-audit/2024-02-perennial-v2-3/blob/main/perennial-v2/packages/perennial-vault/contracts/types/Checkpoint.sol#L169-L177Tool used
Manual Review
Recommendation
Re-work the assets to shares conversion in vault checkpoint to use the correct formula:
shares = (assets[before fee] - settlementFee - tradeFee * assets / (deposit + redeem)) * checkpoint.shares/checkpoint.assets