Part 2

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

MEV attack vector Price Manipulation in settleVaultsDebt

Summary

function settleVaultsDebt(uint256[] calldata vaultsIds) external onlyRegisteredSystemKeepers {
// first, we need to update the credit capacity of the vaults
Vault.recalculateVaultsCreditCapacity(vaultsIds);
// working data, cache usdc address
SettleVaultDebtContext memory ctx;
ctx.usdc = MarketMakingEngineConfiguration.load().usdc;
// load the usdc collateral data storage pointer
Collateral.Data storage usdcCollateralConfig = Collateral.load(ctx.usdc);
for (uint256 i; i < vaultsIds.length; i++) {
// load the vault storage pointer
Vault.Data storage vault = Vault.loadExisting(vaultsIds[i].toUint128());
// cache the vault's unsettled debt, if zero skip to next vault
// amount in zaros internal precision
ctx.vaultUnsettledRealizedDebtUsdX18 = vault.getUnsettledRealizedDebt();
if (ctx.vaultUnsettledRealizedDebtUsdX18.isZero()) continue;
// otherwise vault has debt to be settled, cache the vault's collateral asset
ctx.vaultAsset = vault.collateral.asset;
// loads the dex swap strategy data storage pointer
DexSwapStrategy.Data storage dexSwapStrategy =
DexSwapStrategy.loadExisting(vault.swapStrategy.assetDexSwapStrategyId);
// if the vault is in debt, swap its assets to USDC
if (ctx.vaultUnsettledRealizedDebtUsdX18.lt(SD59x18_ZERO)) {
// get swap amount; both input and output in native precision
ctx.swapAmount = calculateSwapAmount(
dexSwapStrategy.dexAdapter,
ctx.usdc,
ctx.vaultAsset,
usdcCollateralConfig.convertSd59x18ToTokenAmount(ctx.vaultUnsettledRealizedDebtUsdX18.abs())
);
// swap the vault's assets to usdc in order to cover the usd denominated debt partially or fully
// both input and output in native precision
ctx.usdcOut = _convertAssetsToUsdc(
vault.swapStrategy.usdcDexSwapStrategyId,
ctx.vaultAsset,
ctx.swapAmount,
vault.swapStrategy.usdcDexSwapPath,
address(this),
ctx.usdc
);
// sanity check to ensure we didn't somehow give away the input tokens
if (ctx.usdcOut == 0) revert Errors.ZeroOutputTokens();
// uint256 -> udc60x18 scaling native precision to zaros internal precision
ctx.usdcOutX18 = usdcCollateralConfig.convertTokenAmountToUd60x18(ctx.usdcOut);
// use the amount of usdc bought with assets to update the vault's state
// note: storage updates must be done using zaros internal precision
//
// deduct the amount of usdc swapped for assets from the vault's unsettled debt
vault.marketsRealizedDebtUsd -= ctx.usdcOutX18.intoUint256().toInt256().toInt128();
// allocate the usdc acquired to back the engine's usd token
UsdTokenSwapConfig.load().usdcAvailableForEngine[vault.engine] += ctx.usdcOutX18.intoUint256();
// update the variables to be logged
ctx.assetIn = ctx.vaultAsset;
ctx.assetInAmount = ctx.swapAmount;
ctx.assetOut = ctx.usdc;
ctx.assetOutAmount = ctx.usdcOut;
// since we're handling debt, we provide a positive value
ctx.settledDebt = ctx.usdcOut.toInt256();
} else {
// else vault is in credit, swap its USDC previously accumulated
// from market and vault deposits into its underlying asset
// get swap amount; both input and output in native precision
ctx.usdcIn = calculateSwapAmount(
dexSwapStrategy.dexAdapter,
ctx.vaultAsset,
ctx.usdc,
usdcCollateralConfig.convertSd59x18ToTokenAmount(ctx.vaultUnsettledRealizedDebtUsdX18.abs())
);
// get deposited USDC balance of the vault, convert to native precision
ctx.vaultUsdcBalance = usdcCollateralConfig.convertUd60x18ToTokenAmount(ud60x18(vault.depositedUsdc));
// if the vault doesn't have enough usdc use whatever amount it has
// make sure we compare native precision values together and output native precision
ctx.usdcIn = (ctx.usdcIn <= ctx.vaultUsdcBalance) ? ctx.usdcIn : ctx.vaultUsdcBalance;
// swaps the vault's usdc balance to more vault assets and
// send them to the ZLP Vault contract (index token address)
// both input and output in native precision
ctx.assetOutAmount = _convertUsdcToAssets(
vault.swapStrategy.assetDexSwapStrategyId,
ctx.vaultAsset,
ctx.usdcIn,
vault.swapStrategy.assetDexSwapPath,
vault.indexToken,
ctx.usdc
);
// sanity check to ensure we didn't somehow give away the input tokens
if (ctx.assetOutAmount == 0) revert Errors.ZeroOutputTokens();
// subtract the usdc amount used to buy vault assets from the vault's deposited usdc, thus, settling
// the due credit amount (partially or fully).
// note: storage updates must be done using zaros internal precision
vault.depositedUsdc -= usdcCollateralConfig.convertTokenAmountToUd60x18(ctx.usdcIn).intoUint128();
// update the variables to be logged
ctx.assetIn = ctx.usdc;
ctx.assetInAmount = ctx.usdcIn;
ctx.assetOut = ctx.vaultAsset;
// since we're handling credit, we provide a negative value
ctx.settledDebt = -ctx.usdcIn.toInt256();
}
// emit an event per vault settled
emit LogSettleVaultDebt(
vaultsIds[i].toUint128(),
ctx.assetIn,
ctx.assetInAmount,
ctx.assetOut,
ctx.assetOutAmount,
ctx.settledDebt
);
}
}

