Part 2

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

No way to set UsdTokenSwapConfig pd curve parameters

Summary

There is no way to set pd curve parameters of UsdTokenSwapConfig. This will result in division by zero panic when users try to swap usd token to collateral asset.

Vulnerability Details

Root Cause Analysis

UsdTokenSwapConfighas the following pd curve parameters which will be used to define usd token swap rate:

struct Data {
...
uint128 pdCurveYMin;
uint128 pdCurveYMax;
uint128 pdCurveXMin;
uint128 pdCurveXMax;
uint128 pdCurveZ;
...
}

However, there is no way to set pd curve parameters as UsdTokenSwapConfig.update function doesn't have arguments for those parameters:

function update(uint128 baseFeeUsd, uint128 swapSettlementFeeBps, uint128 maxExecutionTime) internal {
Data storage self = load();
self.baseFeeUsd = baseFeeUsd;
self.swapSettlementFeeBps = swapSettlementFeeBps;
self.maxExecutionTime = maxExecutionTime;
emit LogUpdateUsdTokenSwapConfig(baseFeeUsd, swapSettlementFeeBps, maxExecutionTime);
}

This will result in division by zero panic when calculating premiumDiscountFactor:

UD60x18 pdCurveYX18 = pdCurveYMinX18.add(
pdCurveYMaxX18.sub(pdCurveYMinX18).mul(
pdCurveXX18.sub(pdCurveXMinX18).div(pdCurveXMaxX18.sub(pdCurveXMinX18)).pow(pdCurveZX18) // @audit division by zero
)
);

POC

