Part 2

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

Vault accumulated values do not reflect market change correctly

Summary

In market.getVaultAccumulatedValues, important deltas realizedDebtChage, unrealizedDebtChange, usdcCreditChange are all incorrectly calculated. Returned value is way less than the actual market change. This will bring vault malfunctioning.

Vulnerability Details

Root Cause Analysis

Let's take a look at Market.getVaultAccumulatedValues

// calculate the vault's share of the total delegated credit, from 0 to 1
UD60x18 vaultCreditShareX18 = vaultDelegatedCreditUsdX18.div(getTotalDelegatedCreditUsd(self));
// calculate the vault's value changes since its last accumulation
// note: if the last distributed value is zero, we assume it's the first time the vault is accumulating
// values, thus, it needs to return zero changes
realizedDebtChangeUsdX18 = !lastVaultDistributedRealizedDebtUsdPerShareX18.isZero()
? sd59x18(self.realizedDebtUsdPerVaultShare).sub(lastVaultDistributedRealizedDebtUsdPerShareX18).mul(
vaultCreditShareX18.intoSD59x18()
)
: SD59x18_ZERO;
unrealizedDebtChangeUsdX18 = !lastVaultDistributedUnrealizedDebtUsdPerShareX18.isZero()
? sd59x18(self.unrealizedDebtUsdPerVaultShare).sub(lastVaultDistributedUnrealizedDebtUsdPerShareX18).mul(
vaultCreditShareX18.intoSD59x18()
)
: SD59x18_ZERO;
usdcCreditChangeX18 = !lastVaultDistributedUsdcCreditPerShareX18.isZero()
? ud60x18(self.usdcCreditPerVaultShare).sub(lastVaultDistributedUsdcCreditPerShareX18).mul(
vaultCreditShareX18
)
: UD60x18_ZERO;

The function is calculating deltas in the following way:

What's the problem? Both and are divided by totalDelegatedCreditUsd, so we divide by totalDelegatedCreditUsd twice!

So the final result will be very smaller than the actual change.

Consider the following example:

  • A market has 800 delegated credit USD

  • The market has only one connected vault

  • 200 USDC has been deposited to the market

  • market.usdcCreditPerVaultShare is 200 / 800 = 0.25

  • vaultCreditShare = vaultDelegatedCreditUsd / totalDelegatedCreditUsd = 800 / 800 = 1

  • So usdcCreditChange = usdcCreditPerVaultShare * vaultCreditShare = 0.25

But the expected usdcCreditChange = 200

PoC

The following PoC demonstrates the above scenario:

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 {
function getPrice() external pure returns (uint256) {
return 10 ** DEFAULT_DECIMAL;
}
}
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 = 100 * (10 ** DEFAULT_DECIMAL);
uint256 usdcAmount = 200 * (10 ** DEFAULT_DECIMAL);
uint256 creditRatio = 0.8e18;
uint256[] vaultIds = new uint128[](1);
function setUp() external {
MockVault indexToken = new MockVault();
MockPriceAdapter 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 testPoC() external {
Market.Data storage market = Market.load(marketId);
_recalculateVaultsCreditCapacity();
_logStates();
// collateral asset worth of 1000 USD is deposited to market
market.depositCredit(asset, ud60x18(collateralAssetAmount));
// 100 usdc is deposited
market.settleCreditDeposit(address(0), ud60x18(100e18));
market.receiveWethReward(weth, ud60x18(0), ud60x18(1e18));
_recalculateVaultsCreditCapacity();
// another 100 usdc is deposited
market.settleCreditDeposit(address(0), ud60x18(100e18));
_recalculateVaultsCreditCapacity();
_logStates();
}
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);
}
function _logStates() internal {
console.log("\n");
_logMarketState();
_logVaultState();
_logCreditDelegationState();
console.log("\n");
}
function _logMarketState() internal {
Market.Data storage market = Market.load(marketId);
UD60x18 creditDepositsValueUsdX18 = market.getCreditDepositsValueUsd();
SD59x18 marketTotalDebtUsdX18 = market.getTotalDebt();
UD60x18 delegatedCreditUsdX18 = market.getTotalDelegatedCreditUsd();
SD59x18 creditCapacityUsdX18 = Market.getCreditCapacityUsd(delegatedCreditUsdX18, marketTotalDebtUsdX18);
emit log_named_decimal_uint(
"market.creditDepositsValueUsd", creditDepositsValueUsdX18.unwrap(), DEFAULT_DECIMAL
);
emit log_named_decimal_int("market.totalDebtUsd", marketTotalDebtUsdX18.unwrap(), DEFAULT_DECIMAL);
emit log_named_decimal_uint("market.delegatedCreditUsd", delegatedCreditUsdX18.unwrap(), DEFAULT_DECIMAL);
emit log_named_decimal_int("market.creditCapacityUsd", creditCapacityUsdX18.unwrap(), DEFAULT_DECIMAL);
emit log_named_decimal_int(
"market.realizedDebtUsdPerVaultShare", market.realizedDebtUsdPerVaultShare, DEFAULT_DECIMAL
);
emit log_named_decimal_uint(
"market.usdcCreditPerVaultShare", uint256(market.usdcCreditPerVaultShare), DEFAULT_DECIMAL
);
}
function _logVaultState() internal {
Vault.Data storage vault = Vault.load(vaultId);
SD59x18 vaultTotalDebtUsdX18 = vault.getTotalDebt();
SD59x18 vaultTotalCreditCapacityX18 = vault.getTotalCreditCapacityUsd();
emit log_named_decimal_int("vault.totalDebtUsd", vaultTotalDebtUsdX18.unwrap(), DEFAULT_DECIMAL);
emit log_named_decimal_uint("vault.depositedUsdc", uint256(vault.depositedUsdc), DEFAULT_DECIMAL);
emit log_named_decimal_int("vault.totalCreditCapacity", vaultTotalCreditCapacityX18.unwrap(), DEFAULT_DECIMAL);
}
function _logCreditDelegationState() internal {
CreditDelegation.Data storage creditDelegation = CreditDelegation.load(vaultId, uint256(marketId));
emit log_named_decimal_uint("creditDelegation.valueUsd", uint256(creditDelegation.valueUsd), DEFAULT_DECIMAL);
emit log_named_decimal_int(
"creditDelegation.lastVaultDistributedRealizedDebtUsdPerShare",
int256(creditDelegation.lastVaultDistributedRealizedDebtUsdPerShare),
DEFAULT_DECIMAL
);
emit log_named_decimal_uint(
"creditDelegation.lastVaultDistributedUsdcCreditPerShare",
uint256(creditDelegation.lastVaultDistributedUsdcCreditPerShare),
DEFAULT_DECIMAL
);
emit log_named_decimal_uint(
"creditDelegation.lastVaultDistributedUsdcCreditPerShare",
uint256(creditDelegation.lastVaultDistributedWethRewardPerShare),
DEFAULT_DECIMAL
);
}
}

