Part 2

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

Negative Credit Capacity Handling Causes Complete Vault Lockout When Underwater

Summary

The getVaultCreditCapacity function in VaultRouterBranch.sol fails to properly handle underwater vaults where debt exceeds assets. When calculating negative credit capacity, the conversion from signed to unsigned values in convertSd59x18ToTokenAmount reverts, causing a complete system lockout of critical vault functions including withdrawals.

Vulnerability Details

When a vault's debt exceeds its assets, credit capacity becomes negative. The system fails to handle this scenario in the following sequence:

function getVaultCreditCapacity(uint128 vaultId) public view returns (uint256) {
// Calculate total assets minus debt (can be negative)
SD59x18 totalAssetsMinusVaultDebtX18 = totalAssetsX18.sub(vaultDebtInAssetsX18);
// Attempts to convert negative value to unsigned - reverts
return vault.collateral.convertSd59x18ToTokenAmount(totalAssetsMinusVaultDebtX18);
}

The core issue occurs in Math.sol:

function convertSd59x18ToTokenAmount(uint8 decimals, SD59x18 amountX18) internal pure returns (uint256) {
// Always reverts when amountX18 is negative
return amountX18.intoUint256();
}

This causes critical vault functions to become completely unusable once a vault becomes underwater, as they all depend on getVaultCreditCapacity.

Impact

  1. Sudden System Lockout

    • When debt exceeds assets, all redemptions become impossible

    • No partial withdrawals allowed even if assets remain

    • Users completely locked out of withdrawing any funds

  2. Core Function Failure

    • getIndexTokenSwapRate fails due to credit capacity revert

    • getVaultAssetSwapRate fails due to credit capacity revert

    • redeem() becomes completely unusable

Proof of Concept

The following test demonstrates how vault functions fail completely when underwater:

function test_UnderwaterVaultBehavior() public {
UD60x18 price = ud60x18(2000e18); // $2000 per ETH
UD60x18 assets = ud60x18(100e18); // 100 ETH = $200,000
// Case 1: Healthy vault - $190,000 debt
SD59x18 debt = sd59x18(190_000e18);
ethVault.setMockState(assets, debt);
uint256 capacity = getVaultCreditCapacity(vaultId);
// Returns positive credit capacity, system functions normally
// Case 2: Underwater vault - $210,000 debt
debt = sd59x18(210_000e18);
ethVault.setMockState(assets, debt);
// Complete system lockout - all functions revert
vm.expectRevert();
getVaultCreditCapacity(vaultId);
// Demonstrate critical function failures
vm.expectRevert();
vaultRouter.redeem(vaultId, requestId, minAssets);
}

Tools Used

Manual Analysis, Foundry

Recommendations

  1. Modify convertSd59x18ToTokenAmount to properly handle negative values:

function convertSd59x18ToTokenAmount(uint8 decimals, SD59x18 amountX18)
internal pure returns (int256)
{
if (Constants.SYSTEM_DECIMALS == decimals) {
return amountX18.intoInt256();
}
return amountX18.intoInt256() / int256(10 ** (Constants.SYSTEM_DECIMALS - decimals));
}
Updates

Lead Judging Commences

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

When totalAssetsMinusVaultDebtX18 is negative `convertSd59x18ToTokenAmount` reverts, making `getVaultCreditCapacity` revert, making a couple of core functions revert

Support

FAQs

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