POC shows that initiateSwap request reverts with division error

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 { StabilityConfiguration } from "@zaros/market-making/leaves/StabilityConfiguration.sol";
import { UsdTokenSwapConfig } from "@zaros/market-making/leaves/UsdTokenSwapConfig.sol";
import { IVerifierProxy } from "@zaros/external/chainlink/interfaces/IVerifierProxy.sol";
import { PremiumReport } from "@zaros/external/chainlink/interfaces/IStreamsLookupCompatible.sol";
import { IFeeManager } from "@zaros/external/chainlink/interfaces/IFeeManager.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 { IERC20Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
import { MockChainlinkFeeManager } from "test/mocks/MockChainlinkFeeManager.sol";
import { MockChainlinkVerifier } from "test/mocks/MockChainlinkVerifier.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 {
function burn(uint256 amount) external {
_burn(msg.sender, amount);
}
}
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 MarketMakingEngineTest 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 StabilityConfiguration for StabilityConfiguration.Data;
using DexSwapStrategy for DexSwapStrategy.Data;
uint128 dexSwapStrategyId = 1;
MockAsset asset;
MockEngine mockEngine;
MockUSDT mockUsdt;
MockUSDC mockUsdc;
MockWeth mockWeth;
MockPriceAdapter priceAdapter;
UniswapV3Adapter dexAdapter;
uint256 userAssetAmount = 1000 * (10 ** DEFAULT_DECIMAL);
uint256 creditRatio = 10 ** DEFAULT_DECIMAL;
uint256[] marketIds = new uint256[](1);
uint256[] vaultIds = new uint256[](1);
uint128 marketId = 1;
uint128 vaultId = 1;
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();
_deployDexAdapter();
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;
configuration.vaultDepositAndRedeemFeeRecipient = makeAddr("feeRecipient");
marketIds[0] = uint256(marketId);
vaultIds[0] = uint256(vaultId);
LiveMarkets.Data storage liveMarkets = LiveMarkets.load();
_setUpCollaterals();
Market.Data storage market = Market.load(marketId);
market.id = marketId;
market.engine = address(mockEngine);
market.autoDeleverageStartThreshold = uint128(10 ** (DEFAULT_DECIMAL - 1)); // 0.1
market.autoDeleverageEndThreshold = uint128(10 ** DEFAULT_DECIMAL); // 1
market.autoDeleverageExponentZ = uint128(3 * 10 ** DEFAULT_DECIMAL); // 3
liveMarkets.addMarket(marketId);
DexSwapStrategy.Data storage dexSwapStrategy = DexSwapStrategy.load(dexSwapStrategyId);
dexSwapStrategy.id = dexSwapStrategyId;
dexSwapStrategy.dexAdapter = address(dexAdapter);
address mockChainlinkFeeManager = address(new MockChainlinkFeeManager());
address mockChainlinkVerifier = address(new MockChainlinkVerifier(IFeeManager(mockChainlinkFeeManager)));
StabilityConfiguration.Data storage stabilityConfiguration = StabilityConfiguration.load();
stabilityConfiguration.chainlinkVerifier = IVerifierProxy(mockChainlinkVerifier);
MockZlpVault indexToken = new MockZlpVault(vaultId, address(asset));
indexToken.approve(address(this), type(uint128).max);
indexToken.updateAssetAllowance(type(uint128).max);
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;
vault.engine = address(mockEngine);
vault.swapStrategy.usdcDexSwapStrategyId = 1;
vault.swapStrategy.assetDexSwapStrategyId = 1;
// 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);
vm.stopPrank();
}
function testPoC() external {
Market.Data storage market = Market.load(marketId);
Vault.Data storage vault = Vault.load(vaultId);
uint256[] memory _vaultIds = vaultIds;
// accrue some credit on vault
_vaultIds[0] = 1;
_connectVaultsAndMarkets(_vaultIds);
_recalculateVaultsCreditCapacity();
_depositCreditForMarket(address(mockUsdc), 1_000_000e18);
_receiveMarketFee(address(mockWeth), 1e18);
_recalculateVaultsCreditCapacity();
_depositCreditForMarket(address(mockUsdc), 1_000_000e18);
_receiveMarketFee(address(mockWeth), 1e18);
_recalculateVaultsCreditCapacity();
vm.startPrank(user);
mockUsdt.mint(user, userAssetAmount);
mockUsdt.approve(address(this), userAssetAmount);
uint128[] memory swapVaultIds = new uint128[](1);
swapVaultIds[0] = vaultId;
uint128[] memory amountsIn = new uint128[](1);
amountsIn[0] = uint128(userAssetAmount);
uint128[] memory minAmountsOut = new uint128[](1);
// swap request reverts because all pd curve parameters are not set
vm.expectRevert(stdError.divisionError);
IMarketMakingEngine(address(this)).initiateSwap(swapVaultIds, amountsIn, minAmountsOut);
vm.stopPrank();
}
function _deployDexAdapter() internal {
MockUniswapV3SwapStrategyRouter router = new MockUniswapV3SwapStrategyRouter();
dexAdapter = new UniswapV3Adapter();
vm.startPrank(address(0));
dexAdapter.setSwapAssetConfig(address(asset), uint8(DEFAULT_DECIMAL), address(priceAdapter));
dexAdapter.setSwapAssetConfig(address(mockUsdc), uint8(DEFAULT_DECIMAL), address(priceAdapter));
dexAdapter.setUniswapV3SwapStrategyRouter(address(router));
dexAdapter.setSlippageTolerance(100);
vm.stopPrank();
vm.startPrank(address(router));
mockUsdc.mint(address(router), 9_999_999e18);
vm.stopPrank();
}
function _depositCreditForMarket(address _asset, uint256 amount) internal {
vm.startPrank(address(mockEngine));
ERC20Mock(_asset).mint(address(mockEngine), amount);
ERC20Mock(_asset).approve(address(this), amount);
IMarketMakingEngine(address(this)).depositCreditForMarket(marketId, address(_asset), amount);
vm.stopPrank();
}
function _withdrawUsdTokenFromMarket(uint256 amount) internal {
vm.startPrank(address(mockEngine));
IMarketMakingEngine(address(this)).withdrawUsdTokenFromMarket(marketId, uint128(amount));
vm.stopPrank();
}
function _receiveMarketFee(address _asset, uint256 amount) internal {
vm.startPrank(address(mockEngine));
ERC20Mock(_asset).mint(address(mockEngine), amount);
ERC20Mock(_asset).approve(address(this), amount);
IMarketMakingEngine(address(this)).receiveMarketFee(marketId, address(_asset), amount);
vm.stopPrank();
}
function _recalculateVaultsCreditCapacity() internal {
for (uint256 i; i < marketIds.length; i++) {
IMarketMakingEngine(address(this)).updateMarketCreditDelegations(uint128(marketIds[i]));
}
}
function _connectVaultsAndMarkets(uint256[] memory _vaultIds) internal {
uint256[] memory _marketIds = marketIds;
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);
}
}
function _getMockedSignedReport(uint256 price) internal view returns (bytes memory mockedSignedReport) {
bytes memory mockedReportData;
PremiumReport memory premiumReport = PremiumReport({
feedId: bytes32(0),
validFromTimestamp: uint32(block.timestamp),
observationsTimestamp: uint32(block.timestamp),
nativeFee: 0,
linkFee: 0,
expiresAt: uint32(block.timestamp + 100_000),
price: int192(int256(price)),
bid: int192(int256(price)),
ask: int192(int256(price))
});
mockedReportData = abi.encode(premiumReport);
bytes32[3] memory mockedSignatures;
mockedSignatures[0] = bytes32(uint256(keccak256(abi.encodePacked("mockedSignature1"))));
mockedSignatures[1] = bytes32(uint256(keccak256(abi.encodePacked("mockedSignature2"))));
mockedSignatures[2] = bytes32(uint256(keccak256(abi.encodePacked("mockedSignature3"))));
mockedSignedReport = abi.encode(mockedSignatures, mockedReportData);
}
}

Impact

  • Important parameters are missing from protocol

  • Once vault has some debt (tvlRatio > 0), users won't be able to initiate swap request.

Tools Used

Manual Review, Foundry

Recommendations

Implement a way to set and update pd curve parameters

Updates

Lead Judging Commences

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

PremiumDiscountFactor feature cannot be properly configured / used

Support

FAQs

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