Summary
Each swap initiated in the stability contract has a slippage check to prevent users from losing funds and ensuring they get the optimal value but this check during initiation does not ensure that the removal of fees will not revert the transaction.
Also if these fees were removed and the swap is good it should proceed but because the amount out should to checked if this is possible is bigger than the actual amount out the call will revert a good swap.
Vulnerability Details
Ideally, the check should subtract the base fee and settlement fee but this is not done during the swap simulation.
function initiateSwap(
uint128[] calldata vaultIds,
uint128[] calldata amountsIn,
@audit>> uint128[] calldata minAmountsOut
)
external
{
if (vaultIds.length != amountsIn.length) {
revert Errors.ArrayLengthMismatch(vaultIds.length, amountsIn.length);
}
if (amountsIn.length != minAmountsOut.length) {
revert Errors.ArrayLengthMismatch(amountsIn.length, minAmountsOut.length);
}
InitiateSwapContext memory ctx;
Vault.Data storage currentVault = Vault.load(vaultIds[0]);
ctx.initialVaultIndexToken = currentVault.indexToken;
ctx.initialVaultCollateralAsset = currentVault.collateral.asset;
Collateral.Data storage collateral = Collateral.load(ctx.initialVaultCollateralAsset);
collateral.verifyIsEnabled();
MarketMakingEngineConfiguration.Data storage configuration = MarketMakingEngineConfiguration.load();
UsdTokenSwapConfig.Data storage tokenSwapData = UsdTokenSwapConfig.load();
ctx.collateralPriceX18 = currentVault.collateral.getPrice();
ctx.maxExecTime = uint120(tokenSwapData.maxExecutionTime);
ctx.vaultAssetBalance = IERC20(ctx.initialVaultCollateralAsset).balanceOf(ctx.initialVaultIndexToken);
for (uint256 i; i < amountsIn.length; i++) {
if (i != 0) {
currentVault = Vault.load(vaultIds[i]);
if (currentVault.collateral.asset != ctx.initialVaultCollateralAsset) {
revert Errors.VaultsCollateralAssetsMismatch();
}
ctx.vaultAssetBalance = IERC20(ctx.initialVaultCollateralAsset).balanceOf(currentVault.indexToken);
}
@audit>> ctx.expectedAssetOut =
getAmountOfAssetOut(vaultIds[i], ud60x18(amountsIn[i]), ctx.collateralPriceX18).intoUint256();
if (ctx.expectedAssetOut == 0) revert Errors.ZeroOutputTokens();
@audit>> if (ctx.expectedAssetOut < minAmountsOut[i]) {
revert Errors.SlippageCheckFailed(minAmountsOut[i], ctx.expectedAssetOut);
}
@audit>> if (ctx.vaultAssetBalance < ctx.expectedAssetOut) {
revert Errors.InsufficientVaultBalance(vaultIds[i], ctx.vaultAssetBalance, ctx.expectedAssetOut);
}
Issue 1. slippage will not handle the swap correctly
if (ctx.expectedAssetOut == 0) revert Errors.ZeroOutputTokens();
@audit>> check can pass here and fail in swap >> if (ctx.expectedAssetOut < minAmountsOut[i]) {
revert Errors.SlippageCheckFailed(minAmountsOut[i], ctx.expectedAssetOut);
}
In swap
@audit>> ctx.amountOut =
collateral.convertUd60x18ToTokenAmount(ctx.amountOutBeforeFeesX18.sub(ctx.baseFeeX18.add(ctx.swapFeeX18)));
ctx.minAmountOut = request.minAmountOut;
@audit>> if (ctx.amountOut < ctx.minAmountOut) {
revert Errors.SlippageCheckFailed(ctx.minAmountOut, ctx.amountOut);
}
Issue 2. a good swap will fail
Only about 90% of the settlement fee will be transferred after the swap.
@audit>> max fee is 0.9e18>> ctx.protocolSwapFeeX18 = ctx.swapFeeX18.mul(ud60x18(marketMakingEngineConfiguration.totalFeeRecipientsShares));
ctx.protocolReward = collateral.convertUd60x18ToTokenAmount(ctx.baseFeeX18.add(ctx.protocolSwapFeeX18));
vault.marketsRealizedDebtUsd -= int128(ctx.amountIn);
ctx.usdToken.burn(ctx.amountIn);
@audit>> IERC20(ctx.asset).safeTransferFrom(vault.indexToken, address(this), ctx.amountOut + ctx.protocolReward);
@audit>> marketMakingEngineConfiguration.distributeProtocolAssetReward(ctx.asset, ctx.protocolReward);
Hence the check-in initiate will revert to a good swap call, even though we have enough.
if (ctx.vaultAssetBalance < ctx.expectedAssetOut) {
revert Errors.InsufficientVaultBalance(vaultIds[i], ctx.vaultAssetBalance, ctx.expectedAssetOut);
}
Impact
An incorrect slippage check will cause a user to initiate a swap that will reverse during execution and note the user will still pay the basefee regardless. Two, a good swap will be reversed because of the strict amount to balance check without the properly accessing Reward amount and user amount-out that we will be sending out.
Tools Used
manual review
Recommendations
include the fee in the slippage check during initiation and also ensure we correctly track the amount needed against the contract balance.