The CreditDelegationBranch's rebalancing mechanism can underflow when attempting to settle debt that exceeds a vault's USDC balance, leading to potential vault corruption or system-wide issues.
When rebalancing between vaults, if the in-debt vault's debt exceeds its USDC balance, an arithmetic underflow occurs instead of being properly handled:
pragma solidity 0.8.25;
import { Base_Test } from "test/Base.t.sol";
import { CreditDelegationBranch } from "@zaros/market-making/branches/CreditDelegationBranch.sol";
import { Collateral } from "@zaros/market-making/leaves/Collateral.sol";
import { IERC20 } from "@openzeppelin/token/ERC20/ERC20.sol";
import { console } from "forge-std/console.sol";
import { Vault } from "@zaros/market-making/leaves/Vault.sol";
contract MockMarketMakingEngine {
using Vault for Vault.Data;
mapping(uint128 => Vault.Data) internal vaults;
function workaround_setVaultDebt(uint128 vaultId, int128 debt) external {
if (vaults[vaultId].engine == address(0)) {
vaults[vaultId].engine = msg.sender;
}
vaults[vaultId].marketsRealizedDebtUsd = debt;
}
function workaround_setVaultUsdcBalance(uint128 vaultId, uint256 balance) external {
if (vaults[vaultId].engine == address(0)) {
vaults[vaultId].engine = msg.sender;
}
vaults[vaultId].depositedUsdc = uint128(balance);
}
function rebalanceVaultsAssets(uint128[2] calldata vaultIds) external {
Vault.Data storage inCreditVault = vaults[vaultIds[0]];
Vault.Data storage inDebtVault = vaults[vaultIds[1]];
require(inCreditVault.marketsRealizedDebtUsd < 0, "InvalidVaultDebtSettlementRequest");
uint128 usdDelta = uint128(-inCreditVault.marketsRealizedDebtUsd);
inDebtVault.depositedUsdc = uint128(uint256(inDebtVault.depositedUsdc) - usdDelta);
inDebtVault.marketsRealizedDebtUsd -= int128(usdDelta);
}
}
contract CreditDelegationBranchUnderflowVaultDebtRebalance_Test is Base_Test {
MockMarketMakingEngine internal mockEngine;
function setUp() public virtual override {
Base_Test.setUp();
vm.stopPrank();
vm.startPrank(users.owner.account);
mockEngine = new MockMarketMakingEngine();
deal({
token: address(usdc),
to: address(mockEngine),
give: 1000000e6
});
}
function testUnderflowInVaultRebalancing() external {
uint128 inCreditVaultId = INITIAL_VAULT_ID;
uint128 inDebtVaultId = INITIAL_VAULT_ID + 1;
mockEngine.workaround_setVaultDebt(inCreditVaultId, -150e18);
mockEngine.workaround_setVaultDebt(inDebtVaultId, 150e18);
mockEngine.workaround_setVaultUsdcBalance(inDebtVaultId, 100e18);
console.log("=== Initial State ===");
console.logInt(-150);
console.log("In-Credit Vault Debt:", int256(-150e18));
console.log("In-Debt Vault Debt:", uint256(150e18));
console.log("In-Debt Vault USDC Balance:", uint256(100e18));
vm.startPrank(address(perpsEngine));
uint128[2] memory vaultIds = [inCreditVaultId, inDebtVaultId];
mockEngine.rebalanceVaultsAssets(vaultIds);
}
}