Console Output

market.creditDepositsValueUsd: 0.000000000000000000
market.totalDebtUsd: 0.000000000000000000
market.delegatedCreditUsd: 800.000000000000000000
market.creditCapacityUsd: 800.000000000000000000
market.realizedDebtUsdPerVaultShare: 0.000000000000000000
market.usdcCreditPerVaultShare: 0.000000000000000000
vault.totalDebtUsd: 0.000000000000000000
vault.depositedUsdc: 0.000000000000000000
vault.totalCreditCapacity: 800.000000000000000000
creditDelegation.valueUsd: 800.000000000000000000
creditDelegation.lastVaultDistributedRealizedDebtUsdPerShare: 0.000000000000000000
creditDelegation.lastVaultDistributedUsdcCreditPerShare: 0.000000000000000000
creditDelegation.lastVaultDistributedUsdcCreditPerShare: 0.000000000000000000
market.creditDepositsValueUsd: 80.000000000000000000
market.totalDebtUsd: 80.000000000000000000
market.delegatedCreditUsd: 800.125000000000000000
market.creditCapacityUsd: 880.125000000000000000
market.realizedDebtUsdPerVaultShare: 0.100000000000000000
market.usdcCreditPerVaultShare: 0.250000000000000000
vault.totalDebtUsd: -0.125000000000000000
vault.depositedUsdc: 0.125000000000000000
vault.totalCreditCapacity: 800.125000000000000000
creditDelegation.valueUsd: 800.125000000000000000
creditDelegation.lastVaultDistributedRealizedDebtUsdPerShare: 0.100000000000000000
creditDelegation.lastVaultDistributedUsdcCreditPerShare: 0.250000000000000000
creditDelegation.lastVaultDistributedUsdcCreditPerShare: 1.000000000000000000

Note: vault.totalCreditCapacityis 800.125 not 800.25 due to another bug:

Impact

All vault calculation will be disrupted, which in effect will distrupt market calculation.

This will lead to incorrect index token swap rate and usd token swap rate, which will cause user fund loss and protocol's reputation damage.

Tools Used

Manual Review, Foundry

Recommendations

Consier the following change

-- UD60x18 vaultCreditShareX18 = vaultDelegatedCreditUsdX18.div(getTotalDelegatedCreditUsd(self));
++ UD60x18 vaultCreditShareX18 = vaultDelegatedCreditUsdX18;
Updates

Lead Judging Commences

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

`market.getVaultAccumulatedValues` returns a lower value

Appeal created

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

`market.getVaultAccumulatedValues` returns a lower value

Support

FAQs

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