Part 2

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

Initial swap DOS

Summary

The initialSwap function can revert because of a division by zero.

Vulnerability Details

When the function initialSwap is called in order to compute the amount out the function call getPremiumDiscountFactor UsdTokenSwapConfig:93 this function returns the premium or discount to be applied to the amount out of a swap, based on the vault's debt and the system configured premium / discount curve parameters..

The problem occur because of this operation :

// calculate the y point of the premium or discount curve given the x point
UD60x18 pdCurveYX18 = pdCurveYMinX18.add(
pdCurveYMaxX18.sub(pdCurveYMinX18).mul(
pdCurveXX18.sub(pdCurveXMinX18).div(pdCurveXMaxX18.sub(pdCurveXMinX18)).pow(pdCurveZX18)
)
);

Because the pdCurveYMin and the pdCurveYMax are never set so they are always eqal to zero. There is absolutely no way to set them and therefore the call will always revert. if this condition is not met in the function :

UD60x18 vaultDebtTvlRatioAbs = vaultDebtUsdX18.abs().intoUD60x18().div(vaultAssetsValueUsdX18);
// cache the minimum x value of the premium / discount curve
UD60x18 pdCurveXMinX18 = ud60x18(self.pdCurveXMin);
// cache the maximum x value of the premium / discount curve
UD60x18 pdCurveXMaxX18 = ud60x18(self.pdCurveXMax);
// if the vault's debt / tvl ratio is less than or equal to the minimum x value of the premium / discount
// curve, then we don't apply any premium or discount
if (vaultDebtTvlRatioAbs.lte(pdCurveXMinX18)) {
premiumDiscountFactorX18 = UD60x18_UNIT;
return premiumDiscountFactorX18;
}

You can run this POC by copy pasting this code in the performUpkeep.t.sol in the usd-token-swap folder of the integration folder in the test and run forge test --mt test_divisionByZero

You should have this output :

Failing tests:
Encountered 1 failing test in test/integration/external/chainlink/keepers/usd-token-swap/performUpkeep/performUpkeep.t.sol:UsdTokenSwapKeeper_PerformUpkeep_Integration_Test
[FAIL: panic: division or modulo by zero (0x12)] test_divisionByZero() (gas: 1189656)
function test_divisionByZero(
)
external
givenInitializeContract
{
uint256 vaultId = 0;
uint256 assetsToDeposit=0;
VaultConfig memory fuzzVaultConfig = getFuzzVaultConfig(vaultId);
TestFuzz_GivenCallPerformUpkeepFunction_Context memory ctx;
ctx.assetsToDeposit = bound({ x: assetsToDeposit, min: 1e18, max: fuzzVaultConfig.depositCap });
deal({
token: address(fuzzVaultConfig.asset),
to: fuzzVaultConfig.indexToken,
give: fuzzVaultConfig.depositCap
});
ctx.usdTokenSwapKeeper = usdTokenSwapKeepers[fuzzVaultConfig.asset];
changePrank({ msgSender: users.owner.account });
UsdTokenSwapKeeper(ctx.usdTokenSwapKeeper).setForwarder(users.keepersForwarder.account);
marketMakingEngine.configureSystemKeeper(ctx.usdTokenSwapKeeper, true);
changePrank({ msgSender: users.naruto.account });
ctx.mockPrice = IPriceAdapter(fuzzVaultConfig.priceAdapter).getPrice().intoUint256();
UD60x18 assetAmountX18 = ud60x18(IERC4626(fuzzVaultConfig.indexToken).totalAssets());
ctx.amountInUsd = assetAmountX18.mul(ud60x18(ctx.mockPrice)).intoUint256();
deal({ token: address(usdToken), to: users.naruto.account, give: ctx.amountInUsd });
IERC20(usdToken).approve(address(marketMakingEngine), ctx.amountInUsd);
ctx.minAmountOut = 0;
initiateUsdSwap(fuzzVaultConfig.vaultId, ctx.amountInUsd, ctx.minAmountOut);
bytes memory mockSignedReport = getMockedSignedReport(fuzzVaultConfig.streamId, ctx.mockPrice);
bytes memory performData = abi.encode(mockSignedReport, abi.encode(users.naruto.account, 1));
ctx.requestId = 1;
UsdTokenSwapConfig.SwapRequest memory request =
marketMakingEngine.getSwapRequest(users.naruto.account, ctx.requestId);
ctx.amountOut = marketMakingEngine.getAmountOfAssetOut(
fuzzVaultConfig.vaultId, ud60x18(ctx.amountInUsd), ud60x18(ctx.mockPrice)
);
(ctx.baseFeeX18, ctx.swapFeeX18) =
marketMakingEngine.getFeesForAssetsAmountOut(ctx.amountOut, ud60x18(ctx.mockPrice));
ctx.amountOutAfterFee =
convertUd60x18ToTokenAmount(fuzzVaultConfig.asset, ctx.amountOut.sub(ctx.baseFeeX18.add(ctx.swapFeeX18)));
ctx.protocolSwapFee = ctx.swapFeeX18.mul(ud60x18(marketMakingEngine.exposed_getTotalFeeRecipientsShares()));
ctx.protocolReward =
convertUd60x18ToTokenAmount(fuzzVaultConfig.asset, ctx.baseFeeX18.add(ctx.protocolSwapFee));
// it should emit {LogFulfillSwap} event
vm.expectEmit({ emitter: address(marketMakingEngine) });
emit StabilityBranch.LogFulfillSwap(
users.naruto.account,
ctx.requestId,
fuzzVaultConfig.vaultId,
request.amountIn,
request.minAmountOut,
request.assetOut,
request.deadline,
ctx.amountOutAfterFee,
ctx.baseFeeX18.intoUint256(),
ctx.swapFeeX18.intoUint256(),
ctx.protocolReward
);
changePrank({ msgSender: users.keepersForwarder.account });
UsdTokenSwapKeeper(ctx.usdTokenSwapKeeper).performUpkeep(performData);
deal({
token: address(fuzzVaultConfig.asset),
to: fuzzVaultConfig.indexToken,
give: fuzzVaultConfig.depositCap
});
deal({ token: address(usdToken), to: users.naruto.account, give: ctx.amountInUsd });
IERC20(usdToken).approve(address(marketMakingEngine), ctx.amountInUsd);
ctx.minAmountOut = 0;
initiateUsdSwap(fuzzVaultConfig.vaultId, 1, 0);
}

Impact

initialSwap will not be callable

Tools Used

Echidna

Recommendations

The protocol should add two setters to ensure that the pdCurveYMin and pdCurveYMax are not eqal zero.

Updates

Lead Judging Commences

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

PremiumDiscountFactor feature cannot be properly configured / used

Support

FAQs

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