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(
uint128[] calldata vaultIds,
uint128[] calldata amountsIn,
uint128[] calldata minAmountsOut
)
external
{
if (vaultIds.length != amountsIn.length) {
revert Errors.ArrayLengthMismatch(vaultIds.length, amountsIn.length);
}
if (amountsIn.length != minAmountsOut.length) {
revert Errors.ArrayLengthMismatch(amountsIn.length, minAmountsOut.length);
}
InitiateSwapContext memory ctx;
Vault.Data storage currentVault = Vault.load(vaultIds[0]);
ctx.initialVaultIndexToken = currentVault.indexToken;
ctx.initialVaultCollateralAsset = currentVault.collateral.asset;
Collateral.Data storage collateral = Collateral.load(ctx.initialVaultCollateralAsset);
collateral.verifyIsEnabled();
MarketMakingEngineConfiguration.Data storage configuration = MarketMakingEngineConfiguration.load();
UsdTokenSwapConfig.Data storage tokenSwapData = UsdTokenSwapConfig.load();
ctx.collateralPriceX18 = currentVault.collateral.getPrice();
ctx.maxExecTime = uint120(tokenSwapData.maxExecutionTime);
ctx.vaultAssetBalance = IERC20(ctx.initialVaultCollateralAsset).balanceOf(ctx.initialVaultIndexToken);
for (uint256 i; i < amountsIn.length; i++) {
if (i != 0) {
currentVault = Vault.load(vaultIds[i]);
if (currentVault.collateral.asset != ctx.initialVaultCollateralAsset) {
revert Errors.VaultsCollateralAssetsMismatch();
}
ctx.vaultAssetBalance = IERC20(ctx.initialVaultCollateralAsset).balanceOf(currentVault.indexToken);
}
@audit>>>> ctx.expectedAssetOut =
getAmountOfAssetOut(vaultIds[i], ud60x18(amountsIn[i]), ctx.collateralPriceX18).intoUint256();
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 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.
Vault.recalculateVaultsCreditCapacity(vaultsIds);