Max profit can exceed amount reserved from vault
Description
When a new trade is finalized in _registerTrade
, the amount reserved from the vault to ensure that the trade can be closed is the leveraged position size:
function _registerTrade(ITradingStorage.Trade memory _trade)
private returns (ITradingStorage.Trade memory) {
//...
storageT.vaultManager().reserveBalance(
_trade.positionSizeUSDC.mul(_trade.leverage));
storageT.vaultManager().receiveUSDCFromTrader(
_trade.trader, _trade.positionSizeUSDC, 0);
This leveraged position size is calculated by multiplying the unleveraged position size positionSizeUSDC
by the leverage.
However, the amount that the trader receives when closing the position is only capped at 900% of the unleveraged position size:
function _currentPercentProfit(
uint openPrice,
uint currentPrice,
bool buy,
uint leverage
) private pure returns (int p) {
int diff = buy ? (int(currentPrice) - int(openPrice))
: (int(openPrice) - int(currentPrice));
int minPnlP = int(_PRECISION) * (-100);
int maxPnlP = int(_MAX_GAIN_P) * int(_PRECISION);
p = (diff * 100 * int(_PRECISION.mul(leverage))) / int(openPrice);
p = p < minPnlP ? minPnlP : p > maxPnlP ? maxPnlP : p;
}
Here, the constant _MAX_GAIN_P
is 900%, and the function's return value is later multiplied by positionSizeUSDC
to determine the amount of USDC sent back to the trader.
This means that if the leverage is less than 9x and the underlying price moves in the direction the trader expected, the amount reserved from the vault can be less than the amount the trader receives when they close the position.
Impact
The protocol can become insolvent if multiple low-leverage trades are profitable, because the amount reserved would be insufficient to cover rewards.
See this POC output, where an LP supplies liquidity, then several traders open long positions with 2x leverage, and then the underlying price increases 5x so the trades close at max profit:
Start
- Junior reserved = 0 actual = 0
- Senior reserved = 0 actual = 0
LP provides liquidity
- Junior reserved = 0 actual = 995024875621
- Junior util ratio % = 0
- Senior reserved = 0 actual = 1000000000000
- Senior util ratio % = 0
Traders open low-leverage market longs
- Junior reserved = 18671560034 actual = 995024875621
- Junior util ratio % = 1
- Senior reserved = 10053916944 actual = 1000000000000
- Senior util ratio % = 1
Traders close positions at max profit
- Junior reserved = 0 actual = 920503530457
- Junior util ratio % = 0
- Senior reserved = 0 actual = 959873121832
- Senior util ratio % = 0
Out of the junior tranche, the traders were in total awarded 995024875621-920503530457
, which is about 7.5e10, but only 18671560034
, which is about 1.8e10, was reserved. Similarly, about 4e10 was taken out of the senior tranche, but only about 1e10 was reserved.
The following output is the same scenario, except before the traders close their max-profit positions, the LP decides to remove most of the unreserved liquidity:
Start
- Junior reserved = 0 actual = 0
- Senior reserved = 0 actual = 0
LP provides liquidity
- Junior reserved = 0 actual = 995024875621
- Junior util ratio % = 0
- Senior reserved = 0 actual = 1000000000000
- Senior util ratio % = 0
Traders open low-leverage market longs
- Junior reserved = 18671560034 actual = 995024875621
- Junior util ratio % = 1
- Senior reserved = 10053916944 actual = 1000000000000
- Senior util ratio % = 1
LP withdraws most liquidity
- Junior reserved = 18671560034 actual = 19361283100
- Junior util ratio % = 96
- Senior reserved = 10053916944 actual = 10309251944
- Senior util ratio % = 97
Traders close positions at max profit
[FAIL. Reason: ERC20: transfer amount exceeds balance]
After the LP withdraws most of the liquidity, the utilization ratios are 96% and 97%, so even if Finding refā is fixed, this withdrawal would still go through.
Next, the traders' calls to Trading.closeTradeMarket
unfortunately revert because inside the Tranche.withdrawAsVaultManager
, the call to USDC.transfer
reverts as the tranches do not have enough funds to send out. This shows that the reserved quantity was not sufficient.
Recommendations
Instead of reserving the leveraged position size, reserve an amount equal to the max amount of USDC that the trader can potentially get back upon closing the position, maximized across all values the underlying price can be in the future. This quantity will depend on the max profit parameter and should not depend on the leverage.
Remediation
This issue has been acknowledged by Avantis as a risk that is within the risk tolerance of the protocol. The maximum open interest and other safety limits on individual trades are expected to collectively mitigate this risk.