Part 2

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

Users can initiate multiple swaps against the same vault/collateral causing the users to lntiate swaps that can not be fulfillable.

Summary

The initiate swap function fails to prevent a user from initiating multiple swaps against the same vault at the same time(Batched call). The current checks implemented cannot handle multiple calls for the same vault hence the sanity checks to ensure the swap is healthy will fail to prevent calls to swap that ideally will fail.

Vulnerability Details

  1. The total balance of the vault is tracked with address (this) hence we are initiating against the balance over again. instead of the actual vault balance since the last batch call will reduce this balance.

function initiateSwap( //@AUDIT batching isssue NOTE
uint128[] calldata vaultIds,
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
@audit>>> 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) {
@audit>>> currentVault = Vault.load(vaultIds[i]);
// revert for swaps using vaults with different collateral assets
@audit>>> if (currentVault.collateral.asset != ctx.initialVaultCollateralAsset) {
revert Errors.VaultsCollateralAssetsMismatch();
}
// refresh current vault balance in native precision of ctx.initialVaultCollateralAsset
@audit>>> 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);
}
@audit>>> // if there aren't enough assets in the vault to fulfill the swap request, we must revert
@audit>>> if (ctx.vaultAssetBalance < ctx.expectedAssetOut) { // WRONG CHECK note
revert Errors.InsufficientVaultBalance(vaultIds[i], ctx.vaultAssetBalance, ctx.expectedAssetOut);
}

2.The total debt will be decreased for the vault after each individual swaps but the old debt valuation is returned for a batched call.

function getAmountOfAssetOut(
uint128 vaultId,
UD60x18 usdAmountInX18,
UD60x18 indexPriceX18
)
public
view
returns (UD60x18 amountOutX18)
{
// fetch the vault's storage pointer
Vault.Data storage vault = Vault.load(vaultId);
// fetch the vault's total assets in USD; if the vault is empty
// revert here to prevent panic from subsequent divide by zero
UD60x18 vaultAssetsUsdX18 = ud60x18(IERC4626(vault.indexToken).totalAssets()).mul(indexPriceX18); //@AUDIT NOTE changes per swap amount out calculation might be wrong
if (vaultAssetsUsdX18.isZero()) revert Errors.InsufficientVaultBalance(vaultId, 0, 0);
// we use the vault's net sum of all debt types coming from its connected markets to determine the swap rate
@audit SD59x18 vaultDebtUsdX18 = vault.getTotalDebt(); //@audit NOTE QUESTION did we rebalance the vault before taking action here //@AUDIT NOTE changes per swap amount out calculation might be wrong

Total debt changes after each swap action

// update vault debt
@audit>> vault.marketsRealizedDebtUsd -= int128(ctx.amountIn);
// burn usd amount from address(this)
ctx.usdToken.burn(ctx.amountIn);
/// @notice Returns the vault's total debt distributed from connected markets.
/// @dev Takes into account the unrealized debt, the unsettled (yet to be settled) realized debt and the usdc
/// credit deposited by markets.
/// @param self The vault storage pointer.
function getTotalDebt(Data storage self) internal view returns (SD59x18 totalDebtUsdX18) {
totalDebtUsdX18 = getUnsettledRealizedDebt(self).add(sd59x18(self.marketsUnrealizedDebtUsd));
}

Impact

Most batched swap calls initiated will fail if this is done against the same vault, as the current implementation obtains values in a way that cannot handle multiple calls to the same value,e making most of the critical checks ineffective. The user will pay a base fee for these failed swaps, regardless of whether it is executed or not hence the need to accurately track the balance and debt to ensure that only executable swaps are allowed.

Tools Used

Manual Review

Recommendations

Prevent the initiation of multiple calls in the same vault by ensuring that the last vault id is always less than the previous and ensuring that we are not repeating vault ids

Updates

Lead Judging Commences

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

Users can initiate multiple swaps against the same vault/collateral causing the users to lntiate swaps that can not be fulfillable.

Support

FAQs

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