Open sherlock-admin opened 11 months ago
1 comment(s) were left on this issue during the judging contest.
polarzero commented:
High. This would both cause a loss of funds and a malfunction in the protocol.
Since standalone settle
was dropped in v2, I believe it's actually impossible for a local account to have no pending positions once initialized, since a new pending position is always created at current
when settling latest
and current != latest
.
Nonetheless we've fixed this here to remain consistent with the implementation in Market
, which provides better safety in case we ever decide to bring back a standalone settle
functionality in future versions.
Since standalone
settle
was dropped in v2, I believe it's actually impossible for a local account to have no pending positions once initialized, since a new pending position is always created atcurrent
when settlinglatest
andcurrent != latest
.
Agree, didn't think about it but indeed it's not possible to make them equal. Still, it can happen as described in point 2 in the report: if liquidator commits oracle unrequested (so the latest is before the first position settlement of the account), then the loop will never enter the "virtual settlement" part and closableAmount
will remain 0.
Since it can still happen but only in certain edge case, this should be downgraded to medium.
Fixed
panprog
high
MultiInvoker liquidation action will revert most of the time due to incorrect closable amount initialization
Summary
The fix to issue 49 of the main contest introduced new invalidation system and additional condition: liquidations must close maximum
closable
amount, which is the amount which can be maximally closed based on the latest settled position.The problem is that
MultiInvoker
incorrectly calculatesclosableAmount
(it's not initialized and thus will often return 0 instead of correct magnitude) and thus mostLIQUIDATION
actions will revert.Vulnerability Detail
MultiInvoker
calculates theclosable
amount in its_latest
function incorrectly. In particular, it doesn't initializeclosableAmount
, so it's set to 0 initially. It then scans pending positions, settling those which should be settled, and reducingclosableAmount
if necessary for remaining pending positions:Notice, that
closableAmount
is initialized topreviousMagnitude
only if there is at least one position that needs to be settled. However, iflocal.latestId == local.currentId
(which is the case for most of the liquidations - position becomes liquidatable due to price changes without any pending positions created by the user), this loop is skipped entirely, never settingclosableAmount
, so it's incorrectly returned as 0, although it's not 0 (it should be the latest settled position magnitude).Since
LIQUIDATE
action ofMultiInvoker
uses_latest
to calculateclosableAmount
andliquidationFee
, these values will be calculated incorrectly and will revert when trying to update the market. See the_liquidate
market update reducingcurrentPosition
byclosable
(which is 0 when it must be bigger):This line will revert because
Market._invariant
verifies thatclosableAmount
must be 0 after updating liquidated position:Impact
All
MultiInvoker
liquidation actions will revert if trying to liquidate users without positions which can be settled, which can happen in 2 cases:local.latestId == local.currentId
). This is the most common case (price has changed and user is liquidated without doing any actions) and we can reasonably expect that this will be the case for at least 50% of liquidations (probably more, like 80-90%).Since this breaks important
MultiInvoker
functionality in most cases and causes loss of funds to liquidator (revert instead of getting liquidation fee), I believe this should be High severity.Code Snippet
There is no initialization of
closableAmount
inMultiInvoker._latest
before the pending positions loop: https://github.com/sherlock-audit/2023-09-perennial/blob/main/perennial-v2/packages/perennial-extensions/contracts/MultiInvoker.sol#L361-L375Initialization only happens when settling position: https://github.com/sherlock-audit/2023-09-perennial/blob/main/perennial-v2/packages/perennial-extensions/contracts/MultiInvoker.sol#L393
However, the loop will often be skipped entirely if there are no pending positions at all, thus
closableAmount
will be returned uninitialized (0): https://github.com/sherlock-audit/2023-09-perennial/blob/main/perennial-v2/packages/perennial-extensions/contracts/MultiInvoker.sol#L376Tool used
Manual Review
Recommendation
Initialize
closableAmount
topreviousMagnitude
: