Part 2

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

Incorrect Market Weight Distribution in `Vault.updateVaultAndCreditDelegationWeight()`.

Summary

Vault.updateVaultAndCreditDelegationWeight() updates incorrect creditDelegation.weight to connected markets, as a result that markets'
totalDelegatedCreditUsd will be overestimated, therefore affecting CreditDelegationBranch.withdrawUsdTokenFromMarket() and CreditDelegationBranch.getAdjustedProfitForMarketId().

Vulnerability Details

As the name implies Vault.recalculateVaultsCreditCapacity() is a function responsible to update markets connected to a specific vault. It function performs four main operations:

  1. Calls updateVaultAndCreditDelegationWeight() to update each market's creditDelegation.weight and the vault's totalCreditDelegationWeight.

  2. Calls _recalculateConnectedMarketsState() to compute changes to various vault state parameters, such as realized/unrealized debt and USDC credit.

  3. Updates vault storage based on the calculations from step 2.

  4. Calls _updateCreditDelegations() to update the totalDelegatedCreditUsd for each market.

The problem lies in updateVaultAndCreditDelegationWeight(), which incorrectly assigns the same value (totalCreditDelegationWeight) to all connected markets' creditDelegation.weight. (See code snippet below)

function updateVaultAndCreditDelegationWeight(
Data storage self,
uint128[] memory connectedMarketsIdsCache
)
internal
{
// cache the connected markets length
uint256 connectedMarketsConfigLength = self.connectedMarkets.length;
// loads the connected markets storage pointer by taking the last configured market ids uint set
EnumerableSet.UintSet storage connectedMarkets = self.connectedMarkets[connectedMarketsConfigLength - 1];
// get the total of shares
uint128 newWeight = uint128(IERC4626(self.indexToken).totalAssets());
for (uint256 i; i < connectedMarketsIdsCache.length; i++) {
// load the credit delegation to the given market id
CreditDelegation.Data storage creditDelegation =
CreditDelegation.load(self.id, connectedMarkets.at(i).toUint128());
// update the credit delegation weight
creditDelegation.weight = newWeight;
}
// update the vault weight
self.totalCreditDelegationWeight = newWeight;
}

Later in the code, when Vault._updateCreditDelegations() is called, it calculates the share of credit that each market receives from the vault (see code below). However, as we demonstrated earlier, during updateVaultAndCreditDelegationWeight(), all markets' creditDelegation.weight values are set to totalCreditDelegationWeight. This means that creditDelegationShareX18 will always evaluate to 1 (or 1e18 in Zaros' internal precision). As a result, each market will receive the full amount of credit that the vault has available for delegation, leading to an overestimation of each market’s totalDelegatedCreditUsd.

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;

Check the following POC (Move the test contents to the test/ folder and run forge build && forge test --match-test POC). Note how at the end of the testPOC test, the total credit delegated to all markets combined is bigger than that vault's total credit capacity.

// 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";
import { UD60x18 } from "@prb-math/UD60x18.sol";
import "@zaros/market-making/leaves/Vault.sol";
contract POC 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 {
// Vault setup
uint128 vaultId = 8;
VaultConfig memory inCreditVaultConfig = getFuzzVaultConfig(vaultId);
changePrank({ msgSender: users.owner.account });
marketMakingEngine.setVaultEngine(inCreditVaultConfig.vaultId, address(1));
deal({
token: address(inCreditVaultConfig.asset),
to: address(inCreditVaultConfig.indexToken),
give: 1e18
});
// Updating vault capacity
int256 totalVaultCapacity = 2e18;
vm.expectEmit();
emit Vault.LogUpdateVaultCreditCapacity(vaultId, 0, 0, 0, 0, totalVaultCapacity);
marketMakingEngine.updateVaultCreditCapacity(vaultId);
uint128[] memory connectedMarkets = marketMakingEngine.workaround_Vault_getConnectedMarkets(1);
// Iterate over all connected markets and sum the amounts of credit delegated to each of them.
uint256 totalCreditDelegatedToMarkets;
for (uint256 i; i < connectedMarkets.length; i++) {
uint256 creditDelegatedToMarket = marketMakingEngine.workaround_getTotalDelegatedCreditUsd(connectedMarkets[i]).intoUint256();
totalCreditDelegatedToMarkets += creditDelegatedToMarket;
}
// Assert that the total amount of credit to markets is bigger the vault's total credit capacity!
assertGt(totalCreditDelegatedToMarkets, uint256(totalVaultCapacity));
}
}

This leads to over-delegation of credit, meaning the vault does not have enough funds to cover all markets' delegated credit. Additionally, the inflated totalDelegatedCreditUsd affects several calculations in CreditDelegationBranch (See Impact section).

Impact

Markets will receive more delegated credit than expected because the totalDelegatedCreditUsd value for each market will be overestimated. This overestimation impacts various functions within the CreditDelegationBranch, such as:

  • CreditDelegationBranch.withdrawUsdTokenFromMarket(): The amount that can be withdrawn is determined by the market's totalDelegatedCreditUsd. If this value is overestimated, more USDZ will be withdrawn than should be allowed.

  • CreditDelegationBranch.getAdjustedProfitForMarketId(): The adjusted profit calculation relies on the auto-deleverage factor, which in turn depends on the market's totalDelegatedCreditUsd. Overestimating this value affects the adjusted profit of positions in that market.

Tools Used

Manual Review

Recommended Mitigation

Consider modifying Vault.updateVaultAndCreditDelegationWeight() to ensure it correctly sets the weight values for each market connected to a vault.

Updates

Lead Judging Commences

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

Market Credit Delegation Weights Are Incorrectly Distributed

Support

FAQs

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