Part 2

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

rebalanceVaultAssets will revert with erc20 insufficient balance error

Summary

When vault assets are rebalanced, vault in debt asset is not transferred to market making engine beforehand. This will fail swap due to insufficient erc20 balance error.

Vulnerability Details

Root Cause Analysis

In CreditDelegationBranch.rebalanceVaultAssets, collateral asset in debt vault is swapped to usdc. However, this collateral is not transferred to market making engine beforehand.

IERC20(ctx.inDebtVaultCollateralAsset).approve(ctx.dexAdapter, assetInputNative); // @audit asset is not transferred yet
dexSwapStrategy.executeSwapExactInputSingle(swapCallData);

market making engine can spend vault assets because of ZlpVault.updateAssetAllowance. Also, market making engine approves dex adapter to spend vault collateral asset on behalf of it.

But this doesn't mean dex adapter can spend vault assets directly. Assets should be transferred to market making engine beforehand.

This will cause ERC20InsufficientBalance when swapping.

POC

POC demonstrates the following scenario:

  • Vault 1 has some credit

  • Vault 2 has some debt

  • When market making engine try to rebalance assets on these vaults, it reverts with ERC20InsufficientBalance 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 { 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 { 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[](2);
uint128 marketId = 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] = 1;
vaultIds[1] = 2;
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);
for (uint256 i; i < vaultIds.length; i++) {
uint128 vaultId = uint128(vaultIds[i]);
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();
if (vaultId == 2) {
// Vault 2 has some debt: 1000 USD
vault.marketsRealizedDebtUsd = int128(uint128(2 * userAssetAmount));
}
}
}
function testPoC() external {
Market.Data storage market = Market.load(marketId);
uint256[] memory _vaultIds = new uint256[](1);
// accrue some credit on vault 1
_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();
// vault 2 is connected to market
_vaultIds = vaultIds;
_connectVaultsAndMarkets(_vaultIds);
_recalculateVaultsCreditCapacity();
uint128[2] memory rebalanceVaultIds = [uint128(2), uint128(1)];
// revert with ERC20InsufficientBalance
vm.expectRevert(
abi.encodeWithSelector(
IERC20Errors.ERC20InsufficientBalance.selector, address(this), 0, 2 * userAssetAmount
)
);
IMarketMakingEngine(address(this)).rebalanceVaultsAssets(rebalanceVaultIds);
}
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 _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);
}
}
}

Impact

Vault assets rebalancing will not work

Tools Used

Manual Review, Foundry

Recommendations

Transfer vault asset to market making engine before the swap.

Updates

Lead Judging Commences

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

settlevaultsdebt and rebalanceVaultAssets inside CreditDelegationBranch incorrectly swaps tokens from marketmakingengine and not directly from Zlpvault which breaks protocol

Support

FAQs

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