If attacker's unsettled loss is greater than available margin, the remaining loss will be wiped out. Attacker can craft attack to direct theft margin from protocol.
For users who wish to change their existing positions, the following is the logic executed by _fillOrder
: (We omit some security checks for now)
fetch global config, perp market's config, account config, etc from storage
get funding rates for this perp market
update funding rates
calculate order & settlement fees
calculate required initial & maintenance margin for this trade and account's unrealized PNL
Ensure that the user has sufficient margin (based on calculations in step 5)
cache unrealized PNL from potential existing position in this market including accrued funding
update all config storage
If unrealized PnL > 0, mint new usdTokens for trader's margin account
If unrealized PnL < 0, deduct collateral and order/settlement fees from trader's margin account
Let's dive into step 10:
And the implementation of tradingAccount.deductAccountMargin()
:
As shown above, deductAccountMargin()
iterates through all of the user's margin types and attempts to deduct orderFee, settlementFee and pnl from the user's account.
It is worth noting that deductAccountMargin()
allows the difference between user's margin and (orderFee + settlementFee + pnl) to be skipped - if the user's available margin is insufficient to cover all three charges, then the function will exit normally and return the value of total margin deducted.(This is to allow negative margin positions to be liquidated properly as well) However, in SettlementBranch, the return value is not examined, which means when filling a trade, protocol assumes that the user's available margin is sufficient to cover losses in a given market.
In fact, this assumption is incorrect. An attacker can abuse this mechanism to wipe out his losses for free.
Consider such scenario: (For simplicity's sake, we'll ignore all fees for now, as they are a fairly small percentage of the position and are not important to this issue)
Alice has 10,000usd margin and has two positions now: size = +10,000usd at market A; size = -20,000usd at market B.
As token prices change, the prices of both A and B rise dramatically, makes the pnl reach:
market A: unrealized PnL = 30,000usd
market B: unrealized PnL = -15,000usd
Alice now has a full position with a profit of $15,000, and her account is very healthy.
When Alice tries to place an tiny order for Market B, deductAccountMargin()
will try to subtract 15,000usd from Alice‘s margin, but there's only 10,000 usd available. Due to the mechanism we described above, Alice's $5,000 loss was just ignored by the contract.
Alice then closes position at market B, now she has 0 margin balance and +30000usd PnL in market A.
Alice now has a full position with a profit of $20,000. The extra $5,000 in bad debt is covered by protocol.
Likelihood: medium - Attacks depend on a large profitable position and a large losing position
+
Impact: Critical - direct theft of margin
=
Severity: High
Manual review
If the user's negative unsettled PnL is greater than the current margin, a mechanism similar to forced position reduction should be introduced to ensure that the user pays a sufficiently large amount.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.