Open sherlock-admin2 opened 5 months ago
1 comment(s) were left on this issue during the judging contest.
panprog commented:
invalid by sherlock rules
@panprog What is your comment referring to here?
@nevillehuang The comment was meant for issue #7, somehow got mixed up with this one. This issue is valid.
The protocol team fixed this issue in the following PRs/commits: https://github.com/equilibria-xyz/perennial-v2/pull/301
The Lead Senior Watson signed off on the fix.
panprog
high
Empty orders do not request from oracle and during settlement they use an invalid oracle version with
price=0
which messes up a lot of fees and funding accounting leading to loss of funds for the makersSummary
When
market.update
which doesn't change user's position is called, a new (current) global order is created, but the oracle version is not requested due to empty order. This means that during the order settlement, it will use non-existant invalid oracle version withprice = 0
. This price is then used to accumulate all the data in this invalidVersion
, meaning accounting is done usingprice = 0
, which is totally incorrect. For instance, all funding and fees calculations multiply by oracle version's price, thus all time periods between empty order and the next valid oracle version will not accumulate any fees, which is funds usually lost by makers (as makers won't receive fees/funding for the risk they take).Vulnerability Detail
When
market.update
is called, it requests a new oracle version at the current order's timestamp unless the order is empty:The order is empty when it doesn't modify user position:
Later, when a valid oracle version is commited, during the settlement process, oracle version at the position is used:
If the oracle version is invalid, the order is invalidated, but the
price=0
is still used to accumulate. It doesn't affect pnl from price move, because the final oracle version is always valid, thus the correct price is used to evaluate all possible account actions, however it does affect accumulated fees and funding:As can be seen, all funding and fees accumulations multiply by oracle version's price (which is 0), thus during these time intervals fees and funding are 0.
This will happen by itself during any period when there are no orders, because oracle provider's settlement callback uses
market.update
with empty order to settle user account, thus any non-empty order is always followed by an empty order for the next version andprice = 0
will be used to settle it until the next non-empty order:Impact
All fees and funding are incorrectly calculated as 0 during any period when there are no non-empty orders (which will be substantially more than 50% of the time, more like 90% of the time). Since most fees and funding are received by makers as a compensation for their price risk, this means makers will lose all these under-calculated fees and will receive a lot less fees and funding than expected.
Proof of concept
The scenario above is demonstrated in the test, add this to test/unit/market/Market.test.ts:
Console log:
Notice, that fees are accumulated between versions 200 and 300, version 300 has huge pnl (because it's evaluated at price = 0), which then returns to normal at version 400, but no fees are accumulated between version 300 and 400 due to version 300 having
price = 0
.Code Snippet
Market._update
requests a new oracle version only when the order is not empty: https://github.com/sherlock-audit/2024-02-perennial-v2-3/blob/main/perennial-v2/packages/perennial/contracts/Market.sol#L466-L467Market._processOrderGlobal
invalidates the order for invalid oracle version, but still uses invalid oracle's price (which is 0) to accumulate: https://github.com/sherlock-audit/2024-02-perennial-v2-3/blob/main/perennial-v2/packages/perennial/contracts/Market.sol#L610-L625Tool used
Manual Review
Recommendation
Keep the price from the previous valid oracle version and use it instead of oracle version's one if oracle version's price == 0.