The settleVaultsDebt function relies on IDexAdapter.getExpectedOutput() to determine how much of an asset to swap for USDC when covering a vault's debt.

However, it does not enforce a minimum expected output (i.e., slippage protection). This creates a MEV attack vector where a front-runner could manipulate the price before execution.

Vulnerability Details

  • The function calculates swap amounts dynamically using IDexAdapter.getExpectedOutput().

  • This does not protect against front-running or price manipulation, meaning that:

    • Attackers could manipulate swap prices before execution.

    • The system could receive less USDC than expected for an asset.

Impact

  1. Sandwich Attacks on Vault Asset Swaps

    • The vault is swapping a large amount of its assets (e.g., WETH) to USDC using a DEX (e.g., Uniswap).

    • MEV bots detect this large order in the mempool.

    • They execute a buy (front-run) → force a price increase → let your swap go through at a worse rate → sell (back-run).

    • Impact: The vault gets fewer USDC than expected.

  2. Oracle Price Lag & Manipulation

    • If the vault relies on on-chain oracles for pricing, an attacker could manipulate the price moments before execution.

    • Example: If the price of WETH/USDC is determined using Uniswap TWAP, an attacker could manipulate the pool price for a few blocks before settlement.

    • Impact: The vault swaps at a manipulated, unfavorable rate.

  3. Arbitrage Using Market Inefficiencies

    • If the contract blindly accepts the DEX price, arbitrageurs could drain the vault by taking advantage of price discrepancies between:

      • DEX (e.g., Uniswap, SushiSwap)

      • CEX (e.g., Binance, Coinbase)

      • Off-chain data sources (Chainlink, Pyth, etc.)

    • Impact: The vault executes swaps at an outdated price, losing value.

Tools Used

Manual Review

Recommendations

Introduce randomized execution delays so that bots cannot predict swap timing.

  • Example:

    function settleVaultsDebt(uint256[] calldata vaultsIds) external onlyRegisteredSystemKeepers nonReentrant {
    require(block.timestamp % 10 != 0, "Random delay applied"); // Execute only on non-multiples of 10 seconds
    ...
    }
Updates

Lead Judging Commences

inallhonesty Lead Judge
4 months ago
inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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