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
10 months ago
inallhonesty Lead Judge 10 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.

Give us feedback!