i.e. market.totalDelegatedCreditUsd
-> vault.totalCreditCapacityUsd
-> vault.depositedUsdc
-> market.usdcCreditPerVaultShare
-> market.totalDelegatedCreditUsd
This loophole will lead to inconsistent vault credit capacity calculation.
In the POC, market receives some usdc deposit. After that, vault.getTotalCreditCapacityUsd()
is compared before previous result after every Vault.recalculateVaultsCreditCapacity
function call.
pragma solidity 0.8.25;
import { CreditDelegationBranch } from "@zaros/market-making/branches/CreditDelegationBranch.sol";
import { VaultRouterBranch } from "@zaros/market-making/branches/VaultRouterBranch.sol";
import { MarketMakingEngineConfigurationBranch } from
"@zaros/market-making/branches/MarketMakingEngineConfigurationBranch.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 { 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 { 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 { Errors } from "@zaros/utils/Errors.sol";
import "forge-std/Test.sol";
uint256 constant DEFAULT_DECIMAL = 18;
contract MockVault {
function totalAssets() external pure returns (uint256) {
return 1000 * (10 ** DEFAULT_DECIMAL);
}
}
contract MockPriceAdapter {
uint256 price = 10 ** DEFAULT_DECIMAL;
function getPrice() external view returns (uint256) {
return price;
}
function setPrice(uint256 newPrice) external {
price = newPrice;
}
}
contract MockEngine {
function getUnrealizedDebt(uint128) external pure returns (int256) {
return 0;
}
}
contract MarketMakingConfigurationBranchTest is
CreditDelegationBranch,
VaultRouterBranch,
MarketMakingEngineConfigurationBranch,
Test
{
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;
uint128 marketId = 1;
uint128 vaultId = 1;
address asset = vm.addr(1);
address usdc = vm.addr(2);
address weth = vm.addr(3);
uint256 collateralAssetAmount = 1000 * (10 ** DEFAULT_DECIMAL);
uint256 creditRatio = 1e18;
MockPriceAdapter priceAdapter;
uint256[] vaultIds = new uint128[](1);
function setUp() external {
MockVault indexToken = new MockVault();
priceAdapter = new MockPriceAdapter();
MockEngine mockEngine = new MockEngine();
MarketMakingEngineConfiguration.Data storage configuration = MarketMakingEngineConfiguration.load();
configuration.usdc = usdc;
Market.Data storage market = Market.load(marketId);
market.engine = address(mockEngine);
uint256[] memory marketIds = new uint256[](1);
marketIds[0] = uint256(marketId);
vaultIds[0] = uint256(vaultId);
market.id = marketId;
LiveMarkets.Data storage liveMarkets = LiveMarkets.load();
liveMarkets.addMarket(marketId);
Collateral.Data storage collateral = Collateral.load(asset);
collateral.priceAdapter = address(priceAdapter);
collateral.creditRatio = creditRatio;
Vault.Data storage vault = Vault.load(vaultId);
vault.id = vaultId;
vault.indexToken = address(indexToken);
vault.collateral.decimals = uint8(DEFAULT_DECIMAL);
vault.collateral.priceAdapter = address(priceAdapter);
vault.collateral.creditRatio = creditRatio;
uint256[] memory _vaultIds = vaultIds;
_connectVaultsAndMarkets(_vaultIds);
}
function testCircularDependency() external {
Market.Data storage market = Market.load(marketId);
Vault.Data storage vault = Vault.load(vaultId);
_recalculateVaultsCreditCapacity();
CreditDelegation.Data storage creditDelegation = CreditDelegation.load(vaultId, uint256(marketId));
market.depositCredit(asset, ud60x18(collateralAssetAmount));
market.settleCreditDeposit(usdc, ud60x18(1e18));
market.receiveWethReward(weth, ud60x18(0), ud60x18(1e18));
_recalculateVaultsCreditCapacity();
market.settleCreditDeposit(usdc, ud60x18(100_000e18));
_recalculateVaultsCreditCapacity();
SD59x18 previousTotalDelegatedCreditUsd = vault.getTotalCreditCapacityUsd();
uint256 count;
for (;;) {
_recalculateVaultsCreditCapacity();
if (previousTotalDelegatedCreditUsd == vault.getTotalCreditCapacityUsd()) {
break;
}
previousTotalDelegatedCreditUsd = vault.getTotalCreditCapacityUsd();
count++;
}
console.log("Calculation settled after n-th try:", count);
}
function _connectVaultsAndMarkets(uint256[] memory _vaultIds) internal {
uint256[] memory marketIds = new uint256[](1);
marketIds[0] = uint256(marketId);
vm.startPrank(address(0));
MarketMakingConfigurationBranchTest(address(this)).connectVaultsAndMarkets(marketIds, _vaultIds);
vm.stopPrank();
}
function _recalculateVaultsCreditCapacity() internal {
MarketMakingConfigurationBranchTest(address(this)).updateMarketCreditDelegations(marketId);
}
}
This inconsistent update will result in incorrect index token swap rate and usdt token swap rate. It will bring user fund loss and protocol reputation damage.
This seems like a protocol design issue. One needs to break circular dependency in calculation logic.