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:
505:
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:
138: Market.Data storage market = Market.loadLive(marketId);
139: SD59x18 marketTotalDebtUsdX18 = market.getTotalDebt();
140:
141:
142: UD60x18 delegatedCreditUsdX18 = market.getTotalDelegatedCreditUsd();
143: SD59x18 creditCapacityUsdX18 = Market.getCreditCapacityUsd(delegatedCreditUsdX18, marketTotalDebtUsdX18);
144:
145:
146:
147: if (creditCapacityUsdX18.lte(SD59x18_ZERO)) {
148: revert Errors.InsufficientCreditCapacity(marketId, creditCapacityUsdX18.intoInt256());
149: }
150:
151:
152: adjustedProfitUsdX18 = ud60x18(profitUsd);
153:
154:
155:
156: if (market.isAutoDeleverageTriggered(delegatedCreditUsdX18, marketTotalDebtUsdX18)) {
157:
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:
251: Market.Data storage market = Market.loadLive(marketId);
252: uint256[] memory connectedVaults = market.getConnectedVaultsIds();
253:
254:
255:
256: Vault.recalculateVaultsCreditCapacity(connectedVaults);
257:
258:
259: SD59x18 marketTotalDebtUsdX18 = market.getTotalDebt();
260: UD60x18 delegatedCreditUsdX18 = market.getTotalDelegatedCreditUsd();
261:
....
278:
279:
280: UD60x18 amountX18 = ud60x18(amount);
281:
282:
283:
284: uint256 amountToMint = amount;
...
288 if (market.isAutoDeleverageTriggered(delegatedCreditUsdX18, marketTotalDebtUsdX18)) {
289:
290:
291: UD60x18 adjustedUsdTokenToMintX18 =
292: market.getAutoDeleverageFactor(delegatedCreditUsdX18, marketTotalDebtUsdX18).mul(amountX18);
293:
294: amountToMint = adjustedUsdTokenToMintX18.intoUint256();
295: market.updateNetUsdTokenIssuance(adjustedUsdTokenToMintX18.intoSD59x18());
....
305:
306: UsdToken usdToken = UsdToken(marketMakingEngineConfiguration.usdTokenOfEngine[msg.sender]);
307: usdToken.mint(msg.sender, amountToMint);
308:
309:
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));
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);
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.