eGMX engaged Guardian to review the security of their escrowed GMX exit liquidity system. From the 18th of November to the 25th of November, a team of 6 auditors reviewed the source code in scope.
- Published
- Review window
- November 18 to 25, 2025
- Language
- Solidity
- Chains
- Arbitrum
- Sector
- Yield and vaults
- 6 Critical
- 6 High
- 8 Medium
- 31 Low
- 0 Informational
Scope
Test suiteUmamiDAO/eGMX
Overview
eGMX engaged Guardian to review the security of their escrowed GMX exit liquidity system. From the 18th of November to the 25th of November, a team of 6 auditors reviewed the source code in scope.
Issues Detected Throughout the engagement 12 High/Critical issues were uncovered and promptly remediated by the eGMX team.
Security Recommendation Given the number of High and Critical issues detected as well as additional code changes made after the main review, Guardian recommends that an independent security review of the protocol at a finalized frozen commit is conducted before deployment.
Findings 51
-
C-01 Critical User Info Not Updated During Matching Logical Error Resolved
Description
In the
matchWithdrawRequestfunction, thematcher'ssharesare incremented without first invoking the_depositGMXor_depositGLPfunctions. This prevents thewethRewardDebtfor thematcherfrom being updated. As a result, thes.userInfo[matcher].glpStream.lastClaimmapping remains unchanged for thematcher, even though hiss.userInfo[matcher].glpStream.shareshave increased.In the
GMXYieldStrategycontract, when updating thematcherInfoduring position matching, the contract only updates thesharesfield in thegmxStreamstructure but fails to update the corresponding esGMX vesting information.This results in several issues such as a permanent DoS or drained funds. Furthermore, The WETH reward calculation can underflow if the share amount decreased since the last claim.
Here we can see the reward calculation:
uint256 pendingWeth = shares * s.accumulatedTokensPerShare / 1e18 - wethRewardDebt;Therefore an underflow DoS occurs if:
shares * s.accumulatedTokensPerShare / 1e18 < wethRewardDebtThe WETH reward debt is calculated in the same way:
wethRewardDebt = shares * s.accumulatedTokensPerShare / 1e18;That means if a
wethRewardDebtis stored, thesharesamount of the user decreases and it tries to calculate thependingWethamount again resulting in an underflow DoS. A decrease in shares can happen regularly when users withdraw their tokens.If a user withdraws half of their tokens a DoS of the whole system occurs for this user until the user's rewards are doubled which could never happen if the owner exits the system in the meantime.
Recommendation
Be sure to update all relevant values such as the GMX shares, esGMX vesting information as well as recalculating the
wethRewardDebtwhen processing matcher info. Use the_depositGMXor_depositGLPfunction to claim the pending rewards of thematcher, updating hisuserInfobefore increasing his shares.Additionally, save the accumulated amount instead of the debt in the user struct and then calculate the users share of the stake based on the difference between the current accumulated amount and the saved one.
Resolution
eGMX Team: The issue was resolved in PR#13.
-
C-02 Critical Current Constants.GMX_REWARD_ROUTER Was Disabled By GMX Team Configuration Resolved
Description
The current reward router
GMX_REWARD_ROUTER= 0x159854e14A862Df9E39E1D128b8e5F70B4A3cE9B; present in theConstants.solfile has been deprecated by GMX in a recent transaction.A new reward routernew reward router is now being used. Consequently, any operation on the old reward router will revert, affecting the functionality of contracts that rely on it. The
ExitVaultcontract uses the deprecated reward router address for various operations, such as staking and unstaking GMX tokens.This reliance on the outdated address will cause these operations to fail. Do also note that the [new reward router](https://arbiscan.io/address/0x5E4766F932ce00aA4a1A82d3Da85adf15C5694A1) interacts with a new
RewardTracker(ExtendedGmxTracker) which includes GMX tokens as reward.Recommendation
Consider updating the
Constants.GMX_REWARD_ROUTERto the new reward router address. Ensure that the protocol is ready to also support the GMX rewards now given by theExtendedGmxTracker.Resolution
eGMX Team: Resolved.
-
C-03 Critical gmxUnlockDate And glpUnlockData Should Be Updated On Every Deposit Logical Error Resolved
Description
In the
ExitVaultcontract, the variablesgmxUnlockDateandglpUnlockDateare initialized only during the first deposit to mark the end of the one-year vesting period for the esGMX tokens. Specifically, they are set in thedepositfunction when these variables are zero:if (s.gmxUnlockDate = 0) {s.gmxUnlockDate = block.timestamp + SECONDS_IN_YEAR; emit GMXVestingStarted(msg.sender, s.gmxUnlockDate);} if (s.glpUnlockDate = 0) {s.glpUnlockDate = block.timestamp + SECONDS_IN_YEAR; emit GLPVestingStarted(msg.sender, s.glpUnlockDate);}However, these unlock dates are not updated on subsequent deposits. This means that if additional deposits are made after the initial deposit, the
gmxUnlockDateandglpUnlockDateremain unchanged. As a result, the vault owner can callearlyOwnerExitonce the initial unlock dates have passed, even if not all the esGMX tokens from the later deposits have been fully vested into GMX. This allows the owner to exit earlier than expected before the vesting period for the new deposits is complete.When the owner calls
earlyOwnerExit, the vault withdraws all vested tokens and signals the transfer of the vault's account to a specified receiver. However, during this process, the state variablesgmxSupplyandglpSupplyare not updated to reflect the withdrawal. These variables continue to represent the total supply as if the tokens were still in the vault.As a consequence, the accumulated reward per share variables
s.accumulatedGmxWethPerShareands.accumulatedGlpWethPerSharebecome inaccurate because they rely ongmxSupplyandglpSupplyfor their calculations. When users attempt to claim rewards or interact with the vault, the contract may attempt to distribute more WETH rewards than what it actually possesses reverting due to insufficient balance.This inconsistency in the vault's accounting can cause a Denial of Service for users, preventing them from withdrawing their funds or claiming rewards. Finally, it should also be taken into consideration that the vault owner can use
earlyOwnerExitto signal an account transfer and move out all the GMX-related funds.This is only possible if one year has passed since the first user deposit. When the
acceptTransferis executed, it will unstake all the user's funds from the vester contracts and transfer them to the receiver, receiving funds that belong to the depositors.Recommendation
To prevent the vault owner from exiting earlier than expected and to ensure that all users receive their intended GMX rewards, the
gmxUnlockDateandglpUnlockDateshould be updated with each new deposit. On the other hand, in theearlyOwnerExitfunction, update thegmxSupplyandglpSupplyvariables to account for the tokens withdrawn during the exit.Consider also designing a withdrawal system where only the owner's GMX tokens are transferred to receiver and the remaining can be claimed by stakers, including rewards. Consider creating an escrow contract where the GMX and GLP from users are sent during
earlyOwnerExitto avoid being removed during theaccountTransfercall by recipient.Resolution
eGMX Team: The issue was resolved in PR#12.
-
C-04 Critical ExitVault Overcollects GMX/GLP Tokens From Users Logical Error Resolved
Description
In the
ExitVaultcontract, users are required to deposit GMX tokens to assist in vesting esGMX tokens. The intention is that users contribute the precise amount of GMX needed to vest a corresponding amount of esGMX, based on the vesting mechanics defined by the GMX protocol.However, the current implementation in the
ExitVaultmiscalculates the required amount of GMX, and therefore, forces the users to deposit more GMX tokens than necessary. The core of the issue lies in the_depositWithGmxfunction within theExitVaultcontract, which determines the amount of esGMX to vest (esGmxToVest) based on the user's GMX deposit. The calculation is as follows:(uint256 maxVestWithGMX, uint256 maxGMXCapacity, uint256 maxGLPCapacity) = getMaxVestAmountForVault(address(this)); uint256 esGmxToVest = (_amount * maxVestWithGMX) / maxGMXCapacity;This formula attempts to proportionally assign esGMX to vest based on the amount of GMX deposited. However, it does not accurately reflect the vesting requirements defined in the GMX Vester contract, specifically the
getPairAmountfunction, which calculates the exact amount of GMX required to vest a given amount of esGMX.In the GMX Vester contract, the
getPairAmountfunction ensures that the amount of GMX needed is proportional to the user's combined average staked amount and the maximum vestable amount of esGMX, following this formula:pairAmount = esAmount * combinedAverageStakedAmount / maxVestableAmount;By not aligning with this formula, the
ExitVaultoverestimates the amount of GMX needed for vesting. This results in users depositing more GMX tokens than required. Users are locking up excess GMX without receiving additional vesting benefits, which is inefficient and can discourage participation.The overcollection not only misaligns user expectations but also contradicts the protocol's goal of optimizing resource utilization. Users contribute more capital than necessary and the excess GMX remains idle within the vault, providing no additional advantage in terms of vesting esGMX. This same concept also affects the GLP/
GLPVesterflow.Recommendation
To resolve this issue, the
ExitVaultcontract should be modified to accurately calculate the required amount of GMX needed to vest the available esGMX, directly reflecting the logic used in the GMX Vester contract'sgetPairAmountfunction.The
_depositWithGmxfunction should be updated to use the correct formula for determiningesGmxToVest. This involves calculating the amount of esGMX that can be vested based on the amount of GMX deposited, the combined average staked amount, and the maximum vestable amount, as per the Vester's logic.Resolution
eGMX Team: Acknowledged.
-
C-05 Critical Faulty ownerDeposit Mechanism Logical Error Resolved
Description
Proof of concept: PoC
The function
ownerDepositand its implementation brings many problems for the system. It creates several issues for the both sides of the vault. Firstly and most critical issue is : "Double CountingownerInitialGMXandownerInitialGLPinownerDeposit". During vault initialization,ownerInitialGMXandownerInitialGLPvariables are initialized with the comment: “Store the total balance so that we know how much to transfer back to the owner after a year exits.”Which means owner will be able to withdraw this amount after the end of vesting. However in the function
ownerDepositthese values are incremented regardless if owner makes a new deposit or not. Hence the initial amounts that are transferred via full account transfer will be double counted when owner make the deposit for them. Which means owner can withdraw more than what he actually has and this withdrawal will come with a cost to other users, as the cost will be taken from their share.Secondly: "Diminished Yield When Owner Deposits". When an owner deposits their shares are reduced in anticipation of them being increased later on in the respective internal deposit function. However, before they are increased the account will get yield based on shares. Since it was reduced early the yield will also be reduced. Leading to a loss of yield for the owner whenever they deposit. And finally, another issue is: "Initial Stakes Of Owner Won't be Counted in Reward Calculation".
During initialization, Owners GMX's and GLP's will be staked automatically but these won't be counted in
accumulatedGmxWethPerShareandaccumulatedGlpWethPerShareuntil owner does aownerDeposit. Hence the yield accrued in between will be given to other depositors. Moreover it won't be possible for owner to deposit more than theirshareamounts in one step becauseamountwill be withdrawn fromshare.Furthermore, it is not even possible to deposit new GLP tokens to the vault by the owner because there is no transfer capability in
ownerDepositfunction for GLP.Recommendation
While it is possible to try to implement a fix for every issue individually, the optimal resolution that will fix all the issues would be the to:
- Remove the
ownerDepositfunction altogether and vest the initial amounts for the owner during
initialization.
- Add an owner check to
depositfunction to updateownerInitialGMXandownerInitialGLPvariables.
These changes will basically remove the difference between owner's deposit for the already staked tokens and owner's new deposits with transferring new tokens and will prevent all issues mentioned above from occurring.
Resolution
eGMX Team: The issue was resolved in PR#15.
- Remove the
-
C-06 Critical Utilizing Vault Transfer To Steal Funds Gaming Resolved
Description
Proof of concept: PoC
Owner can transfer the ownership of the entire vault to any other address via
transferFromfunction inExitVaultEntryPointcontract. However this transfer only changes the owner of the NFT andownervalue inGmxAStoragestruct, but does not update any other user related values (GMXStreamandGLPStreamvalues).This creates an important attack vector and also some unexpected scenarios because of the ambiguity of the action. Firstly,
ownerInitialGMXandownerInitialGLPare variables that will be transferred back to owner after a year exits. And owner can use ownership transfer mechanism to increment these values and steal funds from other users.Here is the attack path: 1. Transfer ownership to your second address. 2.
createWithdrawRequest, so that when it is matched, your address won't be the owner anymore andownerInitialGmxwon't be updated. 3. Transfer ownership back to main address. 4. You sold your shares but still holdingownerInitialGmxandownerInitialGlpwhich you can get those amounts back when vesting ends.Apart from this issue it is also not clear what is this ownership transfer tries to achieve. For example, when token distribution happens, Vested GMX distribution will go to the new owner because the transfer is done directly to the
s.owner, hence the part that is not donated will go to new owner, but reward accumulation still happens to old owner because owners' reward related variables are not updated.Another thing is while the shares are not transferred,
ownerInitialGMX/GLPwill belong to new owner now. So while it is not clear what new owner will receive from this ownership transfer, it will also create an attack vector to steal funds from other users.Recommendation
During ownership transfer, update everything related to both old owner and new owner. This includes claiming rewards for both parties before transfer, updating
GMXStreamandGLPStreamalongside withs.ownerchange.Resolution
eGMX Team: Resolved.
-
H-01 High Match Withdrawal Donation Underflow DoS Resolved
Description
Proof of concept: PoC
In the
matchWithdrawRequestthe donation amount is not deducted from the withdrawal request when the transaction is a partial withdrawal fill.In this case the donation amount can be larger than the remaining amount for the withdrawal request and lead to an underflow panic revert on the subsequent withdrawal match. This prevents the user’s withdrawal request from being filled after this case has been reached.
Recommendation
Consider refactoring the withdrawal donation computation by using a ratio of the
request.donationto the original entire withdrawal request amount to compute thedonationPart.Resolution
eGMX Team: The issue was resolved in PR#13.
-
H-02 High ExitVaultEntryPoint.transferFrom Can Be Abused By The Vault Owner To Prevent A User From Withdrawing DoS Resolved
Description
The
transferFromfunction in theExitVaultEntryPointcontract allows the vault owner to transfer ownership of the vault to another user. However, this can be abused to prevent a user from completing their withdrawals. The issue happens when the initial owner fully completes their withdrawal and then transfers their ownership/NFT.As the initial vault owner had withdrawn all his funds at this point,
s.ownerInitialGMXands.ownerInitialGLPwill be zero, leading to an underflow when the new owner attempts to complete the withdrawal of his shares in thematchWithdrawRequestfunction:function matchWithdrawRequest (address _staker, address _token, uint256 _fillAmount, uint256 _minDonation) external checkFullPausedVault {Update the new staker UserInfo. if (_token = TOKEN_GMX) {_depositGMX(0, _staker); Claims rewards stakerInfo.gmxStream.shares = totalShares; matcherInfo.gmxStream.shares = totalShares; if (_staker = s.owner) s.ownerInitialGMX = totalShares; <--------} else {_depositGLP(0, _staker); Claims rewards stakerInfo.glpStream.shares = totalShares; matcherInfo.glpStream.shares = totalShares; if (_staker = s.owner) s.ownerInitialGLP = totalShares; <--------}Recommendation
Under the current implementation the
s.ownerInitialGMXands.ownerInitialGLPstate variables do not add any functionality to the contracts except this restriction that can be abused this way. Consider removing them.Resolution
eGMX Team: The issue was resolved in PR#21.
-
H-03 High Lack Of Incentives For Users To Match Withdrawals Configuration Resolved
Description
In the
ExitVaultcontract, withdrawals are fulfilled by other users who match these requests, taking over their staking positions and receiving a portion of their shares asdonationfor helping them exit. Thisdonationis used to incentivize others to fulfill their withdrawal requests.However, once all the esGMX tokens have been fully vested (i.e., the vault reaches
maxVestableAmountGmxandmaxVestableAmountGlp), the vault cannot vest additional esGMX tokens.At this point, new users have no incentive to match withdrawal requests because they can no longer benefit from the vesting rewards as no more esGMX will be converted into GMX.
This creates a scenario where existing users who wish to withdraw are unable to do so unless they find someone willing to match their withdrawal request without the prospect of earning vesting rewards.
As matching a withdrawal request would not yield any benefits to the new participant, it's unlikely anyone would agree to fulfill such requests, even with a
donation. This situation resembles the old "King of the Ether" game, where users are effectively locked into the contract with no viable means of exiting their positions unless someone else takes their place.The only option left is to offer increasingly higher donations to entice someone to take over, leading to an impractical and potentially infinite loop without resolution.
Recommendation
Introduce a mechanism that allows users to withdraw their tokens directly when the vault has reached its maximum vesting capacity.
Resolution
eGMX Team: Resolved.
-
H-04 High Users Forfeit Their esGMX, bnGMX And GMX Rewards When Entering The Vault Configuration Partially resolved
Description
In the
ExitVaultcontract, users can deposit GMX and GLP tokens to participate in the vesting of esGMX tokens and get a portion of them as rewards. While the vault is active, for at least a year, the esGMX, bnGMX, and additional GMX rewards generated from the staked tokens in the different reward trackers are neither claimed nor distributed to the participating users.Instead, these accumulated rewards are only claimed by the vault owner upon his exit, when the
earlyOwnerExitfunction is executed and followed by accepting the account transfer.This design results in two significant issues:
- Firstly, users effectively forfeit access to their esGMX, bnGMX and GMX rewards for at least the
duration of the vault's operation, despite their GMX/GLP stakes contributing to the generation of these rewards. They do not receive any of these rewards during the active period of the vault.
- Secondly, the accumulated rewards are eventually claimed solely by the vault owner upon exit,
rather than being distributed to the users who actually generate them. This creates a scenario where users' contributions lead to benefits that they do not receive, raising concerns about fairness and discouraging users from participating in the vault due to the deprivation of their rewards.
Moreover, this issue fundamentally undermines the benefits that the protocol aims to provide to users. The esGMX, bnGMX and GMX rewards that users miss out on during the vault's operation are likely worth more than the portion of incentives they receive from the esGMX vesting process.
This means that users may actually be worse off by participating in the vault, as they forfeit substantial rewards over the course of a full year, rewards that would likely exceed the benefits gained from the esGMX vesting. Consequently, the vault's current design may inadvertently disadvantage users instead of providing the intended incentives.
Recommendation
Update the vault's reward distribution mechanism to ensure that users receive their fair share of GMX rewards during the vault's active period. Implement a system where the vault regularly claims the accumulated rewards and distributes the GMX among the users according to their staking contributions and the predefined donation and protocol fee percentages. On the other hand, make use of the esGMX rewards accrued to increase the
maxVestableAmountin the vault.Resolution
eGMX Team: Partially Resolved.
-
H-05 High Users Tricked To Match Withdrawals Logical Error Resolved
Description
Proof of concept: PoC
Stakers can opt to exit the vault by creating a withdrawal request that can me matched by other users, incentivizing them with a donation amount. Malicious users can trick matchers to accept their orders by creating a high donation amount compared to the request amount.
By front-running the
ExitVault.matchWithdrawRequest, they can invalidate their request and create a new request with a higher amount causing thedonationPartcalculation to drastically drop.Consider this scenario:
- userA creates a withdrawal request:
amount = 10 donation = 5 - userB sends a tx to match the request,
_fillAmount = 5 _minDonation=5= (should receive 5 in
donation)
- userA frontruns the tx, invalidates request and creates a new one:
amount = 100 donation=5 - userB now receives
5 * 5 / (100-5) = 0.26
Recommendation
Validate the
_minDonationamount against thedonationPartinstead of therequest.donation.Resolution
eGMX Team: The issue was resolved in PR#13.
- userA creates a withdrawal request:
-
H-06 High Owner's Initial GMX Not Updated When Matching Logical Error Resolved
Description
Proof of concept: PoC
In the
matchWithdrawRequestfunction of theExitVaultcontract, there is an accounting error when the matcher is the owner. While the owner'sgmxStream.sharesare increased when matching a withdrawal request, theirownerInitialGMXvalue is not updated accordingly.This creates a mismatch between the owner's actual shares and their recorded initial GMX amount. The issue occurs because: 1. When matching a withdrawal request, the owner receives additional shares through
matcherInfo.gmxStream.shares = totalShares2. However,ownerInitialGMXis only decremented when the owner is the staker (_staker = s.owner). There is no corresponding increment when the owner is the matcher This mismatch has leads to the following issues:- The owner can only request withdrawals up to their
ownerInitialGMXamount - Additional shares obtained through matching become effectively locked
- The owner's withdrawal capacity doesn't reflect their true position
- The discrepancy grows with each matched withdrawal request
The same can be said for the owner's initial GLP.
Recommendation
Add a check in the
matchWithdrawRequestfunction to updateownerInitialGMXwhen the matcher is the owner. After updating matcher shares, add:if (msg.sender = s.owner) s.ownerInitialGMX =totalShares;Repeat this pattern for GLP.Resolution
eGMX Team: The issue was resolved in PR#21.
- The owner can only request withdrawals up to their
-
M-01 Medium deployVault Calls Can Be Front-run Griefing Resolved
Description
The
ExitVaultEntryPoint.deployVaultfunction is designed to deploy a new vault contract using theCREATE2opcode, mint an ERC721 token representing ownership of the vault and initialize the newly deployed vault with specific parameters.However, the function accepts a
_counterparameter provided by the caller, which must match the currentproxiesCounterstored in the contract.When a user attempts to deploy a vault by calling
deployVaultwith a specific_counter, an attacker or even a legitimate user could submit their owndeployVaulttransaction with the same_counterbefore the original one is mined. If this transaction is processed first, it increments theproxiesCounterand successfully deploys a vault.Consequently, when the original user's transaction is executed, the
_counterno longer matches the updatedproxiesCounter, causing the transaction to revert with anInvalidCountererror. This results in wasted approvals, as users must perform multiple approvals before callingdeployVault.Recommendation
Remove the
proxiesCounter = counterrestriction and instead let the user provide any_counter. Instead of then storing the_counterin themapping(uint256 id = address) public vaults;store the salt:keccak256(abi.encodePacked(_counter, msg.sender))This will require an update in the
vaultsmapping as:mapping(uint256 id = bytes32) public vaultsFinally, in order to compute thetokenIdthat must be minted to theowner, convert thebytes32of the salt to anuint256.Resolution
eGMX Team: Resolved.
-
M-02 Medium Precision Loss May Not Allow All Users To Claim Logical Error Resolved
Description
In the
ExitVaultcontract, when users attempt to claim their rewards, the contract may revert with an error indicating that a transfer amount exceeds the contract's GMX token balance by a minimal amount (e.g., 1 wei). This issue is caused by the precision loss in the calculations of reward distributions within theExitVaultcontract.The critical point of failure is in the reward calculation and distribution functions, where the contract updates users' reward debts and calculates pending rewards based on accumulated per share values. For example, when calculating pending rewards:
uint256 pendingGmx = esGmxToVest * (block.timestamp - lastClaim) / 365 days; pendingGmx = claimedGmx + pendingGmx > esGmxToVest * esGmxToVest - claimedGmx : pendingGmx;And when updating reward debts:
s.userInfo[_recipient].gmxStream.wethRewardDebt = shares * s.accumulatedGmxWethPerShare / 1e18;These calculations can introduce rounding errors due to integer division. As a result, when the contract attempts to distribute rewards, it may calculate that it needs to transfer slightly more tokens than it actually holds, leading to a revert when calling the
transferfunction of the GMX token contract.Recommendation
Before performing a transfer, check the contract's actual token balance and adjust the transfer amount if necessary to avoid attempting to transfer more than the available balance:
uint256 contractBalance = IERC20(TOKEN_GMX).balanceOf(address(this)); uint256 transferAmount = reward > contractBalance * contractBalance : reward;Resolution
eGMX Team: Resolved. 24
-
M-03 Medium Users Should Ensure They Hold Only esGMX Before Deploying A Vault To Optimize Gains Configuration Resolved
Description
To deploy an
ExitVaultcontract the user must perform a full account transfer. This process transfers all tokens and stakes associated with the user's GMX protocol account into the vault, including any staked GMX, esGMX, and other tokens. For instance, if a user has staked 1000 GMX tokens and has earned 100 esGMX tokens that they wish to convert through the vault, initiating the vault deployment and performing the account transfer moves all these assets into the vault.Once the vault holds the staked GMX and the esGMX tokens, it can immediately use the staked GMX to unlock the esGMX without needing additional participants. This renders the collaborative aspect of the vault pointless, as the vault no longer requires contributions from other users to maximize its vesting capacity.
Moreover, the user's staked GMX and other tokens are now locked within the vault and the user loses direct operational control over them. They cannot claim rewards, adjust their staking positions, or interact with their tokens outside the vault. To regain access and control, the user would need to exit the vault by performing another full account transfer, which can only be done after a year due to the vesting period.
This situation may lead vault creators to inadvertently lock up their staked tokens and lose flexibility in managing their assets, contrary to their intentions. It also negates the primary purpose of the vault system, which is to pool resources from multiple users to collectively unlock esGMX tokens, maximizing gains through collaboration. By having sufficient GMX within the vault to unlock the esGMX independently, the need for other users to participate is eliminated.
Recommendation
Ensure that this behavior is documented and known by the users before deploying a vault. To optimize gains and maintain control over their assets, users should ensure they hold only esGMX tokens before deploying a vault. Prior to initiating the vault deployment and account transfer, users should unstake their GMX tokens and withdraw any other staked assets, leaving only the esGMX tokens in their account.
By doing so, when they perform the account transfer to the vault, only the esGMX tokens are moved, and the vault will not have sufficient GMX to unlock the esGMX on its own. This preserves the need for collaborative participation, allowing multiple users to contribute GMX to the vault to maximize vesting capacity collectively. On the other hand, consider adding the following
requirestatement in theExitVault.initializefunction to prevent the described scenario:uint256 totalGMXGLP = IStakedGmx(TOKEN_STAKED_GMX).depositBalances(address(this), TOKEN_GMX) + IERC20(TOKEN_STAKED_GLP).balanceOf(address(this)); require(totalGMXGLP < stakedEsGmxBalance);Resolution
eGMX Team: Resolved.
-
M-04 Medium ExitVaultEntryPoint Centralizes Governance Power Instead Of Delegating To ExitVault Owners Centralization Acknowledged
Description
In the current design of the
ExitVaultEntryPointandExitVaultcontracts, the governance power associated with the staked GMX tokens is centralized within theExitVaultEntryPointcontract’s treasury rather than being held by the individualExitVaultowners.When users deposit their GMX tokens into an
ExitVault, the tokens are staked under the contract's address, and the resulting voting power accumulates to theExitVaultEntryPoint’s treasury.This setup means that all the governance rights derived from these staked tokens are controlled by the
ExitVaultEntryPoint’s treasury rather than the actual owners of the tokens. Consequently, the vault owners are deprived of their ability to participate in governance decisions proportionally to their stake.Recommendation
Consider updating the
ExitVaultcontract so it delegates the governance power to the individualExitVaultowners.Resolution
eGMX Team: Acknowledged.
-
M-05 Medium WETH Rewards Stolen From Owner Logical Error Resolved
Description
Proof of concept: PoC
The
ExitVaultwill distributeGMXandWETHrewards to stakers based on thesbfGMXandfGLPprovided. If owner also transferssbfGMXandfGLPtokens to vault during deployment, these will also earnWETHrewards.The issue relies on how the the protocol calculates the accumulated rewards per share, as it does not include the
sbfGMXandfsGLPtokens in the vault usinggmxSideandglpSideThere are two major impacts here:
- External users can steal owner's WETH rewards generated by
sbfGMXandfGLPtokens in vault - Owner is DoS'ed from depositing or claiming, as the
pendingWethcalculation leads to more WETH
than the contract's balance.
Recommendation
Consider using all
sbfGMXandfGLPin the vault for theesGMXvesting, by depositing during initialization.Resolution
eGMX Team: Resolved.
- External users can steal owner's WETH rewards generated by
-
M-06 Medium Protocol Will Be DoS'd When Private Mode True DoS Acknowledged
Description
GMX's reward tracker has the ability to set
inPrivateClaimingModeto true. When this happens claiming will revert as the action is no longer enabled. If this happens any function that interacts with the_updateVesterwill revert.Recommendation
Document the risk that funds can be DoS'd for periods of time when this variable is set to true.
Resolution
eGMX Team: Acknowledged.
-
M-07 Medium Unfair Reward Distribution On Donation Update Logical Error Acknowledged
Description
The
donationPartcan be updated with theincreaseDonationfunction. This update will probably lead to an unfair distribution of rewards:- Current
donationPartis 10% - Alice & Bob are the only stakers and deposit the same amt of funds
yamt of rewards are accumulated- Bob claims his part of the rewards and receives
y/2*0.1% of the accumulated rewards - One block later the donation amt is increased to 20%
- Alice claims her part of the rewards and receives
y/2*0.2% of the accumulated rewards
Now one staker received double the rewards for depositing the same amount of funds over the same period of time. Both should receive 10% of the accumulated rewards up to the point of changing the
donationPartand 20% after that.Recommendation
Use an accumulator calculation to distribute the rewards to the users and remove the protocol and owner share directly in the
_updateVesterfunction.Resolution
eGMX Team: Acknowledged.
- Current
-
M-08 Medium Centralization Issues Centralization Resolved
Description
The
ExitVaultEntryPointcontract grants the admin extensive control, posing significant centralization risks. Specifically, therescueFundsfunction allows the admin to withdraw arbitrary tokens from any vault under the contract's management, including user-deposited assets and accrued rewards such as GMX and WETH. This enables the admin to transfer user funds without consent.Additionally, the admin has the authority to upgrade the implementation of the
ExitVaultcontract via theUpgradeableBeacon. While intended for enhancements and bug fixes, this upgradability feature allows the admin to deploy malicious implementations that could manipulate user balances or drain assets across all existing vaults.Together, these privileges place excessive trust in a single admin, increasing the risk of unauthorized fund withdrawals and malicious activities that could compromise the entire protocol and its users.
Recommendation
To mitigate centralization risks, it is essential to implement stricter access controls and limitations on the admin's privileges. For the
rescueFundsfunction, restrict withdrawals to specific tokens that are not associated with user deposits or rewards and incorporate additional checks to prevent unauthorized access to user assets.Regarding contract upgrades, adopt a decentralized governance mechanism, such as a multi-signature wallet or a DAO governance model, to require consensus among multiple trusted parties before any upgrade can be executed.
Resolution
eGMX Team: The issue was resolved in PR#17.
-
L-01 Low RewardRouter Configuration Configuration Resolved
Description
Currently there is no way to update the reward router implementation that is used to signal and accept account transfers in GMX. However in the past 6 months the
RewardRouterV2contract has been updated several times by the GMX team.Therefore there should be a seamless way to update the address of the
RewardRouterV2instance that the protocol interacts with in case this contract is updated by GMX.Recommendation
Consider adding a function to update the
RewardRouterV2contract in both theExitVaultEntryPointand theExitVaultBeacon proxies.Resolution
eGMX Team: Resolved.
-
L-02 Low Unnecessary Approval Optimization Resolved
Description
In the
depositfunction there is an approval made on line 114 no matter whether it is a GMX deposit or not. Furthermore, inside the_depositWithGmxfunction the same approval is made to thestakedGmxtoken, therefore the approval made directly in thedepositfunction can be removed.Recommendation
Remove the approval invocation in the
depositfunction.Resolution
eGMX Team: Resolved.
-
L-03 Low matchWithdrawRequest Will Never Be Fully Matched If Request.donation Is Non-Zero Logical Error Resolved
Description
In the
ExitVaultcontract, users can create withdrawal requests specifying the amount they wish to withdraw and an optionaldonationto incentivize others to fulfill their request. ThematchWithdrawRequestfunction allows another user to fulfill this withdrawal request by substituting their own tokens and taking over the staker's position.However, there is an issue in the calculation within the
matchWithdrawRequestfunction that prevents users from fully withdrawing their requested amount in a single transaction unless therequest.donationis set to zero.The problem is that due to the way
donationPartandtotalSharesare calculated, there will be a residual amount ("dust") that cannot be withdrawn. This residual amount remains even after attempting to match the full remaining withdrawal request, forcing users to perform additional matches for negligible amounts, which is impractical.The problematic code snippet is as follows:
uint256 donationPart = _fillAmount * request.donation / (request.remaining - request.donation); uint256 totalShares = _fillAmount + donationPart; if (totalShares > request.remaining) revert InvalidRatio();When a user tries to fulfill the entire remaining amount (
_fillAmountequalsrequest.remaining), the calculation ofdonationPartbecomes flawed because the denominator(request.remaining -request.donation)becomes zero or negative whenrequest.donationis equal torequest.remaining. This can cause a division by zero or revert the transaction due to theInvalidRatiocheck.Even when the
donationis less thanrequest.remaining, integer division and rounding errors can preventtotalSharesfrom exactly matchingrequest.remaining, leaving a non withdrawable residual amount. This issue means that users cannot fully withdraw their tokens in a singlematchWithdrawRequestif adonationis specified, as there will always be some small amount left due to the calculation error.Recommendation
Consider refactoring the withdrawal donation computation by using a ratio of the
request.donationto the original entire withdrawal request amount to compute thedonationPart.Resolution
eGMX Team: Resolved.
-
L-04 Low getMaxVestAmountForVault Function Can Be Simplified Optimization Acknowledged
Description
The
getMaxVestAmountForVaultfunction calculates the maximum amount of esGMX tokens that can be vested using GMX and GLP within the vault. The current implementation includes conditional logic that compares themaxVestableAmountGmxandmaxVestableAmountGlpwith the vault'sesGMXBalance, selecting the lesser of the two for both GMX and GLP vesting capacities:maxVestWithGMX = maxVestableAmountGmx > esGMXBalance * esGMXBalance : maxVestableAmountGmx; maxVestWithGLP = maxVestableAmountGlp > esGMXBalance * esGMXBalance : maxVestableAmountGlp;However, the
ExitVaultcontract is not claiming the esGMX and bnGMX tokens from their respective reward trackers that would increase themaxVestableAmountGmxormaxVestableAmountGlpbeyond the currentesGMXBalance.As there are no mechanisms within the vault that allow for accruing extra esGMX or bnGMX to influence these maximum vesting amounts the comparison between
maxVestableAmountandesGMXBalanceis unnecessary becausemaxVestableAmountGmxandmaxVestableAmountGlpwill never exceedesGMXBalance.Simplifying the function by directly assigning the
esGMXBalanceto bothmaxVestWithGMXandmaxVestWithGLPwould make the code clearer and more efficient.Recommendation
Simplify the
getMaxVestAmountForVaultfunction by removing the unnecessary conditional checks and directly assigning theesGMXBalancetomaxVestWithGMXandmaxVestWithGLP:function getMaxVestAmountForVault() public view returns (uint256 maxVestWithGMX, uint256 maxVestWithGLP, uint256 gmxForMaxVest, uint256 glpForMaxVest){GmxAStorage storage = _getStorage(); uint256 esGMXBalance = IERC20(TOKEN_ESGMX).balanceOf(address(this)); uint256 stakedEsGMXBalance = IStakedGmx(TOKEN_STAKED_GMX).depositBalances(address(this), TOKEN_ESGMX); uint256 totalEsGMXBalance = esGMXBalance + stakedEsGMXBalance; maxVestWithGMX = totalEsGMXBalance; maxVestWithGLP = totalEsGMXBalance; gmxForMaxVest = IesTokenVester(s.gmxVester).getPairAmount(address(this), totalEsGMXBalance); glpForMaxVest = IesTokenVester(s.glpVester).getPairAmount(address(this), totalEsGMXBalance);}Do notice that the
address _vaultparameter was also removed as it was not used.Resolution
eGMX Team: Acknowledged.
-
L-05 Low earlyOwnerExit Calls Can Be Backrun DoS Resolved
Description
In the
ExitVaultcontract, the vault owner can initiate an exit by calling theearlyOwnerExitfunction, which withdraws all vested tokens and signals the transfer of the vault's account to a specified receiver.To complete the exit, the owner (or the new receiver) must call the
acceptTransferfunction provided by the GMXRewardRouterV2contract. However, theacceptTransferfunction includes the following requirements:require(IERC20(gmxVester).balanceOf(_sender) = 0, "sender has vested tokens"); require(IERC20(glpVester).balanceOf(_sender) = 0, "sender has vested tokens");This means the sender (the vault) must have no active vested tokens so the receiver can successfully accept the transfer. However, after the owner calls
earlyOwnerExit, but before thereceivercan callRewardRouterV2.acceptTransfer, a malicious actor can backrun this process by callingExitVault.depositto deposit GMX or GLP tokens into the vault.This deposit reinitiates vesting positions within the vault, causing the
RewardRouterV2.acceptTransfercall to fail due to the newly vested tokens, as the vault no longer satisfies the zero balance requirement. As a result, the vault owner is forced to callearlyOwnerExitagain to withdraw the new vested tokens and re-signal the account transfer.Recommendation
Implement safeguards to prevent unauthorized deposits during the owner's exit process. One approach is to introduce a state variable, such as
isExiting, which is set totruewhenearlyOwnerExitis called. Update thedeposit()function to include a check that prohibits any deposits whenisExitingistrue:function deposit(address _token, uint256 _amount) external checkFullPausedVault {require(isExiting, "Deposits are disabled during owner exit"); existing deposit logic}Resolution
eGMX Team: Resolved.
-
L-06 Low Missing Require Check In matchWithdrawRequest Function Logical Error Resolved
Description
In the
ExitVaultcontract, thematchWithdrawRequestfunction enables a user (the matcher) to fulfill a withdrawal request created by another user (the staker), effectively substituting a portion of the staker's position.However, this function does not check that the
matcheris not the same as thestaker.Recommendation
Add the following
requirecheck in thematchWithdrawRequestfunction:require(_staker = msg.sender, "Cannot match your own withdrawal request");Resolution
eGMX Team: Resolved.
-
L-07 Low deployVault Calls Can Be Griefed Logical Error Acknowledged
Description
In the
ExitVaultEntryPointcontract, thedeployVaultfunction utilizes theCREATE2opcode to deploy newExitVaultinstances at deterministic addresses. This means that the vault's address can be precomputed and is publicly known before the actual deployment. An attacker could front-run thedeployVaultcall and transfer tokens to the precomputed vault address before it is deployed.When the vault is eventually deployed and attempts to accept a GMX full account transfer via the
acceptAccountTransferfunction, it interacts with the GMXRewardRouterV2contract, which includes a_validateReceiverfunction. This function contains severalrequirestatements that check whether the receiver (the newly deployed vault) has zero balances and zero cumulative rewards in various GMX reward trackers and vesting contracts:function _validateReceiver(address _receiver) private view {require(IRewardTracker(stakedGmxTracker).averageStakedAmounts(_receiver) = 0, "stakedGmxTracker.averageStakedAmounts > 0"); require(IRewardTracker(stakedGmxTracker).cumulativeRewards(_receiver) = 0, "stakedGmxTracker.cumulativeRewards > 0"); require(IRewardTracker(bonusGmxTracker).averageStakedAmounts(_receiver) = 0, "bonusGmxTracker.averageStakedAmounts > 0"); require(IRewardTracker(bonusGmxTracker).cumulativeRewards(_receiver) = 0, "bonusGmxTracker.cumulativeRewards > 0"); require(IRewardTracker(feeGmxTracker).averageStakedAmounts(_receiver) = 0, "feeGmxTracker.averageStakedAmounts > 0"); require(IRewardTracker(feeGmxTracker).cumulativeRewards(_receiver) = 0, "feeGmxTracker.cumulativeRewards > 0"); require(IVester(gmxVester).transferredAverageStakedAmounts(_receiver) = 0, "gmxVester.transferredAverageStakedAmounts > 0"); require(IVester(gmxVester).transferredCumulativeRewards(_receiver) = 0, "gmxVester.transferredCumulativeRewards > 0"); require(IRewardTracker(stakedGlpTracker).averageStakedAmounts(_receiver) = 0, "stakedGlpTracker.averageStakedAmounts > 0"); require(IRewardTracker(stakedGlpTracker).cumulativeRewards(_receiver) = 0, "stakedGlpTracker.cumulativeRewards > 0"); require(IRewardTracker(feeGlpTracker).averageStakedAmounts(_receiver) = 0, "feeGlpTracker.averageStakedAmounts > 0"); require(IRewardTracker(feeGlpTracker).cumulativeRewards(_receiver) = 0, "feeGlpTracker.cumulativeRewards > 0"); require(IVester(glpVester).transferredAverageStakedAmounts(_receiver) = 0, "gmxVester.transferredAverageStakedAmounts > 0"); require(IVester(glpVester).transferredCumulativeRewards(_receiver) = 0, "gmxVester.transferredCumulativeRewards > 0"); require(IERC20(gmxVester).balanceOf(_receiver) = 0, "gmxVester.balance > 0"); require(IERC20(glpVester).balanceOf(_receiver) = 0, "glpVester.balance > 0");}If an attacker has sent any amount of tokens or initiated any staking activities to the precomputed vault address before deployment, these
requirestatements will fail because the vault address will now have non-zero balances or reward amounts. Consequently, theacceptTransfercall will revert, causing the vault deployment process to fail. This vulnerability allows a malicious user to perform a Denial of Service attack on any vault deployment.Recommendation
This issue is primarily informative as there is no fully effective mitigation against this attack vector due to the deterministic nature of
CREATE2addresses and due to the restrictions given by GMX to accept a full account transfer. However, considering that the contract operates on Arbitrum, where front-running is limited by the network's sequencing and design, the practical risk of such an attack is minimal.Resolution
eGMX Team: Acknowledged.
-
L-08 Low Unused Custom Errors Optimization Resolved
Description
In the
ExitVaultEntryPointandTokenVestercontracts, several custom errors are declared but not utilized within the contract logic. Specifically in theExitVaultEntryPointcontract:error AddressZero();error IllegalUpgrade();
In the
TokenVestercontract:error WithdrawalExceedsBalance();
Recommendation
Consider removing the mentioned custom errors.
Resolution
eGMX Team: Resolved.
-
L-09 Low Use Of Debugging Imports Optimization Acknowledged
Description
Some contracts contain import statements for the
console.sollibrary from the Forge Standard Library, specifically:import {console} from "forge-std/console.sol";Including
console.solis intended for debugging and logging during the development and testing phases. However, retaining these imports in production contracts is unnecessary.Recommendation
Consider removing all import statements of the
console.sollibrary from the contracts before deploying them to a production environment.Resolution
eGMX Team: Acknowledged.
-
L-10 Low Unlicensed Smart Contracts Configuration Acknowledged
Description
The
ExitVault,ExitVaultEntryPoint,ExitVaultStorage,TokenVesterandPausecontracts are currently marked as unlicensed, as indicated by the SPDX license identifier at the top of the file:SPDX-License-Identifier: UNLICENSEDUsing unlicensed contracts can lead to legal uncertainties and conflicts regarding the usage, modification and distribution rights of the code.
Recommendation
It is recommended to choose and apply an appropriate open-source license to the smart contract. Some options are: 1. MIT License: A permissive license that allows for reuse with minimal restrictions. 2. GNU General Public License (GPL): A copyleft license that ensures derivative works are also open-source. 3. Apache License 2.0: A permissive license that provides an express grant of patent rights from contributors to users.
Resolution
eGMX Team: Acknowledged.
-
L-11 Low Missing Pause Modifiers Validation Resolved
Description
All functions that are callable by stakers or the owner in the
ExitVaultcontract have acheckFullPausedVaultmodifier except theownerDepositfunction. The same holds for all external non-admin functions in theExitVaultEntryPointcontract except for thetransferFromfunction.Recommendation
Consider adding pause modifiers to all functions or following a consistent pause approach like pausing inflows but allowing outflows of the system.
Resolution
eGMX Team: Resolved.
-
L-12 Low Vault Initialization Burns bnGMX Rewards Logical Error Acknowledged
Description
During
ExitVault.initialize, if owner has anybnGMXtokens, these are transferred to the vault duringaccceptTransfer. After full account transfer has been made to vault, theExitVault.initializefunction will also unstake any depositedesGMXusing the router.The issue relies on
RewardRouterV2._unstakeGmxas it will burn allbnGMXtokens from the vault as a penalty. The owner loses funds that could have been used to get moresbfGMXforesGMXvesting.Recommendation
Merely informative issue. While the
RewardRouterV2._unstakeGmxcall would prevent the issue as the bnGMX tokens would not be burnt, it would require some major refactoring to properly account for the vault owner tokens that were left staked.Resolution
eGMX Team: Acknowledged.
-
L-13 Low Missing Storage Gaps Upgradeability Resolved
Description
A general best practice is to set a storage gap of 50, and decrement it by the number of variables that are present in the contract. This was not done in the Pause.sol contract that is inherited from
ExitVaultEntryPoint.sol.Recommendation
Consider adding storage gaps at the end of the
Pause.solcontract to ensure future upgrades are safe.Resolution
eGMX Team: Resolved.
-
L-14 Low Incorrect Treasury Address Initialization Deployment Acknowledged
Description
The protocol's deployment script sets the treasury address as the foundry script contract itself. There is no admin function in the
ExitVaultEntryPoint.solcontract to update this treasury address. This is a critical address that will receive all protocol's fees from the deployed vaults, as well as the delegation votes for GMX DAO.Recommendation
Consider assigning the correct treasury wallet during deployment.
Resolution
eGMX Team: Acknowledged.
-
L-15 Low Invalid Withdrawal Request Created Validation Resolved
Description
Stakers can create withdrawal request to exit the vault early, incentivizing users with a donation. However, the
ExitVault.createWithdrawRequestdoes not correctly validate the user's shares against the request amount and donation.Therefore, some invalid request may occur, making request matching fail:
donation = amount: division by 0 when request is matcheddonation > amount: underflow when request is matchedamount + donation > shares: request can't be fully matched
Recommendation
Validate the above conditions so the request amount and donation correctly match the user's shares.
Resolution
eGMX Team: Resolved.
-
L-16 Low Vault ERC721 Tokens Lost For Invalid Recipients Logical Error Resolved
Description
The
ExitVaultEntryPointmints anERC721token to owner when a vault is deployed. Additionally, owner can transfer the vault's ownership usingtransferFrom.None of these actions contain the safety check
ERC721Utils.checkOnERC721Receivedto verify that the recipient can hold ERC721 tokens.Recommendation
Consider using the
_safeMintandsafeTransferFrominstead. Make sure these calls are performed at the end of the function execution to avoid reentrancy issues. If this is an expected behavior, consider documenting it for user to avoid stuckERC721tokensResolution
eGMX Team: Resolved.
-
L-17 Low Typo In “checFullPauseEntryPoint" Function Typo Resolved
Description
There is a typo in the
checFullPauseEntryPointfunction.Recommendation
Write
checkinstead ofchec.Resolution
eGMX Team: Resolved.
-
L-18 Low claimRewards Can Update Uninitialized Accounts Validation Resolved
Description
The
claimRewardsfunction can be used to update the ownlastClaimvariable to block.timestamp without owning any shares. This could lead to front-end bugs.Recommendation
Revert if the caller's account is empty.
Resolution
eGMX Team: Resolved.
-
L-19 Low Owner Can Not Deposit More GLP Informative Resolved
Description
The
depositfunction reverts withUseOwnerDepositFunctionerror if it is called by the owner. TheownerDepositcan deposit new GMX tokens into the system with the_stakeGmxboolean, but it does not allow the owner to add new GLP tokens.Therefore the owner can't deposit new GLP tokens into the system.
Recommendation
If you allow the owner to deposit new GMX tokens into the system consider also allowing the owner to deposit new GLP tokens.
Resolution
eGMX Team: Resolved.
-
L-20 Low Vaults Tokens Allowed To Be Rescued Validation Resolved
Description
The
ExitVaultEntryPointadmin can rescue funds from vaults without any validation. If any user sends tokens to the vault by mistake, then this call will not harm the protocol. In case the token isGMX, it can break the rewards accounting in the vault.Recommendation
Consider validating the token being rescued, it it's GMX token, only allow admin to perform this action after users leave the vault.
Resolution
eGMX Team: Resolved.
-
L-21 Low Protocol Fee Can Be 100% Validation Acknowledged
Description
Admin is able to change the protocol fee using
ExitVaultEntryPoint.setProtocolFee. If the fee is set to 100%, then vaults deployed will not be able to distribute GMX rewards to owner or stakers.Recommendation
Consider lowering the max value of the protocol fee.
Resolution
eGMX Team: Acknowledged.
-
L-22 Low Missing Check In invalidateWithdrawRequest Validation Resolved
Description
The
invalidateWithdrawRequestdoes not check if the calling user has an active withdraw request, it can be called anytime and emits an event. This could lead to frontend bugs.Recommendation
Revert if the user does not have an active withdraw request.
Resolution
eGMX Team: Resolved.
-
L-23 Low GMX Distribution Favors Vault Owner Rounding Resolved
Description
Vault's
GMXrewards are distributed between the protocol, staker and vault' owner. However, the current calculations will round in favor of the owner and not the protocol.Recommendation
Refactor the calculations so protocol receives the most GMX rewards possible.
Resolution
eGMX Team: Resolved.
-
L-24 Low ExitVaultEntryPoint Left With No Admin Validation Acknowledged
Description
The
ExitVaultEntryPoint.setFirstResponderallows an existingfirstResponderto set the status of a "new" responder. However, if there is only one responder (admin), and his status asfalse, then the contract will be left with nofirstResponderand no way to add them back again.Recommendation
Validate that
_firstResponder = msg.senderor_firstResponder = adminto avoid clearing the admin from it's role.Resolution
eGMX Team: Acknowledged.
-
L-25 Low Insufficient GMX Approval To Exit Vault Logical Error Acknowledged
Description
During
ExitVault.earlyOwnerExit, the function performs two approvals:- ERC20(TOKEN_sbfGMX).approve(_receiver, ERC20(TOKEN_sbfGMX).balanceOf(address(this)));
- ERC20(TOKEN_GMX).approve(TOKEN_STAKED_GMX,IStakedGmx(TOKEN_STAKED_GMX).depositB
alances(address(this), TOKEN_GMX));The first approval is needed for the
RewardsRouter.signalTransfercall, and the second one is used during theRewardsRouter.acceptTransfercall. However, the newRewardsRouterV2.acceptTransferclaimsGMXrewards from theextendedGmxTrackerand stakes them in thestakedGmxTracker.Then, the router tries to unstake and stake all the
GMXin thestakedGmxTrackerbut the allowance needed has increased, reverting theacceptTransfercall.Recommendation
During the
ExitVault.earlyOwnerExit, verify if there are any GMX rewards to claim from theextendedTrackerand add the value to theGMXtoken approval.Resolution
eGMX Team: Acknowledged.
-
L-26 Low Token Rewards Claimed Twice Gas Optimization Acknowledged
Description
User can claim rewards from the vault using
ExitVault.claimRewards. It will claim from thegxmStreamand/orglpStreamaccording to the user's shares.In case user has deposited in both stream, this function will try to claim rewards twice in
_updateVester, although the second call won't claim new tokens. Additionally, if the user does not have any deposit in the streams, the function won't do any changes but the transaction succeeds.Recommendation
Consider refactoring the logic to avoid unnecessary zero reward claims.
Resolution
eGMX Team: Acknowledged.
-
L-27 Low Owner Leaves Without Claiming Rewards Logical Error Resolved
Description
Owners can exit the vault early if there are no user deposits that can lock the vault, or else they will need to wait 1 full year.
In either case, the
ExitVault.earlyOwnerExitdoes not claim rewards before leaving, just as theExitVault.matchWithdrawRequestdoes for stakers. This can lead to issues if the owner later tries to claim.Recommendation
Execute
_depositGMX(0, msg.sender)and_depositGLP(0, msg.sender)before exiting the vault.Resolution
eGMX Team: Resolved.
-
L-28 Low Any firstResponder Can Block Other Responders Access Control Acknowledged
Description
In the
Pausecontract, thesetFirstResponderfunction allows any address with the first responder role to grant or revoke the first responder status of any other address. This creates a flat privilege structure where all first responders have equal power to modify the access control system.This is problematic because: 1. Any first responder can remove other first responders. 2. A compromised first responder account could add malicious addresses as first responders. 3. There's no hierarchical control over who can manage first responder roles.
First responders have significant power in the protocol, including the ability to:
- Pause/unpause the entire protocol.
- Pause/unpause the entry point.
- Pause/unpause specific vaults.
Having no hierarchy in role management creates unnecessary security risks.
Recommendation
Implement an owner or admin role that has exclusive permission to manage first responders.
Resolution
eGMX Team: Acknowledged.
-
L-29 Low Duplicate Address In Constants Validation Acknowledged
Description
Currently there are two addresses in the Constant contract that have the same address.
GMX_GMX_REWARDS_TRACKER,TOKEN_sbfGMX.Recommendation
Consider modifying these constants so that the same address does not belong to different constants.
Resolution
eGMX Team: Acknowledged.
-
L-30 Low Inconsistent Vesting Amounts Due To Fluctuating esGmxToVest Calculation Logical Error Acknowledged
Description
In the
ExitVaultcontract, specifically within the_depositWithGmxfunction, the amount of esGMX that a user is allowed to vest is calculated using the formula:uint256 esGmxToVest = (_amount * maxVestWithGMX) / maxGMXCapacity;Here,
_amountrepresents the user's GMX deposit, whilemaxVestWithGMXandmaxGMXCapacityare dynamic values that fluctuate over time due to changes in the vault'sesGMXbalance, vesting activities, and reward accruals.As these variables change, the ratio of
maxVestWithGMXtomaxGMXCapacityvaries, leading to inconsistent outcomes where users depositing the same amount of GMX at different times receive different amounts ofesGMXto vest.This results in an unfair advantage for early users who might receive more
esGMXcompared to later users for identical deposits. The same issue is also present in the_depositWithGlpfunction.Recommendation
The calculation of
esGmxToVestshould be adjusted to provide consistent vesting amounts regardless of when users deposit their GMX tokens.This calculation can be based on the user's proportional share of the total GMX deposited in the vault, aligning
esGmxToVestwith the user's stake relative to the vault's total deposits.This would ensure that each user's vesting amount is equitable and independent of fluctuations in
maxVestWithGMXandmaxGMXCapacity.Resolution
eGMX Team: Acknowledged.
-
L-31 Low ownerDeposit Can Delete Owner Rewards Logical Error Resolved
Description
The
ownerDepositfunction reduces the shares of the owner before calling_depositWithGmxor_depositWithGlpwhich will increase the shares again. This is done to vest the user's tokens without changing the share amount.These functions will also calculate the account rewards based on the current amount of shares, send them to the user, and update the last accumulator and claim timestamp afterward.
When the owner deploys the contract and by doing so deposits funds into the contract which accrue WETH rewards, the owner will probably lose rewards when he calls the
ownerDepositfunction later:- Owner deploys the contract and deposits funds
- The owner funds accrue WETH rewards
- After one month the owner calls
ownerDepositto vest all of his tokens - The
ownerDepositfunction reduces his shares by the full amount and enters the deposit flow. The
deposit flow will not accrue any rewards as the owner's shares are currently zero. It also overwrites the owner's
lastClaimandwethRewardDebtvariables and therefore the system acts as if the owner just deposited the funds and deletes the rewards the owner accrued in the past month.Recommendation
Call
claimRewardsat the beginning of theownerDepositfunction.Resolution
eGMX Team: Resolved.
No findings match.
More from Umami
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.