Part 2

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

Should distribute debt when a market's unrealized debt and realized debt are both 0

Summary

Should distribute debt when a market's unrealized debt and realized debt are both 0.

Vulnerability Details

In recalculateVaultsCreditCapacity(), protocol will distribute market debt to vault.

Vault::_recalculateConnectedMarketsState():

// first we cache the market's unrealized and realized debt
ctx.marketUnrealizedDebtUsdX18 = market.getUnrealizedDebtUsd();
ctx.marketRealizedDebtUsdX18 = market.getRealizedDebtUsd();
// if market has debt distribute it
if (!ctx.marketUnrealizedDebtUsdX18.isZero() || !ctx.marketRealizedDebtUsdX18.isZero()) {
// distribute the market's debt to its connected vaults
market.distributeDebtToVaults(ctx.marketUnrealizedDebtUsdX18, ctx.marketRealizedDebtUsdX18);
}

In distributeDebtToVaults(), the market's storage value realizedDebtUsdPerVaultShare and unrealizedDebtUsdPerVaultShare are updated.

Market::distributeDebtToVaults():

// update storage values
self.realizedDebtUsdPerVaultShare = newRealizedDebtUsdX18.div(totalVaultSharesX18).intoInt256().toInt128();
self.unrealizedDebtUsdPerVaultShare = newUnrealizedDebtUsdX18.div(totalVaultSharesX18).intoInt256().toInt128();

The problem is that only when neither market's unrealized debt or realized debt is 0, the storage values are updated, as protocol assumes when both the debt values are 0, there is no debt update since the last time. However, this is a false assumption, it's possible that the market's realized debt changes from non-zero to zero (currrently in v1, the unrealized debt is alway 0).

Assuming:

  1. Perps Engine calls to deposit 1 WETH (price is 2000u) credit to Market Making Engine, the market's realized debt is 2000e18;

  2. Later Perps Engine calls to deposit 2000 UsdToken credit to Market Making Engine, then the market's realzied debt becomes 0;

  3. It's necessary to distribute debt as there is debt updates (from 2000e18 to 0).

Please run the coded POC:

  • Add a helper function in **MarketMakingEngineConfigurationBranch **for trancking market's debt (also configure the selector when deploys RootProxy):

function getMarketRealizedDebt(uint128 marketId) external view returns (int256) {
Market.Data storage market = Market.load(marketId);
SD59x18 realizedDebtUsdX18 = market.getRealizedDebtUsd();
return SD59x18.unwrap(realizedDebtUsdX18);
}
  • Run test case testAudit_MarketDebt below to see how market's realized debt changes from non-zero to zero:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import { Test, console } from "forge-std/Test.sol";
