Part 2

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

Lack of an update of the pool state will cause Initiate Swap to return an incorrect Amountout

Summary

Whenever a vault is Live the Debt value in the vault changes with the position unrealised and realised debt. But the initiate swap function fails to update the vault first before calculating the Premium discount factor and this will influence the amount-out calculated causing the vault to realise less or more amount than It should.

Vulnerability Details

The amount out calculated is wrong because the current state of the vault is not used in this calculation

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
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
@audit>>>> ctx.expectedAssetOut =
getAmountOfAssetOut(vaultIds[i], ud60x18(amountsIn[i]), ctx.collateralPriceX18).intoUint256(); //@AUDIT NOTE

The debt amount returned will not be the actual debt amount of the system.

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
// calculate the premium or discount that may be applied to the vault asset's index price
// note: if no premium or discount needs to be applied, the premiumDiscountFactorX18 will be
// 1e18 (UD60x18 one value)
@audit>>>> UD60x18 premiumDiscountFactorX18 =
UsdTokenSwapConfig.load().getPremiumDiscountFactor(vaultAssetsUsdX18, //@audit>> vaultDebtUsdX18); //@audit Premium will be off also NOTE
// get amounts out taking into consideration the CL price and the premium/discount
@audit>>>> amountOutX18 = usdAmountInX18.div(indexPriceX18).mul(premiumDiscountFactorX18); // @audit NOTE big precision loss but none found
}

this wrong total debt is used to get the premium discount factor

/// @notice Returns the premium or discount to be applied to the amount out of a swap, based on the vault's debt
/// and the system configured premium / discount curve parameters.
/// @dev The following invariant defining the premium / discount curve must hold true:
/// f(x) = y_min + Δy * ((x - x_min) / (x_max - x_min))^z | x ∈ [x_min, x_max]
/// @dev The proposed initial curve is defined as:
/// f(x) = 1 + 9 * ((x - 0.3) / 0.5)^3
/// @dev If no premium or discount has to be applied, the function returns 1 as UD60x18.
/// @dev Using the proposed z value of 3, the slope of f(x) near the upper bound of x is steeper than near the
/// lower bound, meaning the premium or discount accelerates faster as the vault's debt / tvl ratio increases.
function getPremiumDiscountFactor(
Data storage self,
UD60x18 vaultAssetsValueUsdX18,
@audit>> SD59x18 vaultDebtUsdX18
)
internal
view
returns (UD60x18 premiumDiscountFactorX18)
{
// calculate the vault's tvl / debt absolute value, positive means we'll apply a discount, negative means
// we'll apply a premium
@audit>> UD60x18 vaultDebtTvlRatioAbs = vaultDebtUsdX18.abs().intoUD60x18().div(vaultAssetsValueUsdX18);
// cache the minimum x value of the premium / discount curve
UD60x18 pdCurveXMinX18 = ud60x18(self.pdCurveXMin);
// cache the maximum x value of the premium / discount curve
UD60x18 pdCurveXMaxX18 = ud60x18(self.pdCurveXMax);
// if the vault's debt / tvl ratio is less than or equal to the minimum x value of the premium / discount
// curve, then we don't apply any premium or discount
@audit>> if (vaultDebtTvlRatioAbs.lte(pdCurveXMinX18)) {
premiumDiscountFactorX18 = UD60x18_UNIT;
return premiumDiscountFactorX18;
}
// if the vault's debt / tvl ratio is greater than or equal to the maximum x value of the premium / discount
// curve, we use the max X value, otherwise, use the calculated vault tvl / debt ratio
UD60x18 pdCurveXX18 = vaultDebtTvlRatioAbs.gte(pdCurveXMaxX18) ? pdCurveXMaxX18 : vaultDebtTvlRatioAbs;
// cache the minimum y value of the premium / discount curve
UD60x18 pdCurveYMinX18 = ud60x18(self.pdCurveYMin);
// cache the maximum y value of the premium / discount curve
UD60x18 pdCurveYMaxX18 = ud60x18(self.pdCurveYMax);
// cache the exponent that determines the steepness of the premium / discount curve
UD60x18 pdCurveZX18 = ud60x18(self.pdCurveZ);
// calculate the y point of the premium or discount curve given the x point
UD60x18 pdCurveYX18 = pdCurveYMinX18.add(
pdCurveYMaxX18.sub(pdCurveYMinX18).mul(
pdCurveXX18.sub(pdCurveXMinX18).div(pdCurveXMaxX18.sub(pdCurveXMinX18)).pow(pdCurveZX18) // @audit divide by 1e18 raised to z else we have an incorrect calculation here . NOTE overscaled .div
)
);
// if the vault is in credit, we apply a discount, otherwise, we apply a premium
@audit>> premiumDiscountFactorX18 =
@audit>> vaultDebtUsdX18.lt(SD59x18_ZERO) ? UD60x18_UNIT.sub(pdCurveYX18) : UD60x18_UNIT.add(pdCurveYX18);
}

Without updating the state of the vault the debt value will not be accurate

/// @notice Recalculates the latest credit capacity of the provided vaults ids taking into account their latest
/// assets and debt usd denonimated values.
/// @dev We use a `uint256` array because a market's connected vaults ids are stored at a `EnumerableSet.UintSet`.
/// @dev We assume this function's caller checks that connectedMarketsIdsCache > 0.
/// @param vaultsIds The array of vaults ids to recalculate the credit capacity.
// todo: check where we're messing with the `continue` statement
function recalculateVaultsCreditCapacity(uint256[] memory vaultsIds) internal {
--------------------------
(
uint128[] memory updatedConnectedMarketsIdsCache,
SD59x18 vaultTotalRealizedDebtChangeUsdX18,
SD59x18 vaultTotalUnrealizedDebtChangeUsdX18,
UD60x18 vaultTotalUsdcCreditChangeX18,
UD60x18 vaultTotalWethRewardChangeX18
) = _recalculateConnectedMarketsState(self, connectedMarketsIdsCache, true);
// gas optimization: only write to storage if values have changed
//
// updates the vault's stored unsettled realized debt distributed from markets
if (!vaultTotalRealizedDebtChangeUsdX18.isZero()) {
@audit>> self.marketsRealizedDebtUsd = sd59x18(self.marketsRealizedDebtUsd).add(
vaultTotalRealizedDebtChangeUsdX18
).intoInt256().toInt128();
}
// updates the vault's stored unrealized debt distributed from markets
if (!vaultTotalUnrealizedDebtChangeUsdX18.isZero()) {
@audit>> self.marketsUnrealizedDebtUsd = sd59x18(self.marketsUnrealizedDebtUsd).add(
vaultTotalUnrealizedDebtChangeUsdX18
).intoInt256().toInt128();
}

Total debt is obtained form the Total realised and unrealised debt

/// @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));
}
function getUnsettledRealizedDebt(Data storage self)
internal
view
returns (SD59x18 unsettledRealizedDebtUsdX18)
{
unsettledRealizedDebtUsdX18 =
sd59x18(self.marketsRealizedDebtUsd).add(unary(ud60x18(self.depositedUsdc).intoSD59x18()));
}

Impact

Amount out is released based on the stale state of the vault causing a deflated and somethings inflated amount to be sent as amount out

Tools Used

Manual review

Recommendations

Update the vault to obtain the actual total debt presently in the vault before perform any other action in the initiate swap function.

// first, we need to update the credit capacity of the vaults
Vault.recalculateVaultsCreditCapacity(vaultsIds);
Updates

Lead Judging Commences

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

initiateSwap should call `recalculateVaultsCreditCapacity`

Support

FAQs

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