Part 2

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

The line 666 in the `rebalanceVaultsAssets` function is incorrect.

Summary

The rebalanceVaultsAssets::L666 is incorrect.

Vulnerability Details

https://github.com/Cyfrin/2025-01-zaros-part-2/blob/main/src/market-making/branches/CreditDelegationBranch.sol#L593

593:function rebalanceVaultsAssets(uint128[2] calldata vaultsIds) external onlyRegisteredSystemKeepers {
// load the storage pointers of the vaults in net credit and net debt
Vault.Data storage inCreditVault = Vault.loadExisting(vaultsIds[0]);
Vault.Data storage inDebtVault = Vault.loadExisting(vaultsIds[1]);
// both vaults must belong to the same engine in order to have their debt
// state rebalanced, as each usd token's debt is isolated
if (inCreditVault.engine != inDebtVault.engine) {
revert Errors.VaultsConnectedToDifferentEngines();
}
// create an in-memory dynamic array in order to call `Vault::recalculateVaultsCreditCapacity`
uint256[] memory vaultsIdsForRecalculation = new uint256[](2);
vaultsIdsForRecalculation[0] = vaultsIds[0];
vaultsIdsForRecalculation[1] = vaultsIds[1];
// recalculate the credit capacity of both vaults
Vault.recalculateVaultsCreditCapacity(vaultsIdsForRecalculation);
// cache the in debt vault & in credit vault unsettled debt
SD59x18 inDebtVaultUnsettledRealizedDebtUsdX18 = inDebtVault.getUnsettledRealizedDebt();
SD59x18 inCreditVaultUnsettledRealizedDebtUsdX18 = inCreditVault.getUnsettledRealizedDebt();
// revert if 1) the vault that is supposed to be in credit is not OR
// 2) the vault that is supposed to be in debt is not
if (
619: inCreditVaultUnsettledRealizedDebtUsdX18.lte(SD59x18_ZERO)
|| inDebtVaultUnsettledRealizedDebtUsdX18.gte(SD59x18_ZERO)
) {
revert Errors.InvalidVaultDebtSettlementRequest();
}
// get debt absolute value
SD59x18 inDebtVaultUnsettledRealizedDebtUsdX18Abs = inDebtVaultUnsettledRealizedDebtUsdX18.abs();
// if debt absolute value > credit, use credit value, else use debt value
629: SD59x18 depositAmountUsdX18 = inCreditVaultUnsettledRealizedDebtUsdX18.gt(
inDebtVaultUnsettledRealizedDebtUsdX18Abs
) ? inDebtVaultUnsettledRealizedDebtUsdX18Abs : inCreditVaultUnsettledRealizedDebtUsdX18;
// loads the dex swap strategy data storage pointer
DexSwapStrategy.Data storage dexSwapStrategy =
DexSwapStrategy.loadExisting(inDebtVault.swapStrategy.usdcDexSwapStrategyId);
// load usdc address
address usdc = MarketMakingEngineConfiguration.load().usdc;
// cache input asset and dex adapter
CalculateSwapContext memory ctx;
ctx.inDebtVaultCollateralAsset = inDebtVault.collateral.asset;
ctx.dexAdapter = dexSwapStrategy.dexAdapter;
// 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
650: 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);
663: dexSwapStrategy.executeSwapExactInputSingle(swapCallData);
// SD59x18 -> uint128 using zaros internal precision
666: 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
679: 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();
// emit an event
emit LogRebalanceVaultsAssets(vaultsIds[0], vaultsIds[1], usdDelta);
}
BaseAdapter.sol
function getExpectedOutput(
address tokenIn,
address tokenOut,
uint256 amountIn
)
public
view
returns (uint256 expectedAmountOut)
{
// fail fast for zero input
if (amountIn == 0) revert Errors.ZeroExpectedSwapOutput();
// get token prices
UD60x18 priceTokenInX18 = IPriceAdapter(swapAssetConfigData[tokenIn].priceAdapter).getPrice();
UD60x18 priceTokenOutX18 = IPriceAdapter(swapAssetConfigData[tokenOut].priceAdapter).getPrice();
// convert input amount from native to internal zaros precision
UD60x18 amountInX18 = Math.convertTokenAmountToUd60x18(swapAssetConfigData[tokenIn].decimals, amountIn);
// calculate the expected amount out in native precision of output token
expectedAmountOut = Math.convertUd60x18ToTokenAmount(
swapAssetConfigData[tokenOut].decimals, amountInX18.mul(priceTokenInX18).div(priceTokenOutX18)
);
// revert when calculated expected output is zero; must revert here
// otherwise the subsequent slippage bps calculation will also
// return a minimum swap output of zero giving away the input tokens
if (expectedAmountOut == 0) revert Errors.ZeroExpectedSwapOutput();
}
DexSwapStrategy.sol
function executeSwapExactInputSingle(
Data storage self,
SwapExactInputSinglePayload memory swapCallData
)
internal
returns (uint256 amountOut)
{
IDexAdapter dexAdapter = IDexAdapter(self.dexAdapter);
return dexAdapter.executeSwapExactInputSingle(swapCallData);
}