import "./Base.t.sol";
import { Errors } from "@zaros/utils/Errors.sol";
import { Vault } from "@zaros/market-making/leaves/Vault.sol";
import { Collateral } from "@zaros/market-making/leaves/Collateral.sol";
contract AuditTest is Base_Test {
address owner = makeAddr("Owner");
address feeRecipient = makeAddr("FeeRecipient");
function setUp() public override {
// Perps Engine Set Up
bool isTestnet = false;
address[] memory branches = deployPerpsEngineBranches(isTestnet);
bytes4[][] memory branchesSelectors = getPerpsEngineBranchesSelectors(isTestnet);
RootProxy.BranchUpgrade[] memory branchUpgrades =
getBranchUpgrades(branches, branchesSelectors, RootProxy.BranchUpgradeAction.Add);
address[] memory initializables = getInitializables(branches);
bytes[] memory initializePayloads = getInitializePayloads(owner);
branchUpgrades = deployPerpsEngineHarnesses(branchUpgrades);
RootProxy.InitParams memory initParams = RootProxy.InitParams({
initBranches: branchUpgrades,
initializables: initializables,
initializePayloads: initializePayloads
});
perpsEngine = IPerpsEngine(address(new MockEngine(initParams)));
// Market Making Engine Set Up
address[] memory mmBranches = deployMarketMakingEngineBranches();
bytes4[][] memory mmBranchesSelectors = getMarketMakerBranchesSelectors();
RootProxy.BranchUpgrade[] memory mmBranchUpgrades =
getBranchUpgrades(mmBranches, mmBranchesSelectors, RootProxy.BranchUpgradeAction.Add);
RootProxy.BranchUpgrade[] memory mmbranchUpgrades = new RootProxy.BranchUpgrade[](mmBranchUpgrades.length);
for (uint256 i; i < mmbranchUpgrades.length; i++) {
mmbranchUpgrades[i] = mmBranchUpgrades[i];
}
initializables = getInitializables(mmBranches);
initializePayloads = getInitializePayloads(owner);
RootProxy.InitParams memory mmEngineInitParams = RootProxy.InitParams({
initBranches: mmbranchUpgrades,
initializables: initializables,
initializePayloads: initializePayloads
});
marketMakingEngine = IMarketMakingEngine(address(new MarketMakingEngine(mmEngineInitParams)));
vm.startPrank(owner);
// Configure Collaterals
uint256[2] memory marginCollateralIdsRange;
marginCollateralIdsRange[0] = INITIAL_MARGIN_COLLATERAL_ID;
marginCollateralIdsRange[1] = FINAL_MARGIN_COLLATERAL_ID;
mockSequencerUptimeFeed = address(new MockSequencerUptimeFeed(0));
configureMarginCollaterals(
IPerpsEngine(perpsEngine), marginCollateralIdsRange, true, mockSequencerUptimeFeed, owner
);
usdc = MockERC20(marginCollaterals[USDC_MARGIN_COLLATERAL_ID].marginCollateralAddress);
usdToken = MockUsdToken(marginCollaterals[USD_TOKEN_MARGIN_COLLATERAL_ID].marginCollateralAddress);
weEth = MockERC20(marginCollaterals[WEETH_MARGIN_COLLATERAL_ID].marginCollateralAddress);
wstEth = MockERC20(marginCollaterals[WSTETH_MARGIN_COLLATERAL_ID].marginCollateralAddress);
wEth = MockERC20(marginCollaterals[WETH_MARGIN_COLLATERAL_ID].marginCollateralAddress);
wBtc = MockERC20(marginCollaterals[WBTC_MARGIN_COLLATERAL_ID].marginCollateralAddress);
marketMakingEngine.configureCollateral(
address(usdc),
marginCollaterals[USDC_MARGIN_COLLATERAL_ID].priceAdapter,
MOCK_PERP_CREDIT_CONFIG_DEBT_CREDIT_RATIO,
true,
marginCollaterals[USDC_MARGIN_COLLATERAL_ID].tokenDecimals
);
marketMakingEngine.configureCollateral(
address(usdToken),
marginCollaterals[USD_TOKEN_MARGIN_COLLATERAL_ID].priceAdapter,
MOCK_PERP_CREDIT_CONFIG_DEBT_CREDIT_RATIO,
true,
marginCollaterals[USD_TOKEN_MARGIN_COLLATERAL_ID].tokenDecimals
);
marketMakingEngine.configureCollateral(
address(wEth),
marginCollaterals[WETH_MARGIN_COLLATERAL_ID].priceAdapter,
MOCK_PERP_CREDIT_CONFIG_DEBT_CREDIT_RATIO,
true,
marginCollaterals[WETH_MARGIN_COLLATERAL_ID].tokenDecimals
);
// Set the wETH address
marketMakingEngine.setWeth(address(wEth));
// Configure Engine
marketMakingEngine.configureEngine(address(perpsEngine), address(usdToken), true);
// Configure Fee Recipient
marketMakingEngine.configureVaultDepositAndRedeemFeeRecipient(feeRecipient);
// Create Zlp Vaults
uint256[2] memory vaultsIdsRange;
vaultsIdsRange[0] = INITIAL_VAULT_ID;
vaultsIdsRange[1] = FINAL_VAULT_ID;
setupVaultsConfig();
createZlpVaults(address(marketMakingEngine), owner, vaultsIdsRange);
// Setup Perp Markets
bool isTest = true;
setupPerpMarketsCreditConfig(isTest, address(perpsEngine), address(usdToken));
vm.stopPrank();
}
function testAudit_MarketDebt() public {
vm.startPrank(owner);
// Create WETH Vault
uint128 vaultId = WETH_CORE_VAULT_ID;
address indexToken = address(zlpVaults[vaultsConfig[vaultId].asset][vaultsConfig[vaultId].vaultType]);
ZlpVault zlpVault = ZlpVault(indexToken);
Collateral.Data memory collateral = Collateral.Data({
creditRatio: vaultsConfig[vaultId].creditRatio,
priceAdapter: vaultsConfig[vaultId].priceAdapter,
asset: vaultsConfig[vaultId].asset,
isEnabled: vaultsConfig[vaultId].isEnabled,
decimals: vaultsConfig[vaultId].decimals
});
Vault.CreateParams memory vaultCreatParams = Vault.CreateParams({
depositFee: vaultsConfig[vaultId].depositFee,
redeemFee: vaultsConfig[vaultId].redeemFee,
vaultId: vaultsConfig[vaultId].vaultId,
depositCap: vaultsConfig[vaultId].depositCap,
withdrawalDelay: vaultsConfig[vaultId].withdrawalDelay,
indexToken: indexToken,
engine: address(perpsEngine),
collateral: collateral
});
marketMakingEngine.createVault(vaultCreatParams);
Vault.UpdateParams memory vaultUpdateParams = Vault.UpdateParams({
vaultId: vaultId,
depositCap: vaultCreatParams.depositCap,
withdrawalDelay: vaultCreatParams.withdrawalDelay,
isLive: true,
lockedCreditRatio: 0
});
marketMakingEngine.updateVaultConfiguration(vaultUpdateParams);
// Deposit into vault
uint256 depositAmount = 2e18;
wEth.mint(owner, depositAmount);
wEth.approve(address(marketMakingEngine), depositAmount);
marketMakingEngine.deposit(vaultsConfig[vaultId].vaultId, uint128(depositAmount), 0, "", false);
// Configure Market
uint128 marketCreditConfigId = ETH_PERP_MARKET_CREDIT_CONFIG_ID;
uint128 marketId = perpMarketsCreditConfig[marketCreditConfigId].marketId;
marketMakingEngine.configureMarket(
address(perpsEngine),
perpMarketsCreditConfig[marketCreditConfigId].marketId,
perpMarketsCreditConfig[marketCreditConfigId].autoDeleverageStartThreshold,
perpMarketsCreditConfig[marketCreditConfigId].autoDeleverageEndThreshold,
perpMarketsCreditConfig[marketCreditConfigId].autoDeleverageExpoentZ
);
marketMakingEngine.unpauseMarket(marketId);
// Connect Vault and Market
uint256[] memory marketIds = new uint256[](1);
marketIds[0] = marketId;
uint256[] memory vaultIds = new uint256[](1);
vaultIds[0] = vaultsConfig[vaultId].vaultId;
marketMakingEngine.connectVaultsAndMarkets(marketIds, vaultIds);
marketMakingEngine.updateVaultCreditCapacity(vaultsConfig[vaultId].vaultId);
vm.stopPrank();
// Deposit Credit from Perps Engine to increase debt
vm.startPrank(address(perpsEngine));
wEth.mint(address(perpsEngine), 1 ether);
wEth.approve(address(marketMakingEngine), 1 ether);
marketMakingEngine.depositCreditForMarket(marketId, address(wEth), 1 ether);
vm.stopPrank();
int256 realizedDebt = marketMakingEngine.getMarketRealizedDebt(marketId);
assertEq(realizedDebt, 2000e18);
// Deposit UsdToken from Perps Engine to decrease debt
vm.prank(owner);
usdToken.mint(address(perpsEngine), 2000e18);
vm.startPrank(address(perpsEngine));
usdToken.approve(address(marketMakingEngine), 2000e18);
marketMakingEngine.depositCreditForMarket(marketId, address(usdToken), 2000e18);
vm.stopPrank();
// Market's realized debt becomes 0
realizedDebt = marketMakingEngine.getMarketRealizedDebt(marketId);
assertEq(realizedDebt, 0);
}
}

Impact

Incorrect accounting of market debt.

Tools Used

Manual Review

Recommendations

Distribute debt when market's unrealized debt and realized debt are both 0.

Updates

Lead Judging Commences

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

Vault::_recalculateConnectedMarketsState doesn't update market's realized debt per vault share after clearing market's debt, remove the zero check

Support

FAQs

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