Part 2

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

Total debt used in fulfiling swap actions is wrong because we did not update the vault.

Summary

The contract fails to obtain the actual debt amount by updating the current state of the system before keeper performs the swap request of a user, this will cause a user to gain more amount or get less amount than they should because a stale debt amount will be utilized to get the premium discount factor.

Vulnerability Details

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

/// @notice Fulfills a USD token swap request by converting the specified amount of USD tokens to a collateral
/// asset.
/// @param user The address of the user who initiated the swap request.
/// @param requestId The unique ID of the swap request made by the user.
/// @param priceData The off-chain price data provided by Chainlink, encoded in calldata.
/// @dev Called by data streams powered keeper.
/// @dev Invariants involved in the call:
/// The swap request MUST NOT have been processed
/// MUST apply fees when swap is fulfilled
/// The number of received assets MUST be greater than or equal to min assets.
/// MUST only be callable by a registered keeper
function fulfillSwap(
address user,
uint128 requestId,
bytes calldata priceData,
address engine
)
external
onlyRegisteredSystemKeepers
{
// load request for user by id
UsdTokenSwapConfig.SwapRequest storage request = UsdTokenSwapConfig.load().swapRequests[user][requestId];
// revert if already processed
if (request.processed) {
revert Errors.RequestAlreadyProcessed(user, requestId);
}
// working data
FulfillSwapContext memory ctx;
// if request dealine expired revert
ctx.deadline = request.deadline;
if (ctx.deadline < block.timestamp) {
revert Errors.SwapRequestExpired(user, requestId, ctx.deadline);
}
// set request processed to true
request.processed = true;
// load market making engine config
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
// load vault data
ctx.vaultId = request.vaultId;
Vault.Data storage vault = Vault.loadLive(ctx.vaultId);
// get usd token of engine
ctx.usdToken = UsdToken(marketMakingEngineConfiguration.usdTokenOfEngine[engine]);
// load Stability configuration data
StabilityConfiguration.Data storage stabilityConfiguration = StabilityConfiguration.load();
// get price from report in 18 dec
ctx.priceX18 = stabilityConfiguration.verifyOffchainPrice(priceData);
// get amount out asset
ctx.amountIn = request.amountIn;
@audit>>> ctx.amountOutBeforeFeesX18 = getAmountOfAssetOut(ctx.vaultId, ud60x18(ctx.amountIn), ctx.priceX18);

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 somewhat inflated amount to be sent as the amount out. This can cause losses to the vault or the user and the limit to this loses can be far beyond minimal but a medium severity because this losses can be small also.

Tools Used

Manual review

Recommendations

Update the vault to obtain the actual total debt presently in the vault before performing any other action in the fulfil 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:

fulfillSwap should call recalculateVaultsCreditCapacity

Appeal created

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

fulfillSwap should call recalculateVaultsCreditCapacity

Support

FAQs

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