Part 2

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

CreditDelegationBranch Rebalancing Underflows When Debt Exceeds USDC Balance

Summary

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.

Vulnerability Details

When rebalancing between vaults, if the in-debt vault's debt exceeds its USDC balance, an arithmetic underflow occurs instead of being properly handled:

function rebalanceVaultsAssets(uint128[2] calldata vaultIds) external {
// ... validation checks ...
// This underflows if usdDelta > depositedUsdc
inDebtVault.depositedUsdc = uint128(uint256(inDebtVault.depositedUsdc) - usdDelta);
inDebtVault.marketsRealizedDebtUsd -= int128(usdDelta);
}

Proof of Concept

// SPDX-License-Identifier: MIT
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);
}
// Add rebalancing function to mock
function rebalanceVaultsAssets(uint128[2] calldata vaultIds) external {
Vault.Data storage inCreditVault = vaults[vaultIds[0]];
Vault.Data storage inDebtVault = vaults[vaultIds[1]];
// Replicate the core rebalancing logic
require(inCreditVault.marketsRealizedDebtUsd < 0, "InvalidVaultDebtSettlementRequest");
uint128 usdDelta = uint128(-inCreditVault.marketsRealizedDebtUsd);
// This should trigger the underflow if not properly checked
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();
// Configure initial liquidity for mock
deal({
token: address(usdc),
to: address(mockEngine),
give: 1000000e6 // $1M USDC
});
}
function testUnderflowInVaultRebalancing() external {
uint128 inCreditVaultId = INITIAL_VAULT_ID;
uint128 inDebtVaultId = INITIAL_VAULT_ID + 1;
// Ensure first vault has negative debt (is in credit)
mockEngine.workaround_setVaultDebt(inCreditVaultId, -150e18); // -$150 (in credit)
// Configure in-debt vault with debt > USDC balance
mockEngine.workaround_setVaultDebt(inDebtVaultId, 150e18); // $150 debt
mockEngine.workaround_setVaultUsdcBalance(inDebtVaultId, 100e18); // $100 USDC
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];
// Now call our mock instead of the real contract
mockEngine.rebalanceVaultsAssets(vaultIds);
}
}

Test output shows arithmetic underflow:

=== Initial State ===
-150
In-Credit Vault Debt: -150000000000000000000
In-Debt Vault Debt: 150000000000000000000
In-Debt Vault USDC Balance: 100000000000000000000
[FAIL] Revert: panic: arithmetic underflow or overflow (0x11)

Impact

  • Vault state corruption due to underflow

  • Potential system-wide issues if corrupted values propagate

  • DoS of rebalancing functionality

Recommended Mitigation

Add balance checks and handle partial rebalancing:

function rebalanceVaultsAssets(uint128[2] calldata vaultIds) external {
// ... existing validation ...
// Get actual available balance
uint256 availableBalance = inDebtVault.depositedUsdc;
// Use min of desired amount and available balance
uint256 actualTransferAmount = usdDelta > availableBalance ? availableBalance : usdDelta;
// Update with safe amount
inDebtVault.depositedUsdc -= uint128(actualTransferAmount);
inDebtVault.marketsRealizedDebtUsd -= int128(actualTransferAmount);
emit VaultRebalanced(vaultIds[0], vaultIds[1], actualTransferAmount);
}
Updates

Lead Judging Commences

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

depositAmountUsdX18 calculation inside CreditDelegatioNBranch::rebalanceVaultsAssets is wrong

Support

FAQs

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