Summary
A critical implementation flaw in the debt settlement and rebalancing logic causes vaults' debt-credit states to progressively become more imbalanced, potentially leading to protocol instability.
Links to affected code
Vulnerability details
Finding description and impact
The issue stems from incorrect logic in determining vault credit/debt states in CreditDelegationBranch.sol
. According to the documentation and implementation in Vault.sol
, a vault's unsettled realized debt is calculated as realized debt - deposited usdc
.
CreditDelegationBranch.sol:L567~592
.
The core problem lies in how vault states are interpreted:
When unsettledRealizedDebt < 0
, the vault is in credit
When unsettledRealizedDebt > 0
, the vault is in debt
However, the implementation in settleVaultsDebt
and rebalanceVaultsAssets
incorrectly reverses this logic, causing:
Wrong settlement direction for vault debts
Increasing imbalances between realized debt and deposited USDC
Progressive deterioration of vault states
The same logical error exists in rebalanceVaultsAssets
where debt/credit state checks are reversed, further compounding the issue.
As we can see above, in credit valut unsettled realized debt is less than zero and in debt vault unsettled realized debt is bigger than zero.
In fact, Vault.sol#getUnsettledRealizedDebt()
function is as follows.
function getUnsettledRealizedDebt(Data storage self)
internal
view
returns (SD59x18 unsettledRealizedDebtUsdX18)
{
unsettledRealizedDebtUsdX18 =
sd59x18(self.marketsRealizedDebtUsd).add(unary(ud60x18(self.depositedUsdc).intoSD59x18()));
}
From above code snippet, we can see that unsettled realized debt is realized debt - deposited usdc
.
But CreditDelegationBranch.sol#settleVaultsDebt()
is as follows.
function settleVaultsDebt(uint256[] calldata vaultsIds) external onlyRegisteredSystemKeepers {
Vault.recalculateVaultsCreditCapacity(vaultsIds);
SettleVaultDebtContext memory ctx;
ctx.usdc = MarketMakingEngineConfiguration.load().usdc;
Collateral.Data storage usdcCollateralConfig = Collateral.load(ctx.usdc);
for (uint256 i; i < vaultsIds.length; i++) {
Vault.Data storage vault = Vault.loadExisting(vaultsIds[i].toUint128());
-> ctx.vaultUnsettledRealizedDebtUsdX18 = vault.getUnsettledRealizedDebt();
if (ctx.vaultUnsettledRealizedDebtUsdX18.isZero()) continue;
ctx.vaultAsset = vault.collateral.asset;
DexSwapStrategy.Data storage dexSwapStrategy =
DexSwapStrategy.loadExisting(vault.swapStrategy.assetDexSwapStrategyId);
->
L436-> if (ctx.vaultUnsettledRealizedDebtUsdX18.lt(SD59x18_ZERO)) {
ctx.swapAmount = calculateSwapAmount(
dexSwapStrategy.dexAdapter,
ctx.usdc,
ctx.vaultAsset,
usdcCollateralConfig.convertSd59x18ToTokenAmount(ctx.vaultUnsettledRealizedDebtUsdX18.abs())
);
ctx.usdcOut = _convertAssetsToUsdc(
vault.swapStrategy.usdcDexSwapStrategyId,
ctx.vaultAsset,
ctx.swapAmount,
vault.swapStrategy.usdcDexSwapPath,
address(this),
ctx.usdc
);
if (ctx.usdcOut == 0) revert Errors.ZeroOutputTokens();
ctx.usdcOutX18 = usdcCollateralConfig.convertTokenAmountToUd60x18(ctx.usdcOut);
vault.marketsRealizedDebtUsd -= ctx.usdcOutX18.intoUint256().toInt256().toInt128();
UsdTokenSwapConfig.load().usdcAvailableForEngine[vault.engine] += ctx.usdcOutX18.intoUint256();
ctx.assetIn = ctx.vaultAsset;
ctx.assetInAmount = ctx.swapAmount;
ctx.assetOut = ctx.usdc;
ctx.assetOutAmount = ctx.usdcOut;
ctx.settledDebt = ctx.usdcOut.toInt256();
} else {
ctx.usdcIn = calculateSwapAmount(
dexSwapStrategy.dexAdapter,
ctx.vaultAsset,
ctx.usdc,
usdcCollateralConfig.convertSd59x18ToTokenAmount(ctx.vaultUnsettledRealizedDebtUsdX18.abs())
);
ctx.vaultUsdcBalance = usdcCollateralConfig.convertUd60x18ToTokenAmount(ud60x18(vault.depositedUsdc));
ctx.usdcIn = (ctx.usdcIn <= ctx.vaultUsdcBalance) ? ctx.usdcIn : ctx.vaultUsdcBalance;
ctx.assetOutAmount = _convertUsdcToAssets(
vault.swapStrategy.assetDexSwapStrategyId,
ctx.vaultAsset,
ctx.usdcIn,
vault.swapStrategy.assetDexSwapPath,
vault.indexToken,
ctx.usdc
);
if (ctx.assetOutAmount == 0) revert Errors.ZeroOutputTokens();
vault.depositedUsdc -= usdcCollateralConfig.convertTokenAmountToUd60x18(ctx.usdcIn).intoUint128();
ctx.assetIn = ctx.usdc;
ctx.assetInAmount = ctx.usdcIn;
ctx.assetOut = ctx.vaultAsset;
ctx.settledDebt = -ctx.usdcIn.toInt256();
}
emit LogSettleVaultDebt(
vaultsIds[i].toUint128(),
ctx.assetIn,
ctx.assetInAmount,
ctx.assetOut,
ctx.assetOutAmount,
ctx.settledDebt
);
}
}
On L436, vault debt state is checked incorrectly.
So after settlememt vault's imbalance between realized debt and deposited usdc is increased.
For example:
Initial state:
- Realized debt: 30
- Deposited USDC: 40
- Unsettled realized debt: 30 - 40 = -10 (vault is in credit)
After incorrect settlement:
- Realized debt becomes: 30 - 10 = 20
- Deposited USDC remains: 40
- New imbalance: -20 (doubled from initial -10)
This incorrect consideration exist in CreditDelegationBranch.sol#rebalanceVaultsAssets()
.
function rebalanceVaultsAssets(uint128[2] calldata vaultsIds) external onlyRegisteredSystemKeepers {
Vault.Data storage inCreditVault = Vault.loadExisting(vaultsIds[0]);
Vault.Data storage inDebtVault = Vault.loadExisting(vaultsIds[1]);
if (inCreditVault.engine != inDebtVault.engine) {
revert Errors.VaultsConnectedToDifferentEngines();
}
uint256[] memory vaultsIdsForRecalculation = new uint256[](2);
vaultsIdsForRecalculation[0] = vaultsIds[0];
vaultsIdsForRecalculation[1] = vaultsIds[1];
Vault.recalculateVaultsCreditCapacity(vaultsIdsForRecalculation);
SD59x18 inDebtVaultUnsettledRealizedDebtUsdX18 = inDebtVault.getUnsettledRealizedDebt();
SD59x18 inCreditVaultUnsettledRealizedDebtUsdX18 = inCreditVault.getUnsettledRealizedDebt();
if (
-> inCreditVaultUnsettledRealizedDebtUsdX18.lte(SD59x18_ZERO)
-> || inDebtVaultUnsettledRealizedDebtUsdX18.gte(SD59x18_ZERO)
) {
revert Errors.InvalidVaultDebtSettlementRequest();
}
SD59x18 inDebtVaultUnsettledRealizedDebtUsdX18Abs = inDebtVaultUnsettledRealizedDebtUsdX18.abs();
SD59x18 depositAmountUsdX18 = inCreditVaultUnsettledRealizedDebtUsdX18.gt(
inDebtVaultUnsettledRealizedDebtUsdX18Abs
) ? inDebtVaultUnsettledRealizedDebtUsdX18Abs : inCreditVaultUnsettledRealizedDebtUsdX18;
DexSwapStrategy.Data storage dexSwapStrategy =
DexSwapStrategy.loadExisting(inDebtVault.swapStrategy.usdcDexSwapStrategyId);
address usdc = MarketMakingEngineConfiguration.load().usdc;
CalculateSwapContext memory ctx;
ctx.inDebtVaultCollateralAsset = inDebtVault.collateral.asset;
ctx.dexAdapter = dexSwapStrategy.dexAdapter;
uint256 assetInputNative = IDexAdapter(ctx.dexAdapter).getExpectedOutput(
usdc,
ctx.inDebtVaultCollateralAsset,
Collateral.load(usdc).convertSd59x18ToTokenAmount(depositAmountUsdX18)
);
SwapExactInputSinglePayload memory swapCallData = SwapExactInputSinglePayload({
tokenIn: ctx.inDebtVaultCollateralAsset,
tokenOut: usdc,
amountIn: assetInputNative,
recipient: address(this)
});
IERC20(ctx.inDebtVaultCollateralAsset).approve(ctx.dexAdapter, assetInputNative);
dexSwapStrategy.executeSwapExactInputSingle(swapCallData);
uint128 usdDelta = depositAmountUsdX18.intoUint256().toUint128();
inCreditVault.depositedUsdc += usdDelta;
inCreditVault.marketsRealizedDebtUsd += usdDelta.toInt256().toInt128();
inDebtVault.depositedUsdc -= usdDelta;
inDebtVault.marketsRealizedDebtUsd -= usdDelta.toInt256().toInt128();
emit LogRebalanceVaultsAssets(vaultsIds[0], vaultsIds[1], usdDelta);
}
This wrong implementation will ause broken vault's states.
As time goes by, vault's imbalance will increase and protocol will be broken.
Recommended mitigation steps
Modify CreditDelegationBranch.sol#settleVaultsDebt()
function as follows.
function settleVaultsDebt(uint256[] calldata vaultsIds) external onlyRegisteredSystemKeepers {
Vault.recalculateVaultsCreditCapacity(vaultsIds);
SettleVaultDebtContext memory ctx;
ctx.usdc = MarketMakingEngineConfiguration.load().usdc;
Collateral.Data storage usdcCollateralConfig = Collateral.load(ctx.usdc);
for (uint256 i; i < vaultsIds.length; i++) {
Vault.Data storage vault = Vault.loadExisting(vaultsIds[i].toUint128());
ctx.vaultUnsettledRealizedDebtUsdX18 = vault.getUnsettledRealizedDebt();
if (ctx.vaultUnsettledRealizedDebtUsdX18.isZero()) continue;
ctx.vaultAsset = vault.collateral.asset;
DexSwapStrategy.Data storage dexSwapStrategy =
DexSwapStrategy.loadExisting(vault.swapStrategy.assetDexSwapStrategyId);
- if (ctx.vaultUnsettledRealizedDebtUsdX18.lt(SD59x18_ZERO)) {
+ if (ctx.vaultUnsettledRealizedDebtUsdX18.gt(SD59x18_ZERO)) {
ctx.swapAmount = calculateSwapAmount(
dexSwapStrategy.dexAdapter,
ctx.usdc,
ctx.vaultAsset,
usdcCollateralConfig.convertSd59x18ToTokenAmount(ctx.vaultUnsettledRealizedDebtUsdX18.abs())
);
.........................................................
} else {
.........................................................
}
emit LogSettleVaultDebt(
vaultsIds[i].toUint128(),
ctx.assetIn,
ctx.assetInAmount,
ctx.assetOut,
ctx.assetOutAmount,
ctx.settledDebt
);
}
}
Modify CreditDelegationBranch.sol#rebalanceVaultsAssets()
function as follows.
function rebalanceVaultsAssets(uint128[2] calldata vaultsIds) external onlyRegisteredSystemKeepers {
Vault.Data storage inCreditVault = Vault.loadExisting(vaultsIds[0]);
Vault.Data storage inDebtVault = Vault.loadExisting(vaultsIds[1]);
if (inCreditVault.engine != inDebtVault.engine) {
revert Errors.VaultsConnectedToDifferentEngines();
}
uint256[] memory vaultsIdsForRecalculation = new uint256[](2);
vaultsIdsForRecalculation[0] = vaultsIds[0];
vaultsIdsForRecalculation[1] = vaultsIds[1];
Vault.recalculateVaultsCreditCapacity(vaultsIdsForRecalculation);
SD59x18 inDebtVaultUnsettledRealizedDebtUsdX18 = inDebtVault.getUnsettledRealizedDebt();
SD59x18 inCreditVaultUnsettledRealizedDebtUsdX18 = inCreditVault.getUnsettledRealizedDebt();
if (
- inCreditVaultUnsettledRealizedDebtUsdX18.lte(SD59x18_ZERO)
- || inDebtVaultUnsettledRealizedDebtUsdX18.gte(SD59x18_ZERO)
+ inCreditVaultUnsettledRealizedDebtUsdX18.gte(SD59x18_ZERO)
+ || inDebtVaultUnsettledRealizedDebtUsdX18.lte(SD59x18_ZERO)
) {
revert Errors.InvalidVaultDebtSettlementRequest();
}
-
- SD59x18 inDebtVaultUnsettledRealizedDebtUsdX18Abs = inDebtVaultUnsettledRealizedDebtUsdX18.abs();
+
+ SD59x18 inCreditVaultUnsettledRealizedDebtUsdX18Abs = inCreditVaultUnsettledRealizedDebtUsdX18.abs();
-
- SD59x18 depositAmountUsdX18 = inCreditVaultUnsettledRealizedDebtUsdX18.gt(
- inDebtVaultUnsettledRealizedDebtUsdX18Abs
- ) ? inDebtVaultUnsettledRealizedDebtUsdX18Abs : inCreditVaultUnsettledRealizedDebtUsdX18;
+
+ SD59x18 depositAmountUsdX18 = inDebtVaultUnsettledRealizedDebtUsdX18.gt(
+ inCreditVaultUnsettledRealizedDebtUsdX18Abs
+ ) ? inCreditVaultUnsettledRealizedDebtUsdX18Abs : inDebtVaultUnsettledRealizedDebtUsdX18;
..........................................................
}