Part 2

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

Improper Debt Accounting Due to Unvalidated DEX Swap Outputs in Credit Rebalancing

Summary

The credit rebalancing mechanism in function CreditDelegationBranch.rebalanceVaultsAssets commits a fundamental accounting error by using the precomputed depositAmountUsdX18 value rather than the actual return value of executeSwapExactInputSingle when updating vault debt records. This violates core accounting invariants by:

  1. Ignoring On-Chain Execution Results - The swap's real output (returned as uint256 amountOut) is discarded in favor of theoretical calculations

  2. Creating Phantom Assets - Vault balances are credited with USDC that never materialized in the protocol's reserves

This enables gradual erosion of protocol solvency as vaults develop increasing discrepancies between reported USDC balances and actual holdings, ultimately risking mass unrecoverable withdrawals when liabilities exceed real assets.

Vulnerability Details

The vulnerability occurs in the credit rebalancing process CreditDelegationBranch.rebalanceVaultsAssets (CreditDelegationBranch.sol#L663-L666) when converting collateral assets to USDC. The code currently uses the expected swap amount depositAmountUsdX18 rather than the actual received amount from the DEX swap to update vault accounting records. This creates a discrepancy between real asset movements and internal bookkeeping.

// CreditDelegationBranch.rebalanceVaultsAssets()
// get collateral asset amount in native precision of ctx.inDebtVaultCollateralAsset
uint256 assetInputNative = IDexAdapter(ctx.dexAdapter).getExpectedOutput(
usdc,
ctx.inDebtVaultCollateralAsset,
// convert usdc input to native precision
@> Collateral.load(usdc).convertSd59x18ToTokenAmount(depositAmountUsdX18)
);
// prepare the data for executing the swap asset -> usdc
SwapExactInputSinglePayload memory swapCallData = SwapExactInputSinglePayload({
tokenIn: ctx.inDebtVaultCollateralAsset,
tokenOut: usdc,
amountIn: assetInputNative,
@> recipient: address(this) // deposit the usdc to the market making engine proxy
});
// approve the collateral token to the dex adapter and swap assets for USDC
IERC20(ctx.inDebtVaultCollateralAsset).approve(ctx.dexAdapter, assetInputNative);
@> dexSwapStrategy.executeSwapExactInputSingle(swapCallData);
// SD59x18 -> uint128 using zaros internal precision
@> uint128 usdDelta = depositAmountUsdX18.intoUint256().toUint128();
// important considerations:
// 1) all subsequent storge updates must use zaros internal precision
// 2) code implicitly assumes that 1 USD = 1 USDC
//
// deposits the USDC to the in-credit vault
inCreditVault.depositedUsdc += usdDelta;
// increase the in-credit vault's share of the markets realized debt
// as it has received the USDC and needs to settle it in the future
inCreditVault.marketsRealizedDebtUsd += usdDelta.toInt256().toInt128();
// withdraws the USDC from the in-debt vault
inDebtVault.depositedUsdc -= usdDelta;
// decrease the in-debt vault's share of the markets realized debt
// as it has transferred USDC to the in-credit vault
inDebtVault.marketsRealizedDebtUsd -= usdDelta.toInt256().toInt128();
// DexSwapStrategy.executeSwapExactInputSingle()
/// @notice Executes a swap with the given calldata on the configured router.
/// @param self The SwapRouter data storage.
/// @param swapCallData The calldata to perform the swap.
@> /// @return amountOut The result of the swap execution.
function executeSwapExactInputSingle(
Data storage self,
SwapExactInputSinglePayload memory swapCallData
)
internal
returns (uint256 amountOut)
{
IDexAdapter dexAdapter = IDexAdapter(self.dexAdapter);
@> return dexAdapter.executeSwapExactInputSingle(swapCallData);
}

This mismatch allows for gradual accumulation of accounting errors due to:

  • Slippage in DEX swaps

  • Fee differences between expected and actual rates

  • Partial fills or failed swaps being treated as successful

The protocol assumes perfect swap execution by using pre-calculated values rather than verifying on-chain results, violating the "checks-effects-interactions" pattern and creating solvency risks.

Impact

This accounting mismatch creates systemic risk to protocol solvency through:

  1. Vault Under-collateralization
    Persistent negative slippage causes vaults to report more USDC than actually held, creating artificial liquidity that could lead to unrecoverable withdrawals

  2. Protocol-Wide Insolvency Risk
    Accumulated discrepancies between real and reported balances may render the system unable to honor redemption requests during market stress

  3. Arbitrage Opportunities
    Malicious actors could exploit the slippage gap by frontrunning rebalancing swaps, extracting value from vault inaccuracies

  4. Broken Economic Assumptions
    The core credit delegation mechanism becomes unreliable as vault balances diverge from on-chain asset holdings, undermining the entire market-making system's integrity

This constitutes a high-severity vulnerability as it directly impacts the protocol's ability to maintain accurate accounting of user funds - a critical requirement for any DeFi lending/credit system.

Tools Used

Manual Review

Recommendations

Implement the following changes to align accounting with actual swap results:

1.Use Actual Swap Output
Modify CreditDelegationBranch.sol#L663-L666 to:

uint256 usdcReceived = dexSwapStrategy.executeSwapExactInputSingle(swapCallData);
uint128 usdDelta = usdcReceived.toUint128();

2.Add Slippage Protection
Implement minimum output checks using oracle prices to prevent unfavorable swaps:

uint256 minAmountOut = calculateMinimumOutput(assetInputNative);
require(usdcReceived >= minAmountOut, "Insufficient output");

These changes ensure protocol accounting remains grounded in real asset movements while adding critical safeguards against market volatility and execution risks.

Updates

Lead Judging Commences

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

`CreditDelegationBranch::rebalanceVaultsAssets` doesn't take DEX swap slippage into consideration when swapping debt vault's collateral asset to credit vault's usdc

he rebalanceVaultsAssets function in CreditDelegationBranch.sol updates vault accounting using the pre-swap USD value (usdDelta) rather than the actual post-swap USDC amount received. This means slippage is not accounted for, causing accounting misalignment - if there's negative slippage, the credit vault gets credited more USDC than actually received; if there's positive slippage, it gets credited less.

Support

FAQs

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