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
function fulfillSwap(
address user,
uint128 requestId,
bytes calldata priceData,
address engine
)
external
onlyRegisteredSystemKeepers
{
UsdTokenSwapConfig.SwapRequest storage request = UsdTokenSwapConfig.load().swapRequests[user][requestId];
if (request.processed) {
revert Errors.RequestAlreadyProcessed(user, requestId);
}
FulfillSwapContext memory ctx;
ctx.deadline = request.deadline;
if (ctx.deadline < block.timestamp) {
revert Errors.SwapRequestExpired(user, requestId, ctx.deadline);
}
request.processed = true;
MarketMakingEngineConfiguration.Data storage marketMakingEngineConfiguration =
MarketMakingEngineConfiguration.load();
ctx.vaultId = request.vaultId;
Vault.Data storage vault = Vault.loadLive(ctx.vaultId);
ctx.usdToken = UsdToken(marketMakingEngineConfiguration.usdTokenOfEngine[engine]);
StabilityConfiguration.Data storage stabilityConfiguration = StabilityConfiguration.load();
ctx.priceX18 = stabilityConfiguration.verifyOffchainPrice(priceData);
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)
{
Vault.Data storage vault = Vault.load(vaultId);
UD60x18 vaultAssetsUsdX18 = ud60x18(IERC4626(vault.indexToken).totalAssets()).mul(indexPriceX18);
if (vaultAssetsUsdX18.isZero()) revert Errors.InsufficientVaultBalance(vaultId, 0, 0);
@audit>>>> SD59x18 vaultDebtUsdX18 = vault.getTotalDebt();
@audit>>>> UD60x18 premiumDiscountFactorX18 =
UsdTokenSwapConfig.load().getPremiumDiscountFactor(vaultAssetsUsdX18,
@audit>>>> amountOutX18 = usdAmountInX18.div(indexPriceX18).mul(premiumDiscountFactorX18);
}
this wrong total debt is used to get the premium discount factor
function getPremiumDiscountFactor(
Data storage self,
UD60x18 vaultAssetsValueUsdX18,
@audit>> SD59x18 vaultDebtUsdX18
)
internal
view
returns (UD60x18 premiumDiscountFactorX18)
{
@audit>> UD60x18 vaultDebtTvlRatioAbs = vaultDebtUsdX18.abs().intoUD60x18().div(vaultAssetsValueUsdX18);
UD60x18 pdCurveXMinX18 = ud60x18(self.pdCurveXMin);
UD60x18 pdCurveXMaxX18 = ud60x18(self.pdCurveXMax);
@audit>> if (vaultDebtTvlRatioAbs.lte(pdCurveXMinX18)) {
premiumDiscountFactorX18 = UD60x18_UNIT;
return premiumDiscountFactorX18;
}
UD60x18 pdCurveXX18 = vaultDebtTvlRatioAbs.gte(pdCurveXMaxX18) ? pdCurveXMaxX18 : vaultDebtTvlRatioAbs;
UD60x18 pdCurveYMinX18 = ud60x18(self.pdCurveYMin);
UD60x18 pdCurveYMaxX18 = ud60x18(self.pdCurveYMax);
UD60x18 pdCurveZX18 = ud60x18(self.pdCurveZ);
UD60x18 pdCurveYX18 = pdCurveYMinX18.add(
pdCurveYMaxX18.sub(pdCurveYMinX18).mul(
pdCurveXX18.sub(pdCurveXMinX18).div(pdCurveXMaxX18.sub(pdCurveXMinX18)).pow(pdCurveZX18)
)
);
@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
function recalculateVaultsCreditCapacity(uint256[] memory vaultsIds) internal {
--------------------------
(
uint128[] memory updatedConnectedMarketsIdsCache,
SD59x18 vaultTotalRealizedDebtChangeUsdX18,
SD59x18 vaultTotalUnrealizedDebtChangeUsdX18,
UD60x18 vaultTotalUsdcCreditChangeX18,
UD60x18 vaultTotalWethRewardChangeX18
) = _recalculateConnectedMarketsState(self, connectedMarketsIdsCache, true);
if (!vaultTotalRealizedDebtChangeUsdX18.isZero()) {
@audit>> self.marketsRealizedDebtUsd = sd59x18(self.marketsRealizedDebtUsd).add(
vaultTotalRealizedDebtChangeUsdX18
).intoInt256().toInt128();
}
if (!vaultTotalUnrealizedDebtChangeUsdX18.isZero()) {
@audit>> self.marketsUnrealizedDebtUsd = sd59x18(self.marketsUnrealizedDebtUsd).add(
vaultTotalUnrealizedDebtChangeUsdX18
).intoInt256().toInt128();
}
Total debt is obtained form the Total realised and unrealised debt
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.
Vault.recalculateVaultsCreditCapacity(vaultsIds);