Part 2

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

Lack of any proper validation will cause some users to Loss all initial usdtoken deposited in the stabilitybranch.sol

Summary

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.

Vulnerability Details

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( //@AUDIT batching isssue NOTE
uint128[] calldata vaultIds,
@audit>> uint128[] calldata amountsIn, uint128[] calldata minAmountsOut
)
external
{
// Perform length checks
if (vaultIds.length != amountsIn.length) {
revert Errors.ArrayLengthMismatch(vaultIds.length, amountsIn.length);
}
if (amountsIn.length != minAmountsOut.length) {
revert Errors.ArrayLengthMismatch(amountsIn.length, minAmountsOut.length);
}
// working data
InitiateSwapContext memory ctx;
// cache the vault's index token and asset addresses
Vault.Data storage currentVault = Vault.load(vaultIds[0]);
ctx.initialVaultIndexToken = currentVault.indexToken;
ctx.initialVaultCollateralAsset = currentVault.collateral.asset;
// load collateral data; must be enabled
Collateral.Data storage collateral = Collateral.load(ctx.initialVaultCollateralAsset);
collateral.verifyIsEnabled();
// load market making engine config
MarketMakingEngineConfiguration.Data storage configuration = MarketMakingEngineConfiguration.load();
// load usd token swap data
UsdTokenSwapConfig.Data storage tokenSwapData = UsdTokenSwapConfig.load();
// cache additional common fields
// ctx.collateralPriceX18 in zaros internal precision
ctx.collateralPriceX18 = currentVault.collateral.getPrice();
ctx.maxExecTime = uint120(tokenSwapData.maxExecutionTime);
// ctx.vaultAssetBalance in native precision of ctx.initialVaultCollateralAsset
ctx.vaultAssetBalance = IERC20(ctx.initialVaultCollateralAsset).balanceOf(ctx.initialVaultIndexToken);
for (uint256 i; i < amountsIn.length; i++) {
// for all but first iteration, refresh the vault and enforce same collateral asset
if (i != 0) {
currentVault = Vault.load(vaultIds[i]);
// revert for swaps using vaults with different collateral assets
if (currentVault.collateral.asset != ctx.initialVaultCollateralAsset) {
revert Errors.VaultsCollateralAssetsMismatch();
}
// refresh current vault balance in native precision of ctx.initialVaultCollateralAsset
ctx.vaultAssetBalance = IERC20(ctx.initialVaultCollateralAsset).balanceOf(currentVault.indexToken); // NOTE @audit user can initiate a swap that will fail, track balance correctly medium. user loses funds, base fee.
}
// cache the expected amount of assets acquired with the provided parameters
// amountsIn[i] and ctx.collateralPriceX18 using zaros internal precision
ctx.expectedAssetOut =
getAmountOfAssetOut(vaultIds[i], ud60x18(amountsIn[i]), ctx.collateralPriceX18).intoUint256(); //@AUDIT NOTE
// revert if the slippage wouldn't pass or the expected output was 0
if (ctx.expectedAssetOut == 0) revert Errors.ZeroOutputTokens();
if (ctx.expectedAssetOut < minAmountsOut[i]) {
revert Errors.SlippageCheckFailed(minAmountsOut[i], ctx.expectedAssetOut);
}
// if there aren't enough assets in the vault to fulfill the swap request, we must revert
if (ctx.vaultAssetBalance < ctx.expectedAssetOut) { // WRONG CHECK note
revert Errors.InsufficientVaultBalance(vaultIds[i], ctx.vaultAssetBalance, ctx.expectedAssetOut);
}
// transfer USD: user => address(this) - burned in fulfillSwap
ctx.usdTokenOfEngine = IERC20(configuration.usdTokenOfEngine[currentVault.engine]);
ctx.usdTokenOfEngine.safeTransferFrom(msg.sender, address(this), amountsIn[i]);
// get next request id for user
ctx.requestId = tokenSwapData.nextId(msg.sender);
// load swap request
UsdTokenSwapConfig.SwapRequest storage swapRequest = tokenSwapData.swapRequests[msg.sender][ctx.requestId];
// Set swap request parameters
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]; // @audit NOTE no check to ensure this is bigger than the base fee if the swap fails we have a situation of no refund medium @audit NOTE
emit LogInitiateSwap(
msg.sender,
ctx.requestId,
vaultIds[i],
amountsIn[i],
minAmountsOut[i],
ctx.initialVaultCollateralAsset,
ctx.deadlineCache
);
}
}

issue

When calculating the refund if we cannot pay the basefee the funds become stuck in the contract

/// @notice Refunds a swap request that has not been processed and has expired.
/// @param requestId The unique ID of the swap request to be refunded.
function refundSwap(uint128 requestId, address engine) external { // WE DEY REFUND ooo tor God abeg oo
// load swap data
UsdTokenSwapConfig.Data storage tokenSwapData = UsdTokenSwapConfig.load();
// load swap request
UsdTokenSwapConfig.SwapRequest storage request = tokenSwapData.swapRequests[msg.sender][requestId];
// if request already procesed revert
if (request.processed) {
revert Errors.RequestAlreadyProcessed(msg.sender, requestId);
}
// if dealine has not yet passed revert
uint120 deadlineCache = request.deadline;
if (deadlineCache > block.timestamp) {
revert Errors.RequestNotExpired(msg.sender, requestId);
}
// set precessed to true
request.processed = true;
// load Market making engine config
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
// get usd token for engine
address usdToken = marketMakingEngineConfiguration.usdTokenOfEngine[engine];
// cache the usd token swap base fee
uint256 baseFeeUsd = tokenSwapData.baseFeeUsd;
// cache the amount of usd token previously deposited
@audit>>>> uint128 depositedUsdToken = request.amountIn;
// transfer base fee too protocol fee recipients
marketMakingEngineConfiguration.distributeProtocolAssetReward(usdToken, baseFeeUsd);
// cache the amount of usd tokens to be refunded
@audit>>>> uint256 refundAmountUsd = depositedUsdToken - baseFeeUsd;
// transfer usd refund amount back to user
@audit>>>> IERC20(usdToken).safeTransfer(msg.sender, refundAmountUsd);
emit LogRefundSwap(
msg.sender,
requestId,
request.vaultId,
depositedUsdToken,
request.minAmountOut,
request.assetOut,
deadlineCache,
baseFeeUsd,
refundAmountUsd
);
}

Impact

Amount-in will be stuck in the contract with a low likelihood and a high impact, proper validation should be set to prevent this.

Tools Used

manual review

Recommendations

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.

Updates

Lead Judging Commences

inallhonesty Lead Judge
4 months ago
inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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