The amount-in passed by users is not verified and checked hence the user will not be able to get a refund or get fulfillment of their swaps, this issue has a low likelihood but a high impact.
When funds are sent to the STABILITYBRANCH we fail to check if the amount-in is sufficient. If this amount is insufficient, the user's token will be stuck in the contract until the admin intervenes by deprecating all fees.
function initiateSwap(
uint128[] calldata vaultIds,
@audit>> uint128[] calldata amountsIn, 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);
}
ctx.expectedAssetOut =
getAmountOfAssetOut(vaultIds[i], ud60x18(amountsIn[i]), ctx.collateralPriceX18).intoUint256();
if (ctx.expectedAssetOut == 0) revert Errors.ZeroOutputTokens();
if (ctx.expectedAssetOut < minAmountsOut[i]) {
revert Errors.SlippageCheckFailed(minAmountsOut[i], ctx.expectedAssetOut);
}
if (ctx.vaultAssetBalance < ctx.expectedAssetOut) {
revert Errors.InsufficientVaultBalance(vaultIds[i], ctx.vaultAssetBalance, ctx.expectedAssetOut);
}
ctx.usdTokenOfEngine = IERC20(configuration.usdTokenOfEngine[currentVault.engine]);
ctx.usdTokenOfEngine.safeTransferFrom(msg.sender, address(this), amountsIn[i]);
ctx.requestId = tokenSwapData.nextId(msg.sender);
UsdTokenSwapConfig.SwapRequest storage swapRequest = tokenSwapData.swapRequests[msg.sender][ctx.requestId];
swapRequest.minAmountOut = minAmountsOut[i];
swapRequest.vaultId = vaultIds[i];
swapRequest.assetOut = ctx.initialVaultCollateralAsset;
ctx.deadlineCache = uint120(block.timestamp) + ctx.maxExecTime;
swapRequest.deadline = ctx.deadlineCache;
@audit>> swapRequest.amountIn = amountsIn[i];
emit LogInitiateSwap(
msg.sender,
ctx.requestId,
vaultIds[i],
amountsIn[i],
minAmountsOut[i],
ctx.initialVaultCollateralAsset,
ctx.deadlineCache
);
}
}
When calculating the refund if we cannot pay the basefee the funds become stuck in the contract
function refundSwap(uint128 requestId, address engine) external {
UsdTokenSwapConfig.Data storage tokenSwapData = UsdTokenSwapConfig.load();
UsdTokenSwapConfig.SwapRequest storage request = tokenSwapData.swapRequests[msg.sender][requestId];
if (request.processed) {
revert Errors.RequestAlreadyProcessed(msg.sender, requestId);
}
uint120 deadlineCache = request.deadline;
if (deadlineCache > block.timestamp) {
revert Errors.RequestNotExpired(msg.sender, requestId);
}
request.processed = true;
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
address usdToken = marketMakingEngineConfiguration.usdTokenOfEngine[engine];
uint256 baseFeeUsd = tokenSwapData.baseFeeUsd;
@audit>>>> uint128 depositedUsdToken = request.amountIn;
marketMakingEngineConfiguration.distributeProtocolAssetReward(usdToken, baseFeeUsd);
@audit>>>> uint256 refundAmountUsd = depositedUsdToken - baseFeeUsd;
@audit>>>> IERC20(usdToken).safeTransfer(msg.sender, refundAmountUsd);
emit LogRefundSwap(
msg.sender,
requestId,
request.vaultId,
depositedUsdToken,
request.minAmountOut,
request.assetOut,
deadlineCache,
baseFeeUsd,
refundAmountUsd
);
}
Amount-in will be stuck in the contract with a low likelihood and a high impact, proper validation should be set to prevent this.
Nest a check to ensure that the amount-in passed by the user is always enough to cover the base fee else revert the transaction.