Summary
Wrong calculation of usdDelta in CreditDelegationBranch.sol#rebalanceVaultsAssets() will cause vaults' state to be different from real usd balance.
Vulnerability Details
CreditDelegationBranch.sol#rebalanceVaultsAssets() function is 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]);
...
646 uint256 assetInputNative = IDexAdapter(ctx.dexAdapter).getExpectedOutput(
usdc,
ctx.inDebtVaultCollateralAsset,
Collateral.load(usdc).convertSd59x18ToTokenAmount(depositAmountUsdX18)
);
654 SwapExactInputSinglePayload memory swapCallData = SwapExactInputSinglePayload({
tokenIn: ctx.inDebtVaultCollateralAsset,
tokenOut: usdc,
amountIn: assetInputNative,
recipient: address(this)
});
IERC20(ctx.inDebtVaultCollateralAsset).approve(ctx.dexAdapter, assetInputNative);
663 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);
}
On L646, assetInputNative is calculated as input amount for depositAmountUsdX18 as output amount through following code.
File: BaseAdapter.sol
function getExpectedOutput(
address tokenIn,
address tokenOut,
uint256 amountIn
)
public
view
returns (uint256 expectedAmountOut)
{
if (amountIn == 0) revert Errors.ZeroExpectedSwapOutput();
UD60x18 priceTokenInX18 = IPriceAdapter(swapAssetConfigData[tokenIn].priceAdapter).getPrice();
UD60x18 priceTokenOutX18 = IPriceAdapter(swapAssetConfigData[tokenOut].priceAdapter).getPrice();
UD60x18 amountInX18 = Math.convertTokenAmountToUd60x18(swapAssetConfigData[tokenIn].decimals, amountIn);
expectedAmountOut = Math.convertUd60x18ToTokenAmount(
swapAssetConfigData[tokenOut].decimals, amountInX18.mul(priceTokenInX18).div(priceTokenOutX18)
);
if (expectedAmountOut == 0) revert Errors.ZeroExpectedSwapOutput();
}
As we can see above, assetInputNative is calculated from prices of asset and usdc.
And then on L654, swapCallData is configured with assetInputNative as input amount.
L663 swaps assets to usdc but its return amount is not used.
So usdDelta is same as depositAmountUsd even though asset is swapped through exact input mode.
This is wrong.
The swapped output usdc amount is different from usdDelta.
This vulnerability causes vault's states such as depositedUsdc and marketsRealizedDebtUsd to be different from real balance.
This problem will cause protocol's broken state.
Impact
This vulnerability causes vault's states such as depositedUsdc and marketsRealizedDebtUsd to be different from real balance.
This problem will cause protocol's broken state.
Tools Used
Manual review
Recommendations
There can be two ways to mitigate this problem.
Modify code so that usdDelta is calculated from return value of swap 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]);
...
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);
++ uint256 amountOut = dexSwapStrategy.executeSwapExactInputSingle(swapCallData);
-- uint128 usdDelta = depositAmountUsdX18.intoUint256().toUint128();
++ uint128 usdDelta = Collateral.load(usdc).convertTokenAmountToSd59x18(amountOut);
inCreditVault.depositedUsdc += usdDelta;
inCreditVault.marketsRealizedDebtUsd += usdDelta.toInt256().toInt128();
inDebtVault.depositedUsdc -= usdDelta;
inDebtVault.marketsRealizedDebtUsd -= usdDelta.toInt256().toInt128();
emit LogRebalanceVaultsAssets(vaultsIds[0], vaultsIds[1], usdDelta);
We can add logic so that assetInputNative is calculated though exact output mode.