Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: high
Valid

The Deleverage Will apply twice on market USDtoken minting

Summary

When the _fillOrder is called we check that if the old position of user has some PNL , than we check that adjust profit and tries to find if any deleverage could be applied otherwise the PNL value will be provided withdrawUsdTokenFromMarket , the issue arises in case when deleverage factor could be applied in this case the getAdjustedProfitForMarketId returns the value on which we have applied the deleverage factor and pass that value to withdrawUsdTokenFromMarket which also applies Deleverage factor , so in this case the market will receive less usdToken than expected.

Vulnerability Details

let have a look on how the issue occurs:
first we will call fillOrder function which check that pnlUsdX18>0 , than we calls getAdjustedProfitForMarketId.

/src/perpetuals/branches/SettlementBranch.sol:496
496: if (ctx.pnlUsdX18.gt(SD59x18_ZERO)) {
497: IMarketMakingEngine marketMakingEngine = IMarketMakingEngine(perpsEngineConfiguration.marketMakingEngine);
498:
499: ctx.marginToAddX18 =
500: marketMakingEngine.getAdjustedProfitForMarketId(marketId, ctx.pnlUsdX18.intoUD60x18().intoUint256());
501:
502: tradingAccount.deposit(perpsEngineConfiguration.usdToken, ctx.marginToAddX18);
503:
504: // mint settlement tokens credited to trader; tokens are minted to
505: // address(this) since they have been credited to the trader's margin
506: marketMakingEngine.withdrawUsdTokenFromMarket(marketId, ctx.marginToAddX18.intoUint256());
507: }
508:

The getAdjustedProfitForMarketId function will checks that if the deleverage factor could be applied in our case we assumes that it could be applied:

/src/market-making/branches/CreditDelegationBranch.sol:129
129: function getAdjustedProfitForMarketId(
130: uint128 marketId,
131: uint256 profitUsd
132: )
133: public
134: view
135: returns (UD60x18 adjustedProfitUsdX18)
136: {
137: // load the market's data storage pointer & cache total debt
138: Market.Data storage market = Market.loadLive(marketId);
139: SD59x18 marketTotalDebtUsdX18 = market.getTotalDebt();
140:
141: // caches the market's delegated credit & credit capacity
142: UD60x18 delegatedCreditUsdX18 = market.getTotalDelegatedCreditUsd();
143: SD59x18 creditCapacityUsdX18 = Market.getCreditCapacityUsd(delegatedCreditUsdX18, marketTotalDebtUsdX18);
144:
145: // if the credit capacity is less than or equal to zero then
146: // the total debt has already taken all the delegated credit
147: if (creditCapacityUsdX18.lte(SD59x18_ZERO)) {
148: revert Errors.InsufficientCreditCapacity(marketId, creditCapacityUsdX18.intoInt256());
149: }
150:
151: // uint256 -> UD60x18; output default case when market not in Auto Deleverage state
152: adjustedProfitUsdX18 = ud60x18(profitUsd);
153:
154: // we don't need to add `profitUsd` as it's assumed to be part of the total debt
155: // NOTE: If we don't return the adjusted profit in this if branch, we assume marketTotalDebtUsdX18 is positive
156: if (market.isAutoDeleverageTriggered(delegatedCreditUsdX18, marketTotalDebtUsdX18)) {
157: // if the market's auto deleverage system is triggered, it assumes marketTotalDebtUsdX18 > 0
158: adjustedProfitUsdX18 =
159: market.getAutoDeleverageFactor(delegatedCreditUsdX18, marketTotalDebtUsdX18).mul(adjustedProfitUsdX18);
160: }
161: }

At Line 158 we can see that it will return the profit value on which the deleverage factor is already applied than we pass this value to withdrawUsdTokenFromMarket function which again applies this factor :

