User may not be able to claim staking rewards when multiple vaults are connected to the same market.
Then the rewards are distributed to the vault, so users who stake vault shares can claim the rewards.
Assuming Alice and Bob stake in 2 different vaults, and these 2 vaults are connected to a same market, when the market receives 1 WETH rewards, both Alice and Bob would be able to claim 1 WETH, which is 2 WETH in total, however, there is only 1 WETH for claim, and it eventually leads to insolvency.
Moreover, user who has pending rewards will not able to unstake, their stakes are stucked in vaults.
pragma solidity 0.8.25;
import { Test, console } from "forge-std/Test.sol";
import "./Base.t.sol";
import { Errors } from "@zaros/utils/Errors.sol";
import { Vault } from "@zaros/market-making/leaves/Vault.sol";
import { Collateral } from "@zaros/market-making/leaves/Collateral.sol";
contract AuditTest is Base_Test {
address owner = makeAddr("Owner");
address feeRecipient = makeAddr("FeeRecipient");
function setUp() public override {
bool isTestnet = false;
address[] memory branches = deployPerpsEngineBranches(isTestnet);
bytes4[][] memory branchesSelectors = getPerpsEngineBranchesSelectors(isTestnet);
RootProxy.BranchUpgrade[] memory branchUpgrades =
getBranchUpgrades(branches, branchesSelectors, RootProxy.BranchUpgradeAction.Add);
address[] memory initializables = getInitializables(branches);
bytes[] memory initializePayloads = getInitializePayloads(owner);
branchUpgrades = deployPerpsEngineHarnesses(branchUpgrades);
RootProxy.InitParams memory initParams = RootProxy.InitParams({
initBranches: branchUpgrades,
initializables: initializables,
initializePayloads: initializePayloads
});
perpsEngine = IPerpsEngine(address(new MockEngine(initParams)));
address[] memory mmBranches = deployMarketMakingEngineBranches();
bytes4[][] memory mmBranchesSelectors = getMarketMakerBranchesSelectors();
RootProxy.BranchUpgrade[] memory mmBranchUpgrades =
getBranchUpgrades(mmBranches, mmBranchesSelectors, RootProxy.BranchUpgradeAction.Add);
RootProxy.BranchUpgrade[] memory mmbranchUpgrades = new RootProxy.BranchUpgrade[](mmBranchUpgrades.length);
for (uint256 i; i < mmbranchUpgrades.length; i++) {
mmbranchUpgrades[i] = mmBranchUpgrades[i];
}
initializables = getInitializables(mmBranches);
initializePayloads = getInitializePayloads(owner);
RootProxy.InitParams memory mmEngineInitParams = RootProxy.InitParams({
initBranches: mmbranchUpgrades,
initializables: initializables,
initializePayloads: initializePayloads
});
marketMakingEngine = IMarketMakingEngine(address(new MarketMakingEngine(mmEngineInitParams)));
vm.startPrank(owner);
uint256[2] memory marginCollateralIdsRange;
marginCollateralIdsRange[0] = INITIAL_MARGIN_COLLATERAL_ID;
marginCollateralIdsRange[1] = FINAL_MARGIN_COLLATERAL_ID;
mockSequencerUptimeFeed = address(new MockSequencerUptimeFeed(0));
configureMarginCollaterals(
IPerpsEngine(perpsEngine), marginCollateralIdsRange, true, mockSequencerUptimeFeed, owner
);
usdc = MockERC20(marginCollaterals[USDC_MARGIN_COLLATERAL_ID].marginCollateralAddress);
usdToken = MockUsdToken(marginCollaterals[USD_TOKEN_MARGIN_COLLATERAL_ID].marginCollateralAddress);
weEth = MockERC20(marginCollaterals[WEETH_MARGIN_COLLATERAL_ID].marginCollateralAddress);
wstEth = MockERC20(marginCollaterals[WSTETH_MARGIN_COLLATERAL_ID].marginCollateralAddress);
wEth = MockERC20(marginCollaterals[WETH_MARGIN_COLLATERAL_ID].marginCollateralAddress);
wBtc = MockERC20(marginCollaterals[WBTC_MARGIN_COLLATERAL_ID].marginCollateralAddress);
marketMakingEngine.configureCollateral(
address(usdc),
marginCollaterals[USDC_MARGIN_COLLATERAL_ID].priceAdapter,
MOCK_PERP_CREDIT_CONFIG_DEBT_CREDIT_RATIO,
true,
marginCollaterals[USDC_MARGIN_COLLATERAL_ID].tokenDecimals
);
marketMakingEngine.configureCollateral(
address(usdToken),
marginCollaterals[USD_TOKEN_MARGIN_COLLATERAL_ID].priceAdapter,
MOCK_PERP_CREDIT_CONFIG_DEBT_CREDIT_RATIO,
true,
marginCollaterals[USD_TOKEN_MARGIN_COLLATERAL_ID].tokenDecimals
);
marketMakingEngine.configureCollateral(
address(wEth),
marginCollaterals[WETH_MARGIN_COLLATERAL_ID].priceAdapter,
MOCK_PERP_CREDIT_CONFIG_DEBT_CREDIT_RATIO,
true,
marginCollaterals[WETH_MARGIN_COLLATERAL_ID].tokenDecimals
);
marketMakingEngine.configureCollateral(
address(wBtc),
marginCollaterals[WBTC_MARGIN_COLLATERAL_ID].priceAdapter,
MOCK_PERP_CREDIT_CONFIG_DEBT_CREDIT_RATIO,
true,
marginCollaterals[WBTC_MARGIN_COLLATERAL_ID].tokenDecimals
);
marketMakingEngine.setWeth(address(wEth));
marketMakingEngine.configureEngine(address(perpsEngine), address(usdToken), true);
marketMakingEngine.configureVaultDepositAndRedeemFeeRecipient(feeRecipient);
uint256[2] memory vaultsIdsRange;
vaultsIdsRange[0] = INITIAL_VAULT_ID;
vaultsIdsRange[1] = FINAL_VAULT_ID;
setupVaultsConfig();
createZlpVaults(address(marketMakingEngine), owner, vaultsIdsRange);
bool isTest = true;
setupPerpMarketsCreditConfig(isTest, address(perpsEngine), address(usdToken));
vm.stopPrank();
}
function testAudit_OneMarketTwoVaults() public {
vm.startPrank(owner);
uint128 wethVaultId = WETH_CORE_VAULT_ID;
address wethVaultIndexToken = address(zlpVaults[vaultsConfig[wethVaultId].asset][vaultsConfig[wethVaultId].vaultType]);
ZlpVault wethZlpVault = ZlpVault(wethVaultIndexToken);
{
Collateral.Data memory collateral = Collateral.Data({
creditRatio: vaultsConfig[wethVaultId].creditRatio,
priceAdapter: vaultsConfig[wethVaultId].priceAdapter,
asset: vaultsConfig[wethVaultId].asset,
isEnabled: vaultsConfig[wethVaultId].isEnabled,
decimals: vaultsConfig[wethVaultId].decimals
});
Vault.CreateParams memory vaultCreatParams = Vault.CreateParams({
depositFee: vaultsConfig[wethVaultId].depositFee,
redeemFee: vaultsConfig[wethVaultId].redeemFee,
vaultId: vaultsConfig[wethVaultId].vaultId,
depositCap: vaultsConfig[wethVaultId].depositCap,
withdrawalDelay: vaultsConfig[wethVaultId].withdrawalDelay,
indexToken: wethVaultIndexToken,
engine: address(perpsEngine),
collateral: collateral
});
marketMakingEngine.createVault(vaultCreatParams);
Vault.UpdateParams memory vaultUpdateParams = Vault.UpdateParams({
vaultId: wethVaultId,
depositCap: vaultCreatParams.depositCap,
withdrawalDelay: vaultCreatParams.withdrawalDelay,
isLive: true,
lockedCreditRatio: 0
});
marketMakingEngine.updateVaultConfiguration(vaultUpdateParams);
}
vm.stopPrank();
address alice = makeAddr("Alice");
{
uint256 depositAmount = 1e18;
wEth.mint(alice, depositAmount);
vm.startPrank(alice);
wEth.approve(address(marketMakingEngine), depositAmount);
marketMakingEngine.deposit(vaultsConfig[wethVaultId].vaultId, uint128(depositAmount), 0, "", false);
uint256 aliceShares = wethZlpVault.balanceOf(alice);
wethZlpVault.approve(address(marketMakingEngine), aliceShares);
marketMakingEngine.stake(wethVaultId, uint128(aliceShares));
vm.stopPrank();
}
vm.startPrank(owner);
uint128 usdcVaultId = USDC_CORE_VAULT_ID;
address usdcVaultIndexToken = address(zlpVaults[vaultsConfig[usdcVaultId].asset][vaultsConfig[usdcVaultId].vaultType]);
ZlpVault usdcZlpVault = ZlpVault(usdcVaultIndexToken);
{
Collateral.Data memory collateral = Collateral.Data({
creditRatio: vaultsConfig[usdcVaultId].creditRatio,
priceAdapter: vaultsConfig[usdcVaultId].priceAdapter,
asset: vaultsConfig[usdcVaultId].asset,
isEnabled: vaultsConfig[usdcVaultId].isEnabled,
decimals: vaultsConfig[usdcVaultId].decimals
});
Vault.CreateParams memory vaultCreatParams = Vault.CreateParams({
depositFee: vaultsConfig[usdcVaultId].depositFee,
redeemFee: vaultsConfig[usdcVaultId].redeemFee,
vaultId: vaultsConfig[usdcVaultId].vaultId,
depositCap: vaultsConfig[usdcVaultId].depositCap,
withdrawalDelay: vaultsConfig[usdcVaultId].withdrawalDelay,
indexToken: usdcVaultIndexToken,
engine: address(perpsEngine),
collateral: collateral
});
marketMakingEngine.createVault(vaultCreatParams);
Vault.UpdateParams memory vaultUpdateParams = Vault.UpdateParams({
vaultId: usdcVaultId,
depositCap: vaultCreatParams.depositCap,
withdrawalDelay: vaultCreatParams.withdrawalDelay,
isLive: true,
lockedCreditRatio: 0
});
marketMakingEngine.updateVaultConfiguration(vaultUpdateParams);
}
address bob = makeAddr("Bob");
{
uint256 depositAmount = 2000e6;
usdc.mint(bob, depositAmount);
vm.startPrank(bob);
usdc.approve(address(marketMakingEngine), depositAmount);
marketMakingEngine.deposit(vaultsConfig[usdcVaultId].vaultId, uint128(depositAmount), 0, "", false);
uint256 bobShares = usdcZlpVault.balanceOf(bob);
usdcZlpVault.approve(address(marketMakingEngine), bobShares);
marketMakingEngine.stake(usdcVaultId, uint128(bobShares));
vm.stopPrank();
}
vm.startPrank(owner);
uint128 marketCreditConfigId = ARB_PERP_MARKET_CREDIT_CONFIG_ID;
uint128 marketId = perpMarketsCreditConfig[marketCreditConfigId].marketId;
marketMakingEngine.configureMarket(
address(perpsEngine),
perpMarketsCreditConfig[marketCreditConfigId].marketId,
perpMarketsCreditConfig[marketCreditConfigId].autoDeleverageStartThreshold,
perpMarketsCreditConfig[marketCreditConfigId].autoDeleverageEndThreshold,
perpMarketsCreditConfig[marketCreditConfigId].autoDeleverageExpoentZ
);
marketMakingEngine.unpauseMarket(marketId);
uint256[] memory marketIds = new uint256[](1);
marketIds[0] = marketId;
uint256[] memory vaultIds = new uint256[](2);
vaultIds[0] = vaultsConfig[wethVaultId].vaultId;
vaultIds[1] = vaultsConfig[usdcVaultId].vaultId;
marketMakingEngine.connectVaultsAndMarkets(marketIds, vaultIds);
marketMakingEngine.updateVaultCreditCapacity(wethVaultId);
marketMakingEngine.updateVaultCreditCapacity(usdcVaultId);
vm.stopPrank();
wEth.mint(address(perpsEngine), 1 ether);
vm.startPrank(address(perpsEngine));
wEth.approve(address(marketMakingEngine), 1 ether);
marketMakingEngine.receiveMarketFee(marketId, address(wEth), 1 ether);
vm.stopPrank();
vm.prank(alice);
marketMakingEngine.claimFees(wethVaultId);
vm.prank(bob);
vm.expectRevert(abi.encodeWithSelector(bytes4(keccak256("ERC20InsufficientBalance(address,uint256,uint256)")), address(marketMakingEngine), 1, 999999999999999999));
marketMakingEngine.claimFees(usdcVaultId);
vm.startPrank(bob);
uint256 bobShares = usdcZlpVault.balanceOf(bob);
vm.expectRevert(abi.encodeWithSelector(Errors.UserHasPendingRewards.selector, bytes32(uint256(uint160(bob))), 999999999999999999));
marketMakingEngine.unstake(usdcVaultId, bobShares);
vm.stopPrank();
}
}
User won't be able to claim staking rewards, and their staked shares are stucked.
Weth rewards should be distributed based on the vaults' delegated shares.