In line 666, the usdDelta = depositAmountUsd does not represent the actual USD amount swapped.
The assetInputNative and depositAmountUsd are not related to the dexAdapter.
However, the actual USD amount swapped depends on whether the dexAdapter is Uniswap or Curve.
As a result, depositedUsdc and marketsRealizedDebtUsd is different than it should.

Impact

The depositedUsdc and marketsRealizedDebtUsd of vaults are not correct.
Incorrect accounting results in losses for users.

Recommendations

593:function rebalanceVaultsAssets(uint128[2] calldata vaultsIds) external onlyRegisteredSystemKeepers {
// load the storage pointers of the vaults in net credit and net debt
Vault.Data storage inCreditVault = Vault.loadExisting(vaultsIds[0]);
Vault.Data storage inDebtVault = Vault.loadExisting(vaultsIds[1]);
// both vaults must belong to the same engine in order to have their debt
// state rebalanced, as each usd token's debt is isolated
if (inCreditVault.engine != inDebtVault.engine) {
revert Errors.VaultsConnectedToDifferentEngines();
}
// create an in-memory dynamic array in order to call `Vault::recalculateVaultsCreditCapacity`
uint256[] memory vaultsIdsForRecalculation = new uint256[](2);
vaultsIdsForRecalculation[0] = vaultsIds[0];
vaultsIdsForRecalculation[1] = vaultsIds[1];
// recalculate the credit capacity of both vaults
Vault.recalculateVaultsCreditCapacity(vaultsIdsForRecalculation);
// cache the in debt vault & in credit vault unsettled debt
SD59x18 inDebtVaultUnsettledRealizedDebtUsdX18 = inDebtVault.getUnsettledRealizedDebt();
SD59x18 inCreditVaultUnsettledRealizedDebtUsdX18 = inCreditVault.getUnsettledRealizedDebt();
// revert if 1) the vault that is supposed to be in credit is not OR
// 2) the vault that is supposed to be in debt is not
if (
inCreditVaultUnsettledRealizedDebtUsdX18.lte(SD59x18_ZERO)
|| inDebtVaultUnsettledRealizedDebtUsdX18.gte(SD59x18_ZERO)
) {
revert Errors.InvalidVaultDebtSettlementRequest();
}
// get debt absolute value
SD59x18 inDebtVaultUnsettledRealizedDebtUsdX18Abs = inDebtVaultUnsettledRealizedDebtUsdX18.abs();
// if debt absolute value > credit, use credit value, else use debt value
SD59x18 depositAmountUsdX18 = (inDebtVault.marketsRealizedDebtUsd).gt(
inDebtVault.depositedUsdc
) ? inDebtVault.depositedUsdc : inDebtVault.marketsRealizedDebtUsd;
// loads the dex swap strategy data storage pointer
DexSwapStrategy.Data storage dexSwapStrategy =
DexSwapStrategy.loadExisting(inDebtVault.swapStrategy.usdcDexSwapStrategyId);
// load usdc address
address usdc = MarketMakingEngineConfiguration.load().usdc;
// cache input asset and dex adapter
CalculateSwapContext memory ctx;
ctx.inDebtVaultCollateralAsset = inDebtVault.collateral.asset;
ctx.dexAdapter = dexSwapStrategy.dexAdapter;
// 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);
+ uint256 amountUsd = dexSwapStrategy.executeSwapExactInputSingle(swapCallData);
// SD59x18 -> uint128 using zaros internal precision
- uint128 usdDelta = depositAmountUsdX18.intoUint256().toUint128();
+ uint128 usdDelta = amountUsd.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();
// emit an event
emit LogRebalanceVaultsAssets(vaultsIds[0], vaultsIds[1], usdDelta);
}
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.