/src/market-making/branches/CreditDelegationBranch.sol:249
249: function withdrawUsdTokenFromMarket(uint128 marketId, uint256 amount) external onlyRegisteredEngine(marketId) {
250: // loads the market's data and connected vaults
251: Market.Data storage market = Market.loadLive(marketId);
252: uint256[] memory connectedVaults = market.getConnectedVaultsIds();
253:
254: // once the unrealized debt is distributed update credit delegated
255: // by these vaults to the market
256: Vault.recalculateVaultsCreditCapacity(connectedVaults);
257:
258: // cache the market's total debt and delegated credit
259: SD59x18 marketTotalDebtUsdX18 = market.getTotalDebt();
260: UD60x18 delegatedCreditUsdX18 = market.getTotalDelegatedCreditUsd();
261:
....
278: // uint256 -> UD60x18
279: // NOTE: we don't need to scale decimals here as it's known that USD Token has 18 decimals
280: UD60x18 amountX18 = ud60x18(amount);
281:
282: // prepare the amount of usdToken that will be minted to the perps engine;
283: // initialize to default non-ADL state
284: uint256 amountToMint = amount;
...
288 if (market.isAutoDeleverageTriggered(delegatedCreditUsdX18, marketTotalDebtUsdX18)) {
289: // if the market is in the ADL state, it reduces the requested USD
290: // Token amount by multiplying it by the ADL factor, which must be < 1
291: UD60x18 adjustedUsdTokenToMintX18 =
292: market.getAutoDeleverageFactor(delegatedCreditUsdX18, marketTotalDebtUsdX18).mul(amountX18);
293:
294: amountToMint = adjustedUsdTokenToMintX18.intoUint256();
295: market.updateNetUsdTokenIssuance(adjustedUsdTokenToMintX18.intoSD59x18());
....
305: // mint USD Token to the perps engine
306: UsdToken usdToken = UsdToken(marketMakingEngineConfiguration.usdTokenOfEngine[msg.sender]);
307: usdToken.mint(msg.sender, amountToMint);
308:
309: // emit an event
310: emit LogWithdrawUsdTokenFromMarket(msg.sender, marketId, amount, amountToMint);
311: }
312:

Here we can see Line 288 if isAutoDeleverageTriggered is true than we again apply deleveregae factor and mint that value to market.

POC

The following POC will proof it :

function test_WhenTheAutoDeleverageFactorIsTriggered(
uint256 marketId
)
external
whenTheMarketIsLive
whenTheCreditCapacityIsGreaterThanZero
{
uint256 profitUsd= 10e18;
PerpMarketCreditConfig memory fuzzMarketConfig = getFuzzPerpMarketCreditConfig(marketId);
marketMakingEngine.workaround_setMarketUsdTokenIssuance(fuzzMarketConfig.marketId, 5e9 + 10);
UD60x18 delegatedCreditUsdX18 =
marketMakingEngine.workaround_getTotalDelegatedCreditUsd(fuzzMarketConfig.marketId);
SD59x18 totalDebtUsdX18 = marketMakingEngine.workaround_getTotalMarketDebt(fuzzMarketConfig.marketId);
UD60x18 autoDeleverageFactorX18 = marketMakingEngine.workaround_getAutoDeleverageFactor(
fuzzMarketConfig.marketId, delegatedCreditUsdX18, totalDebtUsdX18
);
UD60x18 adjustedProfitUsdX18 =
marketMakingEngine.getAdjustedProfitForMarketId(fuzzMarketConfig.marketId, profitUsd);
console.log("adjustedProfitUsdX18" , adjustedProfitUsdX18.intoUint256());
console.log("autoDeleverageFactorX18" , autoDeleverageFactorX18.intoUint256());
uint256 balBefore = IERC20(usdToken).balanceOf(address(perpsEngine));
// it should return the adjusted profit
// applying the Deleveraging factor we exepect to mint this much adjustedProfitUsdX18 to market
assertEq(ud60x18(profitUsd).mul(autoDeleverageFactorX18).intoUint256(), adjustedProfitUsdX18.intoUint256());
changePrank({ msgSender:address(perpsEngine)});
marketMakingEngine.withdrawUsdTokenFromMarket(fuzzMarketConfig.marketId, adjustedProfitUsdX18.intoUint256());
uint256 balAfter = IERC20(usdToken).balanceOf(address(perpsEngine));
assertNotEq(adjustedProfitUsdX18.intoUint256(),balAfter - balBefore);
// but the code apply deleverage twice
adjustedProfitUsdX18 = adjustedProfitUsdX18.mul(autoDeleverageFactorX18);
assertEq(adjustedProfitUsdX18.intoUint256(),balAfter - balBefore);
}

Add the above test in CreditDelegationBranch_GetAdjustedProfitForMarketId_Integration_Test test contract and run with command :
forge test --mt test_WhenTheAutoDeleverageFactorIsTriggered -vvv

Impact

Due to apllying twice delevereage factor the prep engine will receive less usdToken. So lose of funds for prep engine.

Tools Used

Manual Review

Recommendations

Pass the exact PNL value to withdrawUsdTokenFromMarket or remove the deleverage factor calculation for withdrawUsdTokenFromMarket function.

Updates

Lead Judging Commences

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

`SettlementBranch._fillOrder` profit adjustment is applied to positive PnL twice.

Support

FAQs

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