Guardian's review of Buyback Updates for GMX, published October 2024. The report records 22 findings across 2 review rounds, including 1 critical and 3 high.
- Published
- Review window
- September 23 to October 9, 2024
- Rounds
- Main Review, Remediation Review
- Language
- Solidity
- Chains
- Arbitrum, Avalanche
- Sector
- Perpetuals
- 1 Critical
- 3 High
- 3 Medium
- 15 Low
- 0 Informational
Scope
Findings 22
Main Review
20 findings · September 23 to 26, 2024-
C-01 Critical Necessary Handler Statuses Are Not Set DoS Resolved
Description
In the
BuybackMigratorcontract the_toggleRewardRouterfunction does not set theextendedGmxTrackercontract as a handler for thebonusGmxTrackerandbnGmxtokens.As a result the
stakeForAccountfunction requires approval from the user as it relies onsafeTransferFromfor the staked token. Thus theRewardRouterV2.batchRestakeForAccountsfunction does not work unless users explicitly approve the newextendedGmxTrackercontract, which is not the expected behavior.Additionally, the
feeGmxTrackerneeds to be set as a handler for theextendedGmxTrackertoken to be able to transfer it in.The current impact is that any
stakeGmx()call would revert and that users would be required to perform multiple approvals upon staking/compounding…Finally, the
GlpManagershould set as handler the newRewardRouterV2contract otherwise, calls to themintAndStakeGlp,mintAndStakeGlpETH,unstakeAndRedeemGlpandunstakeAndRedeemGlpETHfunctions will revert.Recommendation
Enable the correct handler statuses in the
_toggleRewardsRouterfunction with the following:if(isEnabled){ IHandlerTarget(bonusGmxTracker).setHandler(extendedGmxTracker, true); IHandlerTarget(extendedGmxTracker).setHandler(feeGmxTracker, true); IHandlerTarget(bnGmx).setHandler(extendedGmxTracker, true); }On the other hand, consider setting the new
RewardRouterV2contract as a handler for theGlpManagercontract. -
H-01 High Old Reward Router Cannot Be Disabled Logical Error Resolved
Description
In the
disableOldRewardRouterfunction there is no request for the gov permission of theextendedGmxTracker.As a result when the
afterGovGrantedcallback is invoked, it will revert as theBuybackMigratoraddress has not been granted governorship over theextendedGmxTracker.Currently in the test setup the
extendedGmxTrackergovernor is initially assigned as the timelock: https://github.com/gmx-io/gmx-contracts/pull/149/files#diff-2ceafaae01d44042116d49f1333f49a9a808d6378b176ef9d72ac111968592d0R278Recommendation
Request the gov role for the
extendedGmxTrackerin the list of targets. Or otherwise ensure that theBuybackMigratorcontract is the governor of theextendedGmxTrackerwhen the old reward router is disabled. -
H-02 High Incorrect Vesting Validation On Account Transfer Logical Error Resolved
Description
In the
acceptTransferfunction the_validateNotVestinginternal function is called to validate that the sender does not currently have any vested tokens.The original
acceptTransfervalidation validated that neither thegmxVesternor theglpVestercould have vested tokens:require(IERC20(gmxVester).balanceOf(_sender) == 0, "sender has vested tokens"); require(IERC20(glpVester).balanceOf(_sender) == 0, "sender has vested tokens");However the
_validateNotVestingfunction allows the sender to have one of thegmxVesterorglpVesterwith vesting tokens:require(IERC20(gmxVester).balanceOf(_sender) == 0 || IERC20(glpVester).balanceOf(_sender) == 0, "sender has vested tokens");Thus the sender can have a nonzero amount of vesting tokens in the
gmxVesterOR theglpVesterat the time of transfer. This can invalidate subsequent vesting logic as the average staked amounts used to determine the amount of paired tokens necessary for the sender's vest are changed on account transfer with thetransferStakeValuesfunction.Recommendation
Change the validation in the
_validateNotVestingfunction to:require(IERC20(gmxVester).balanceOf(_sender) == 0 && IERC20(glpVester).balanceOf(_sender) == 0, "sender has vested tokens"); -
H-03 High Account Transfers Abused To Invalidate Vesting Logical Error Resolved
Description
The
Vestercontract implements the functiontransferStakeValues:function transferStakeValues(address _sender, address _receiver) external override nonReentrant { _validateHandler(); transferredAverageStakedAmounts[_receiver] = getCombinedAverageStakedAmount(_sender); transferredAverageStakedAmounts[_sender] = 0; uint256 transferredCumulativeReward = transferredCumulativeRewards[_sender]; uint256 cumulativeReward = hasRewardTracker() ? IRewardTracker(rewardTracker).cumulativeRewards(_sender) : 0; transferredCumulativeRewards[_receiver] = transferredCumulativeReward.add(cumulativeReward); cumulativeRewardDeductions[_sender] = cumulativeReward; transferredCumulativeRewards[_sender] = 0; bonusRewards[_receiver] = bonusRewards[_sender]; bonusRewards[_sender] = 0; }This function is used every time the
acceptTransferfunction is called in theRewardRouterV2:IVester(gmxVester).transferStakeValues(_sender, receiver); IVester(glpVester).transferStakeValues(_sender, receiver);As defined in the GMX docs, this is the function that implements the full account transfer flow which transfers all the GMX, esGMX, GLP, Multiplier Points and voting power to a new account.
On the other hand, the
Vestercontract is also responsible for the vesting of esGMX tokens. esGMX represents vested rewards and theVestercontract allows users to gradually convert these esGMX tokens into fully transferable GMX tokens over a set period. This contact enforces a limit on the amount of esGMX tokens that can be locked for GMX:function getMaxVestableAmount(address _account) public override view returns (uint256) { uint256 transferredCumulativeReward = transferredCumulativeRewards[_account]; uint256 bonusReward = bonusRewards[_account]; uint256 maxVestableAmount = transferredCumulativeReward.add(bonusReward); if (hasRewardTracker()) { uint256 cumulativeReward = IRewardTracker(rewardTracker).cumulativeRewards(_account); maxVestableAmount = maxVestableAmount.add(cumulativeReward); } uint256 cumulativeRewardDeduction = cumulativeRewardDeductions[_account]; if (maxVestableAmount < cumulativeRewardDeduction) { return 0; } return maxVestableAmount.sub(cumulativeRewardDeduction); }The
getMaxVestableAmountfunction determines how this limit is calculated. As it can be seen in the code above, the higher thetransferredCumulativeRewardthe higher the amount of esGMX that can be locked.Therefore, the following exploit could be possible:
- Alice stakes 100 GMX through the
RewardRouterV2. - A week later, Alice compounds her rewards. This increases her
StakedGMXTrackercumulativeRewardsto 87626020444780629. - Alice now unstakes the 100 GMX through the
RewardRouterV2. - Alice has 10000 GMX tokens available to stake.
- Alice creates 100 different accounts. For each of those accounts Alice does a full account transfer from her initial account to those 100 accounts. This is done by calling
RewardRouterV2.signalTransfer(destAcc)from Alice’s account and then callingRewardRouterV2.acceptTransfer(AliceAcc)from each of the different accounts created. - With each of these
acceptTransfercalls the receiver account gets itsMaxVestableAmountset to 87626020444780629. This is the source of the exploit as thecumulativeRewardis always assigned to the receiver account but never decremented for the sender as can be seen in the following line of thetransferStakeValuesfunction:uint256 cumulativeReward = hasRewardTracker() ? IRewardTracker(rewardTracker).cumulativeRewards(_sender) : 0; - Alice now sends 100 GMX tokens to each of her 100 created accounts (100 x 100 = 10000) and stakes them through the
RewardRouterV2. - Alice is now able to deposit an additional 87626020444780629 esGMX tokens per account in the
Vestercontract, for a total of 87626020444780629 x 100 = 8762602044478062900 tokens.
Furthermore in the
transferStakeValuesfunction, thetransferredAverageStakedAmountsis set to zero for the sender account, but therewardTracker.cumulativeRewards(sender)is not reset. As a result subsequent receivers will receive the fulltransferredCumulativeRewardsamount, while not receiving thetransferredAverageStakedAmountsthat the sender was originally assigned.Thus it is possible for the receiver to only be required to stake 0 paired sbfGMX tokens to vest their esGMX. This is because the
getPairAmountfunction computes the paired amount as a ratio of thecombinedAverageStakedAmountcompared to themaxVestableAmount.In the scenario where the sending account holds their entire
averageStakedAmountin thetransferredAverageStakedAmountsmapping, the numerator of thegetPairAmountratio is assigned to zero after the first account transfer out of the sender account. However the cumulative rewards are not reset as described above.Recommendation
Consider implementing the following logic for the
transferStakeValuesfunction:function transferStakeValues(address _sender, address _receiver) external override nonReentrant { _validateHandler(); transferredAverageStakedAmounts[_receiver] = getCombinedAverageStakedAmount(_sender); transferredAverageStakedAmounts[_sender] = 0; uint256 transferredCumulativeReward = transferredCumulativeRewards[_sender]; - uint256 cumulativeReward = hasRewardTracker() ? IRewardTracker(rewardTracker).cumulativeRewards(_sender) : 0; + uint256 cumulativeReward = hasRewardTracker() ? IRewardTracker(rewardTracker).cumulativeRewards(_sender) - cumulativeRewardDeductions[_sender] : 0; transferredCumulativeRewards[_receiver] = transferredCumulativeReward.add(cumulativeReward); - cumulativeRewardDeductions[_sender] = cumulativeReward; + cumulativeRewardDeductions[_sender] += cumulativeReward; transferredCumulativeRewards[_sender] = 0; bonusRewards[_receiver] = bonusRewards[_sender]; bonusRewards[_sender] = 0; } - Alice stakes 100 GMX through the
-
M-01 Medium Use Of DelegateCall Within A For Loop In A Payable Multicall Logical Error Resolved
Description
The
RewardRouterV2contract provides amulticall()function designed to batch multiple function calls into a single transaction. This function is declared as payable, suggesting it can accept native assets and forward them to the called functions:function multicall(bytes[] memory data) external payable returns (bytes[] memory results) { results = new bytes[](data.length); for (uint256 i; i < data.length; i++) { (bool success, bytes memory result) = address(this).delegatecall(data[i]); require(success, "call failed"); results[i] = result; } return results; }However, the
multicall()function uses delegatecall to execute the provided calls. As a result, when users attempt to call themintAndStakeGlpETH()function viamulticall()the call forwards the fullmsg.valueeach time, thus effectively duplicating themsg.valueprovided.In many cases this will simply cause the subsequent non-payable function calls to revert or cause subsequent payable function calls to revert as no more Ether is available in the RewardRouterV2 contract.
Recommendation
Consider updating the
multicall()function to correctly forward themsg.valueto the called functions. A possible update could be:function multicall(bytes[] memory data, uint256[] memory msgValues) external payable returns (bytes[] memory results) { require(data.length == msgValues.length, "Diff array lengths"); results = new bytes[](data.length); uint256 msgValue = msg.value; uint256 totalMsgValue; for(uint256 i; i < msgValues.length; ++i){ totalMsgValue += msgValues[i]; } require(totalMsgValue == msgValue, "Diff msgValues"); for (uint256 i; i < data.length; i++) { // Determine the amount of Ether to send with each call if necessary. // For simplicity, this example sends all available Ether with each call. // In a real-world scenario, you might need to split `msg.value` among calls or accept an array of values. (bool success, bytes memory result) = address(this).delegatecall{value: msgValues[i]}(data[i]); require(success, "call failed"); results[i] = result; } return results; }Otherwise consider making the
multicallfunction non-payable and accept the fact that payable functions may not be batched. -
M-02 Medium ExternalHandler Tokens Could Be Drained Logical Error Acknowledged
Description
The
RewardRouterV2contract implements the functionmakeExternalCalls():function makeExternalCalls( address[] memory externalCallTargets, bytes[] memory externalCallDataList, address[] memory refundTokens, address[] memory refundReceivers ) external { IExternalHandler(externalHandler).makeExternalCalls( externalCallTargets, externalCallDataList, refundTokens, refundReceivers ); }This function interacts with an
externalHandlercontract:contract ExternalHandler is IExternalHandler, ReentrancyGuard { using Address for address; using SafeERC20 for IERC20; // @notice refundTokens should be unique, this is because the refund loop // sends the full refund token balance on each iteration, so if there are // duplicate refund token addresses, then only the first refundReceiver // for that token would receive the tokens function makeExternalCalls( address[] memory targets, bytes[] memory dataList, address[] memory refundTokens, address[] memory refundReceivers ) external nonReentrant { if (targets.length != dataList.length) { revert Errors.InvalidExternalCallInput(targets.length, dataList.length); } if (refundTokens.length != refundReceivers.length) { revert Errors.InvalidExternalReceiversInput(refundTokens.length, refundReceivers.length); } for (uint256 i; i < targets.length; i++) { _makeExternalCall( targets[i], dataList[i] ); } for (uint256 i; i < refundTokens.length; i++) { IERC20 refundToken = IERC20(refundTokens[i]); uint256 balance = refundToken.balanceOf(address(this)); if (balance > 0) { refundToken.safeTransfer(refundReceivers[i], balance); } } } }This
ExternalHandlercontract is supposed to execute some external calls and interactions with theRewardRouterV2, receive tokens/rewards and then refund those to the receivers. However, the following exploit could be possible:- Alice calls the
ExternalHandler.makeExternalCalls()and max. approves her smart contract with multiple tokens addresses on behalf of theExternalHandlercontract. - Bob, afterwards, calls
ExternalHandler.makeExternalCalls()batching multiple external calls. During the first external call theExternalHandlercontract received some USDT and GMX tokens. In the second external call, Alice’s smart contract receive some native assets, receiving the control. - Alice’s smart contract, which is max. approved by the
ExternalHandlercontract simply drains all the tokens. - Bob’s
ExternalHandler.makeExternalCalls()is completed but therefundReceiversdo not receive any USDT/GMX as they were stolen by Alice.
On the other hand, the following exploitation path is also present:
- Alice calls the
ExternalHandler.makeExternalCalls()passing as parameter the following external callRewardRouterV2.signalTransfer(<Alice's smart contract>). This external call does the following update:pendingReceivers[<ExternalHandler>] = <Alice's smart contract>; - Bob sends
exGMXandbnGmxto theExternalHandlercontract and then perform multiple external calls. One of these external calls gives the control to Alice’s smart contract which callsRewardRouterV2.acceptTransfer(<ExternalHandler>):. - Alice’s smart contract drains, this way, all the exGMX and bnGmx tokens from the
ExternalHandlercontract.
This attack scenario may be made more likely if tokens with callbacks are used in the external actions.
Recommendation
Consider adding an extra parameter to the
ExternalHandler.makeExternalCalls()function which specifies theminAmountOutthat should be refunded. Any call to this function that does not refund more tokens than theminAmountOutto therefundReceiversshould revert. - Alice calls the
-
M-03 Medium esGMX Claimed To Any Address Logical Error Acknowledged
Description
Currently the
stakedGmxTrackerhasprivateClaimingModeset to false, therefore the reward token, esGMX, can be claimed to any receiver address.This effectively bypasses the non-transferrable nature of esGMX and allows users to leverage the isHandler privilege of the RewardTracker contract to send their esGMX rewards to any address they would like.
This enables issues like H-03 even after the esGMX emissions have been disabled since some accounts may have not yet claimed their esGMX rewards.
Recommendation
Update the configuration of the
stakedGmxTrackercontract such thatprivateClaimingModeis true. -
L-01 Low Users Cannot Unstake Until Re-staked Documentation Resolved
Description
In the current deployment plan for the new RewardRouterV2 contract it is mentioned that the order of operations will be as follows:
- deploy new RewardRouterV2
- enable new RewardRouterV2
- have interface use new RewardRouterV2
- notify integrations to use new RewardRouterV2
- restake for all accounts
- disable old RewardRouterV2
However once the UI is updated to use the new RewardRouterV2 contract users will be unable to unstakeGmx, unstakeEsGmx, compound, claim, claimFees until they are re-staked through the extendedGmxTracker.
This may be unexpected, especially as it may take a non-trivial amount of time to re-stake all accounts.
Recommendation
This issue cannot be solved by re-ordering the deployment plan, as the same issue would persist for users who were already re-staked, and trying to interact with the old RewardRouterV2 contract.
Instead, be sure to document this behavior for users so that this does not come as a surprise.
-
L-02 Low RewardRouterV2 Contract Missing GOV_TOKEN_CONTROLLER Role DoS Acknowledged
Description
The
RewardRouterV2contract implements the internal function_syncVotingPower:function _syncVotingPower(address _account, uint256 _amount) private { uint256 currentVotingPower = IERC20(govToken).balanceOf(_account); if (currentVotingPower == _amount) { return; } if (currentVotingPower > _amount) { uint256 amountToBurn = currentVotingPower.sub(_amount); IMintable(govToken).burn(_account, amountToBurn); return; } uint256 amountToMint = _amount.sub(currentVotingPower); IMintable(govToken).mint(_account, amountToMint); }This function ensures that a user's governance token balance (voting power) is updated after actions such as staking or unstaking. If the user's current voting power exceeds the target amount (
_amount), the excess tokens are burned. Conversely, if the voting power is lower than the target, the difference is minted to the user's account, thereby synchronizing the governance token balance with the required voting power.In order to be able to mint/burn the governance tokens, the
RewardRouterV2contract must be given theGOV_TOKEN_CONTROLLERrole in the RoleStore contract.Recommendation
Ensure that the new
RewardRouterV2contract is given theGOV_TOKEN_CONTROLLERrole in theRoleStorecontract. -
L-03 Low _restakeBnGmx Ignores RewardRouterV2 maxAllowedBnGmxAmount Logical Error Resolved
Description
In the
RewardRouterV2contract themaxBoostBasisPointsstate variable determines the maximum amount of bnGMX tokens that can be staked into theExtendedGmxTrackercontract.Additionally, the
RewardRouterV2contract implements the functionbatchRestakeForAccounts():function batchRestakeForAccounts(address[] memory _accounts) external nonReentrant onlyGov { for (uint256 i = 0; i < _accounts.length; i++) { _restakeForAccount(_accounts[i]); } } function _restakeForAccount(address _account) private { uint256 bonusGmxTrackerBalance = IRewardTracker(feeGmxTracker).depositBalances(_account, bonusGmxTracker); if (bonusGmxTrackerBalance > 0) { uint256 reservedForVesting = IVester(gmxVester).pairAmounts(_account); if (reservedForVesting > 0) { IERC20(feeGmxTracker).safeTransferFrom(gmxVester, _account, reservedForVesting); _restakeBonusGmxTracker(_account, bonusGmxTrackerBalance); _restakeBnGmx(_account); IERC20(feeGmxTracker).safeTransferFrom(_account, gmxVester, reservedForVesting); } else { _restakeBonusGmxTracker(_account, bonusGmxTrackerBalance); _restakeBnGmx(_account); } } } function _restakeBnGmx(address _account) private { uint256 stakedBnGmx = IRewardTracker(feeGmxTracker).depositBalances(_account, bnGmx); if (stakedBnGmx > 0) { IRewardTracker(feeGmxTracker).unstakeForAccount(_account, bnGmx, stakedBnGmx, _account); IRewardTracker(extendedGmxTracker).stakeForAccount(_account, _account, bnGmx, stakedBnGmx); IRewardTracker(feeGmxTracker).stakeForAccount(_account, _account, extendedGmxTracker, stakedBnGmx); } }This function is used to move stakes from the old router to the new one. However, the re-staking process does not verify if the bnGMX tokens staked in the
ExtendedGmxTrackerexceed the limit imposed bymaxBoostBasisPoints. This lack of a limit check could allow accounts to bypass the staking cap for bnGMX tokens on theExtendedGmxTracker.Finally, there is a nonzero amount of bnGMX in the balance of the sbfGMX contract address. There is no trivial way to check if this is currently deposited on behalf of a user or merely sitting in the contract balance. But if the former is the case then that user’s bnGMX would be restaked through the
extendedGmxTracker, ignoring themaxBoostBasisPointswhich may lead to unexpected behaviors.Recommendation
Consider implementing a check within the re-staking process. Specifically, before staking bnGMX tokens in the
ExtendedGmxTracker, a condition should be added to ensure that the total staked bnGMX for an account does not exceed the limit defined bymaxBoostBasisPoints.Additionally, consider verifying whether the bnGMX sitting in the sbfGmx contract address belongs to a user’s deposited amount. If this is the case, consider burning it from that user.
-
L-04 Low FeeGmxTracker Should Only Allow To Stake sbeGMX Configuration Acknowledged
Description
The table below represents the staking/rewards setup after the addition of the new
ExtendedGmxTrackercontract. This is the logic that will be followed as well by the newRewardRouterV2contract:Tracker Stakes Rewards StakedGMXTracker (sGMX) GMX, esGMX esGMX BonusGmxTracker (sbGMX) sGMX bnGMX ExtendedGmxTracker (sbeGMX) sbGMX, bnGMX GMX FeeGmxTracker (sbfGMX) sbeGMX WETH FeeGlpTracker (fGLP) GLP WETH StakedGLPTracker (fsGLP) fGLP esGMX GMXVester esGMX GMX GLPVester esGMX GMX Consequently, as the
FeeGmxTrackercurrently supports sbGMX and bnGMX deposits, these depositTokens should be removed from theFeeGmxTrackerafter the upgrade to the newRewardRouterV2is completed.Recommendation
Ensure that sbGMX and bnGMX are disabled as depositTokens in the
FeeGmxTrackerwhen the upgrade is completed. -
L-05 Low Multiple Operations Require An Unintended GMX/bnGMX Approval Configuration Resolved
Description
The
RewardRouterV2contract implements thecompoundfunction:function compound() external nonReentrant { _compound(msg.sender); } function _compound(address _account) private { uint256 gmxAmount = _claimGmxFees(_account, _account); if (gmxAmount > 0) { _stakeGmx(_account, _account, gmx, gmxAmount); } uint256 esGmxAmount = _claim(stakedGmxTracker, stakedGlpTracker, _account, _account); if (esGmxAmount > 0) { _stakeGmx(_account, _account, esGmx, esGmxAmount); } _stakeBnGmx(_account); _syncVotingPower(_account); }This function stakes the GMX/bnGMX tokens by calling the
_stakeGmxinternal function:function _stakeGmx(address _fundingAccount, address _account, address _token, uint256 _amount) private { _validateAmount(_amount); IRewardTracker(stakedGmxTracker).stakeForAccount(_fundingAccount, _account, _token, _amount); IRewardTracker(bonusGmxTracker).stakeForAccount(_account, _account, stakedGmxTracker, _amount); IRewardTracker(extendedGmxTracker).stakeForAccount(_account, _account, bonusGmxTracker, _amount); IRewardTracker(feeGmxTracker).stakeForAccount(_account, _account, extendedGmxTracker, _amount); _syncVotingPower(_account); emit StakeGmx(_account, _token, _amount); }The
StakedGmxTrackerwill pull the GMX tokens from the_fundingAccountand stake them into the contract. This is thetransferFromimplementation of the GMX/bnGMX tokens:function transferFrom(address _sender, address _recipient, uint256 _amount) external override returns (bool) { if (isHandler[msg.sender]) { _transfer(_sender, _recipient, _amount); return true; } uint256 nextAllowance = allowances[_sender][msg.sender].sub(_amount, "BaseToken: transfer amount exceeds allowance"); _approve(_sender, msg.sender, nextAllowance); _transfer(_sender, _recipient, _amount); return true; }However, as the
StakedGmxTrackercontract is not a handler for the GMX token, the contract can not transfer tokens from an account without enough allowance. (Currently esGMX has already set as a handler theStakedGmxTrackercontract).Therefore, users would be required to GMX approve the
StakedGmxTrackercontract before any compound or stake call. This step would not be necessary if theStakedGmxTrackercontract was a handler for the GMX contract. This addition should be safe as thestakeGmx,stakeEsGmx,compoundandhandleRewardsfunctions directly operate with thecaller/msg.senderaccount.Additionally, the
ExtendedGmxTrackertries to pull the bnGMX tokens from the_fundingAccountand stake them into theExtendedGmxTrackercontract, and thus, bnGMX should also set theExtendedGmxTrackercontract as a handler.The current impact is that all the operations below would require 2 extra approvals from the caller, 1 for GMX and 1 for bnGMX:
stakeGMXcompoundhandleRewardshandleRewardsV2acceptTransferbatchCompoundForAccounts
Recommendation
Consider adding the
StakedGmxTrackercontract as a handler for GMX. Moreover, consider also adding theExtendedGmxTrackeras a handler of the bnGMX contract. By doing so, the contracts would have the necessary permissions to transfer tokens on behalf of users without requiring manual approval for each transaction. This would streamline the user experience by eliminating the need for separateapprovecalls before staking, handling rewards and compounding GMX/bnGMX tokens. -
L-06 Low Missing Initialized Values Warning Acknowledged
Description
The following variables are not assigned with an initial value in the initialize function:
- maxBoostBasisPoints
- inStrictTransferMode
- votingPowerType
Recommendation
Consider if these values should be able to have a nonzero initial value upon initialization and implement these initial assignments in the initialize function if necessary.
-
L-07 Low Lacking Zero Address Checks Validation Acknowledged
Description
In the RewardRouterV2 contract there are several functions which configure addresses which do not validate against the zero address input.
Recommendation
Consider adding zero address checks to the initialize and setGovToken functions.
-
L-08 Low VesterCap May Not Function Correctly Warning Acknowledged
Description
The VesterCap._updateBnGmxForAccount function assumes that accounts have been re-staked as it attempts to unstake amounts from the extendedGmxTracker.
Thus some accounts may be immune to the VesterCap actions if they leveraged any of the methods to avoid the re-staking which we highlighted in other findings.
The VesterCap is not planned for future use, this finding serves only to document this risk.
Recommendation
Be aware of this risk and consider it for future VesterCap usage.
-
L-09 Low VesterCap maxBoostBasisPoints Inconsistency Warning Acknowledged
Description
The VesterCap contract holds a maxBoostBasisPoints storage variable which could be inconsistent with the value used in the RewardRouterV2 contracts.
Recommendation
Consider verifying that all live VesterCap contracts have the same maxBoostBasisPoints as the RewardRouterV2 contracts. Additionally if future use is desired, consider refactoring the VesterCap to rely on the maxBoostBasisPoints stored in the relevant RewardRouterV2 contract.
-
L-10 Low Missing nonReentrant Modifier Reentrancy Resolved
Description
The makeExternalCalls function in the RewardRouterV2 contract does not have a nonReentrant modifier.
Recommendation
Consider adding a nonReentrant modifier to this function.
-
L-11 Low validateReceiver Is Missing Checks On The extendedGmxTracker Logical Error Resolved
Description
The
RewardRouterV2contract implements the functionvalidateReceiver():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"); }This function is used in the
signal/acceptTransfer()logic, to check that theaverageStakedAmountsandcumulativeRewardsvalues of an account are zero ensuring that vesting calculations are executed correctly.After this update, the
ExtendedGmxTrackerwould also be used as part of thestakeGMX()flow. The RewardRouterV2 would stake sbGMX and bnGMX tokens to receive GMX tokens as rewards. However, the_validateReceiver()function fails to check that theaverageStakedAmountsandcumulativeRewardsof this new reward tracker are zero.However there is no identified impact of this issue as bnGMX cannot be staked currently so it isn’t possible to achieve an account with only a nonzero
averageStakedAmountin theextendedGmxTrackerand a zeroaverageStakedAmountfor all otherRewardTrackercontracts.Recommendation
Update the
_validateReceiver()function so it enforces that theaverageStakedAmountsandcumulativeRewardsof theExtendedGmxTrackerare zero. -
L-12 Low Some Accounts Cannot Be Re-Staked Warning Acknowledged
Description
In the
_restakeForAccountfunction only accounts where the depositedbonusGmxTrackeramount in thefeeGmxTrackercontract is nonzero can be restaked.However for accounts where there is only bnGMX staked in the
feeGmxTracker, these accounts will not be able to be processed.This is not a large issue as no accounts should have any staked bnGMX at this point, but it is worth noting.
Recommendation
Be aware of this issue, and confirm that no accounts have deposited amounts for bnGMX on both Arbitrum and Avalanche.
-
L-13 Low Some Accounts May Need To Be ReReStaked Warning Acknowledged
Description
While both RewardRouterV2 contracts are live, it is possible that a user can be re-staked through the new system and then manually unstake and stake through the old RewardRouterV2 contract.
In this case the same user would have to be re-re-staked for a second time. This may also occur if the user calls the compound or handleRewards function on the old RewardRouterV2 contract.
Recommendation
Be aware of this possibility and be prepared to re-stake users for a second time after the old reward router has been disabled.
Remediation Review
2 findings · October 9, 2024-
L-01 Low Private Staking Mode Warning Acknowledged
Description
This finding simply serves as a warning/reminder to be sure to configure the
extendedGmxTrackerto have private staking mode as true.Otherwise unexpected behavior may occur as users can stake through it before the new reward router is enabled with the migrator. This is because the deposit tokens will be set upon initialization of the
extendedGmxTracker.Recommendation
Be sure to set
inPrivateStakingModeas true when deploying theextendedGmxTracker. -
L-02 Low Unused Variable Superfluous Code Resolved
Description
In the
RewardRouterV2contract ainRestakingModevariable has been introduced with a corresponding setter.However this variable is not used for anything notable.
Recommendation
Consider either implementing it's usecase or removing the
inRestakingModevariable.
No findings match.
More from GMX
All 44 reportsPut 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.
