Guardian's review of jGM for Jones DAO, published May 2024. The report records 68 findings across 2 review rounds, including 18 critical and 9 high.
- Published
- Review window
- April 2 to 24, 2024
- Rounds
- Main Review, Remediation Review
- Language
- Solidity
- Chains
- Arbitrum
- Sector
- Yield and vaults
- 18 Critical
- 9 High
- 20 Medium
- 21 Low
- 0 Informational
Scope
Findings 68
Main Review
48 findings · April 2 to 15, 2024-
C-01 Critical Leverage Strategy DoS Through Depositing And Withdrawing Dust DoS Partially resolved
Description
When an user enters a position by depositing funds through the
LeverageRoutercontract and subsequently withdraws it,GMStrategy.afterWithdrawalExecutionis called, swapping the long token for USDC.At the beginning of the withdrawal process, the
actionvariable inLeverageStrategyis set to 3. If the amount deposited is very small, and the long token is BTC, the withdraw fails before setting the variable back to its original state, without reverting the transaction.This happens because 1
SATS, the smallestWBTCunit, is worth more than the smallestUSDCdenominator, and whenGMStrategy.afterWithdrawalExecutionis called by the GMX Withdrawal Handler, the Uniswap v3 call reverts dueamountSpecifiedrounding down to zero.The
actionstate variable is supposed to be set back to 1 in an external call back toLeverageStrategymade past this point.The transaction still goes through as the handler will catch the error and only emit an event, without reverting the call stack up until this point. This leaves the contract at the
action = 3state and blocks most of its functions as they require the variable to be at an idle state of 1 to work.The DOS can be resolved only by having governance call the
enforceActionfunction, to reset the contract back but an attacker can continuously re-exploit it.Recommendation
Implement a minimum withdrawal amount that must be met when creating a withdrawal. This can by dynamic in case the protocol need to change it.
-
C-02 Critical Leverage Strategy Debt Pay Back Fails Due To Insufficient Allowance Logical Error Resolved
Description
When the leverage strategy needs to pay back a part of its debt to the stable vault, the
payBackfunction is invoked by an operator. TheLeverageStrategy.payBackfunction calls theIUnderlyingVault.payBackfunction and the underlying stable vault transfers the tokens.For payback to be successful, the underlying stable vault (
stableVault) requires allowance. The vault is approved with infinite allowance during initialization here, resulting in normal operation flow.However,
LeverageStrategycontract also has an internal_repayStablefunction, which is called duringunwind,leverageDownand somewithdrawactions.function _repayStable(uint256 _amount) internal returns (uint256) { uint256 amountToRepay = _amount > stableDebt ? stableDebt : _amount; stable.approve(address(stableVault), amountToRepay); stableVault.payBack(amountToRepay, 0); // ...This function approves the
stableVaultagain with the value ofamountToRepay(even though it already has infinite allowance), and immediately consumes the same amount of allowance. After this action, the infinite allowance will basically be overwritten and zeroed out.LeverageStrategy.payBackfunction will always revert due to insufficient allowance, after_repayStableis called even once. This results in the complete blocking of payback functionality, a core feature.Although the contract has a way to re-approve the underlying vault by calling the
forceApprovalfunction, it is callable only by governance and calling it before every payback is not feasible.Recommendation
Do not re-approve the
stableVaultin the_repayStablefunction since the vault is already approved. -
C-03 Critical Attacker Can DOS GM Strategy By Abusing Blackisted USDC Addresses DoS Resolved
Description
Withdrawing funds is a 2-Step process:
- The user calls
createWithdrawal - The GMX keeper bot executes the order
No other action in the system is allowed till this process is completed.
The last state change of the second step is the transfer of USDC to the user. If the user is blacklisted by USDC the call from GMX will revert and retrying will also fail.
This enables the following attack path:
- Attacker calls
createWithdrawalwith a receiver that is on the USDC blacklist depositCallbackwill revert when attempting to transfer USDC to the blacklisted addressoperationOnGoingis stuck as true preventing future deposits and withdrawals
Recommendation
Check if the receiver of the withdrawal is blacklisted with USDC.isBlacklisted(receiver).
- The user calls
-
C-04 Critical Function isInRange will not catch out of range values Logical Error Resolved
Description
The isInRange function should be used in the withdrawal process to compare the expected return to the actual return (slippage check). But as we can see it doesn't and instead compares the given parameters to themselves:
if (_idealUSDC < applySlippage(_idealUSDC) || _actualUSDC > addSlippage(_actualUSDC)) { revert OutOfRange(); }It checks if the ideal USDC value is bigger than itself - x% and if the actual USDC value is smaller than itself + x%. This will always be true and therefore nothing is checked at all.
Recommendation
Compare the parameters against each other, not to themselves.
-
C-05 Critical Value Extracted During Faulty Rebalance Logical Error Resolved
Description
When a keeper starts a rebalance by calling
RebalanceStrategy.startRebalance, the function will set theoperationOngoingflag in theGMStrategycontract in order to prevent new deposits and withdrawals from being submitted.This is done because the rebalance is an operation sensitive to the quantity of funds in the strategy contract. At the start of a rebalance, the
GMStrategy.rebalanceInputfunction is called, which sets the flag to true.However, the flag is only set when the rebalance stage is 1 (
rebalanceStage == 1), and thenextRebalanceStagefunction, which increments the stage, is called before therebalanceInputfunction is called, incrementing therebalanceStagevariable to 2, thus preventing the flag from being activated.As a consequence, deposits and withdrawals will be enabled as normal during the rebalance.
An attacker can look for rebalances and when one starts, after the funds were moved from the
GMStrategythen they deposit into the strategy. By doing this, after the rebalance is then finished, they withdraw with a profit since finishing the rebalance implies sending back the new GM tokens to the strategy, effectively increasing the value of the attacker’s deposit.Recommendation
In
GMStrategy.rebalanceInputfunction, setoperationOnGoingto true regardless of current rebalance stage, since this function is only called at the start of a rebalance. -
C-06 Critical Borrowed USDC Is Counted As User Deposit Logical Error Resolved
Description
When a deposit is made,
jGMshares will be minted to the user based ondata.shares. The issue arises when a deposit is leveraged becausedata.sharesincludes the additional borrowed amount. However, when the deposit is not leveraged,data.sharesonly reflects the amount the user provided initially.This discrepancy creates a situation where users depositing with leverage receive more shares per asset than users making non-leveraged deposits, despite facing the same risk associated with
jGM.This discrepancy directly leads to profit at the expense of the other users for those that deposit while leverage is below target, as the excess shares that were minted can then be withdrawn.
Recommendation
To address this issue, modify the minting of
jGMshares to be based on theamountstored in thecallbackDatamapping. This change will exclude the leveraged amount from the calculation:shares = _jGM.mulDivDown(callback.amount, callback.amount + callback.leverage); -
C-07 Critical Full Protocol DOS Via Spamming Operations DoS Partially resolved
Description
The entire
jGMsystem does not allow more then 1 operation (withdraw/deposit) to function at a time. During the time from when an operation was initiated on GMXv2 and when it was done (the keepers execute the request) no other operation is allowed.This odd design, combined with the lack of a minimum deposit/withdraw amount, allows an attacker to continuously spam deposits to block any operations to be done on the system. The attack is cheap and would result in a complete system block.
Recommendation
Implemented a minimum withdraw/deposit amount. Also consider completely redesigning the system to not have a global lock on each operation.
-
C-08 Critical Attacker Can Repurpose Signatures To Damage Protocol Logical Error Resolved
Description
The vulnerability is a combination of 3 issues with the core issue being that whenever a user interactions with the protocol, the off-chain systems must validated a part of the input data and sign it, in order to protect against manipulations. The information within a signature that is validates is represented by the
IGMRouter.Datastructure. This structure, however does not include the type of operation was created or the input token amount.One attack scenario
When a user withdraws, the amount
Leveraged jGMshares he burns is returned to him in equivalent USDC. The information in the signedIGMRouter.Datais created as such that he receives a fair amount but there are no checks on-chain that the equivalent burnedLeveraged jGMis roughly the same as that indicated by theIGMRouter.Datastructure, validations are off-chain only.By combining the above, an attacker can:
- say the TVL in the strategy is 1,000,000 USD distributed in GM tokens according to the targeted weights
- initiate a deposit in the dapp of 1,000,000 USDC equivalent
- the dapp will generate a signature and a
IGMRouter.Datastructure with a buy equivalent of GM tokens, to equal 1,000,000 USD in value, distribute in GM tokens according to targeted weights - the dapp will now provide the data to the "user" for him call the protocol with
- the attacker then takes the entire
IGMRouter.Dataand signature and passes it directly, bypassing the dapp, to a withdraw instead, setting an insignificant amount ifLeveraged jGMtokens - the withdraw will be initiated successfully, due to no value checks on the passed data in the
strategyWithdrawfunction but will fail after GMX has fully closed all the GM strategy positions due to the faulty signature data in theafterWithdrawalExecutioncallback
Although it blocks an attacker from stealing the funds it does not stop him from completely withdrawing from GMX the entire
GMStrategy balance. This also results in loss of yield form the unstaked GM tokens as well as blocking all further withdraws, since there are no more GM tokens to withdraw, requiring manual intervention to resolve.Another attack path is regarding deposits. In deposit again there is no validation that the passed USDC is equivalent to the value to be bought in GM tokens. Attack scenario:
- attacker waits for stables to accumulate in the contract
- initiates a deposit of the exact amount of stables existing in the contract
- the dapp will generate a signature and a
IGMRouter.Datastructure with a buy equivalent of GM tokens - the attacker then again takes the entire
IGMRouter.Dataand signature and passes it directly, bypassing the dapp, while depositing dust USDC. - due to deposit implementation attacker only gets an equivalent share amount to the dust he deposited but the entire USDC in the strategy was added to GMX
For the second scenario, an attacker can force a "compound" action instead of a "payBack". Example, the attack might be done exactly after a
keeperPayBackcall, where the intention to initiate a debt payback is clear (and made in a 2 step manner).Recommendation
Modify the
IGMRouter.Datastructure to include amount and operation type (use an Enum for operations).In the
strategyWithdrawfunction from theGMStrategycontract, validated that the approximate USDC gains from withdrawing the all GM tokens from theGMDataarray are equal to the_assetsamounts. Meaning also create the equivalent_isInRangecheck before initiating the withdraw call.In the
strategyDepositfunction from theGMStrategycontract, validate that the passedUSDC_amountis equal to that used to buy all the GM tokens. -
C-09 Critical Attacker can cause DoS by setting receiver to address(0) DoS Resolved
Description
When a user calls the
createDepositfunction, they have the option to choose any address as the receiver. After the deposit completes, theafterDepositExecutionfunction triggers amintoperation to create shares for the designated receiver. However, attempting to mint shares toaddress(0)causes a revert. This revert leads to the contract state variableoperationOnGoingremaining true, which is required to be false for both deposits and withdrawals to proceed. This situation will prevent future deposits or withdrawals.Similarly, the
withdrawalCallbackfunction also encounters a revert when attempting to transfer assets tocallback.receiver, resulting in the same outcome.Recommendation
Check and require that
receiverdoes not equaladdress(0)in bothcreateDepositandcreateWithdrawalfunctions. -
C-10 Critical Liquidated Users Can Steal Future Deposits Logical Error Partially resolved
Description
When the governor or keeper calls
LeverageStrategy.unwind, the contract can become unusable due to the action state being stuck at 2 after a standard user deposit, as discussed in a separate issue.However, even in the scenario where the responsible actors unstuck the contract state by enforcing action back to 1 and providing it some funding, so operations return to normal, previous users liquidated by the unwind operation will still own their leverage vault shares.
Due to this reason, even when a new user deposits in the system after the governance/keeper intervention, a previously liquidated user can steal some of the funds provided by the new user, by submitting a withdrawal with their leveraged vault shares.
The amount of funds the previously liquidated user can "claw back" will depend on the total underlying value the keeper has injected into the protocol; in some cases, it can be greater than their previous deposit, and in other cases, it will be less.
On top of this, when the new user attempts to withdraw his funds, the callback from GMX will revert without reverting the whole transaction, causing the action to be in an invalid state again.
The provided PoC showcases a scenario where the governance will provide fresh new underlying funds immediately after an unwind, a new user enters, a previous user steals a portion of funds, and the new user attempts to exit all their shares unsuccessfully.
Recommendation
In order to not be able to manipulate the system, liquidated users should not own leveraged vault shares, so the unwind operation must burn them. Otherwise, migrate the leverage vault.
-
C-11 Critical Attacker can call cancellation callback to deplete strategy of GM tokens Validation Partially resolved
Description
The
afterDepositCancellationandafterWithdrawalCancellationfunctions trigger the bot to retry a cancelled order by calling eitherexecuteSingleDepositorexecuteSingleWithdrawbased on the emitted data from the cancellation. However, any user can set thecallbackContractto theGMStrategycontract on their own malicious contract.This allows an attacker to orchestrate a guaranteed-to-fail withdraw with the
GMStrategyas the callback contract. Subsequently, when the cancellation callback is invoked, the keeper will retry the order with the GM tokens from theGMStrategy.This vulnerability enables an attacker to deplete the strategy of its GM tokens, leaving the underlying temporarily stuck in the
GMStrategy. As a result, users who withdraw during this time will experience a substantial loss, as their JGM shares will be backed by little to no actual GM tokens.Recommendation
Utilize the
keyparameter to verify that the cancellation is originating from an order initiated byjGM. This can be achieved by checking ifkeys[key]is empty and reverting if it is.Additionally all GMX callbacks should explicitly check that the
keyis from an order initiated byjGM. -
C-12 Critical Attacker Can cause DoS by Intentionally exceeding range check DoS Partially resolved
Description
When a user creates a withdrawal, they will transfer GM tokens to the GMX withdrawal handler. GMX will then record the transfer by taking the
balanceOfof the contract. This quirk allows for users to alter the amount used when creating an order from what was initially intended when thecreateWithdrawalfunction was called. In some cases, this is not a bad thing as it results in more underlying tokens received.However, with jGM's current design, this action results in the
operationOnGoingvariable being stuck as true, preventing future deposits and withdrawals. The way an attacker would exploit this would be to execute a transaction that does two things:- First, transfer a small amount of GM tokens to the
withdrawHandler. - Then call the
createWithdrawalfunction in jGM.
What this will do is unexpectedly increase the amount of jGM tokens used to withdraw and consequently the amount of underlying tokens received. Leading to a revert in the
afterWithdrawalExecutionfunction when the_isInRangefunction is called and compares thedata.assetsvalue todata.usdc.The reason this will revert is that
data.usdcis based on the greater than expected amount of GM tokens, whiledata.assetsis based on what was sent by the protocol, which was not aware of the attacker's initial transfer.Recommendation
Alter the check so that if
data.usdcis above the acceptable range, it is truncated to the maximum acceptable value. The excess can then be transferred to a protocol-approved address. - First, transfer a small amount of GM tokens to the
-
C-13 Critical Compound and Rebalance do not increase the nonce Logical Error Resolved
Description
The
compoundas well as the rebalance functions do not increase the nonce value. This enables the following attack paths:Compound:
- Attacker fetches a signed data param from the keeper bot every few seconds
- Keeper bot calls the
compoundfunction - After the compound flow ends the attacker calls
createDepositwith the stale data param from before the keeper bot calledcompound - As the nonce was not increased the stale data param is still valid and the attacker receives excess shares as the stale
usdTotalValuemesses with the share calculation
Rebalance:
- Keeper bot calls the
rebalancefunction - The attacker already watched out for this event and fetches the signed data param between the withdraw and the deposit step of the rebalance flow
- After the rebalance flow ends the attacker calls
createDepositwith the stale data param from the middle of the rebalance flow - As the nonce was not increased the stale data param is still valid and the attacker receives excess shares as the stale
usdTotalValuemesses with the share calculation
Recommendation
Increase the nonce in the
GMRouterwhen callingcompound, orrebalance. -
C-14 Critical Attacker can deposit while ongoingOperation is true to mint extra shares Logical Error Partially resolved
Description
When a deposit is created the keeper will retrieve the
usdTotalValue. This value is the USD value of all the GM tokens in the GMStrategy. TheusdTotalValueis used to calculate the right amount of shares to be minted to the user as well as the amount of IJGM (index) tokens. BecauseusdTotalValueis the denominator when calculating the shares to be minted, meaning the smallerusdTotalValueis the more shares that will be minted.The issue here is that after a deposit is created, and before it is executed, the keeper will retrieve an inaccurate value for
usdTotalValue. This is because the total amount of GM tokens in the vault do not include the incoming GM tokens from the most recent deposit, as the deposit has not been executed yet. Because of this when a user pings the keeper to create a deposit_dataobject whileoperationOngoingis true, they can then use that outdatedusdTotalValuevalue on their deposit. Because the outdatedusdTotalValueis smaller than it should be, it will mint more shares to themselves at the expense of the other users.Recommendation
Create a pending USD value when deposit are created, and add this to the
usdTotalValue. After a deposit is executed set the pending to zero. Some version of the following would mitigate this attack.In the
strategyDepositfunction:pendingIncomingUSD = pendingIncomingUSD + ((minAmount * 1e12 / 0.995e12) * GMPrice(info.gm.oracle, info.gm.stalePeriod));In the
afterDepositExecutionfunction:pendingIncomingUSD = 0;In the
totalValuefunction:return (_totalValue + pendingIncomingUSD) / 1e18; -
C-15 Critical Attacker can cause DoS by overriding gmxData key DoS Partially resolved
Description
In both the
strategyDepositandstrategyWithdrawfunctions the data relevant to the deposit and withdraw is stored in thegmxDatamapping. The key for this mapping is a combination of thereceiveras well asblock.timestamp. Thereceiverwill always be determined by the user who is creating the deposit. And theblock.timestampwill be the current time in seconds.One way Arbitrum differs from some other chains is that Arbitrum can produce multiple blocks per second, in many cases four blocks in a single second. Because the key to the
gmxDatamapping is made up of the current time, and because Arbitrum can have multiple blocks in a second it is possible to override one order with another.Even with the asynchronous nature of the GMX keepers it is possible to have two withdraws close enough together that the second withdraw will override the first. The main impact of this is that the
data.usdcwill start as a non-zero value on the second withdraw, which results indata.usdcbeing larger than the amount of underlying in the contract.Because of this the attempt to transfer
data.usdcwill fail due to insufficient funds. When the callback fails the protocol will not be able to perform future deposits or withdraws as theoperationOngoingwill still be true. With high enough traffic on the protocol, or a malicious sequence of withdraws, the protocols functionality can be halted.Recommendation
Either add a nonce when setting
info.opHashin both thestrategyDepositandstrategyWithdrawfunctions. This will keep the value unique even when actions are made within the sameblock.timestamp.Or reference the Arbitrum block number instead of block.timestamp. This will ensure a unique hash on every order.
-
H-01 High GM Strategy Token Weight Is Errantly Determined Logical Error Partially resolved
Description
The GM index composition of the
jGMvault is monitored to ensure it remains within a predetermined range. This is crucial to marinating a healthy GM system and protocol robustness.The target weight is read directly from the
GMStrategycontract using thetokenWeightfunction. This function incorrectly calculates the weights and returns them as 30 decimals values, instead of less then 12 decimals, as they are intended.The error appears when multiplying the in-question Strategy GM token balance value, by omitting to divide by
1e18uint256 currentValue = IERC20(gm.token).balanceOf(address(this)) * GMPrice(gm.oracle, gm.stalePeriod);Since this value is instrumental in maintaining the balance of the system and triggering a rebalance, it is crucial to have it correctly determined.
Recommendation
Divide the
currentValuewith1e18before dividing it to the total value. -
H-02 High Withdraws Over Leverages The System Logical Error Acknowledged
Description
When a user initiates a withdraw from the system via the
LeverageRouter.createWithdrawalfunction, execution reaches theLeverageStrategycontract where the current leverage is checked in order to determine if there is a need to deleverage.This check is incorrectly implemented because instead of comparing if the current leverage is higher then target leverage, it checks if it is higher them maximum leverage. This means that even if the system is above the target leverage, until it reaches the maximum no automatic actions will be taken.
uint256 maxLev = getMaxLeverage(); // ... if (currentLev > maxLev) {With the current implementation there is always a tendency to over-leverage the system and always be above target leverage. This reduces the overall system health introduction economical risk to users.
Recommendation
Change withdraws that if they are above target leverage to deleverage.
-
H-03 High Incorrect Underlying jGM Calculations Due To Fees And Slippage Logical Error Partially resolved
Description
Throughout the protocol, in the
LeverageStrategycontract there is a_getjGMToPaybackfunction which is used to estimate how muchjGMtokens should be withdrawn in order to get the passed amount of USDC (debt).function _getjGMToPayback(uint256 _debt, uint256 _totalUSDValue) private view returns (uint256) { return GMStrategy.addSlippage(GMViewer.getPreviewDeposit(_debt, _totalUSDValue)); }This function retrieves the required amount and increases it with the set GM Strategy slippage (defaulted to 0.5%). This is done to ensure that deposit with swaps that reach the slippage limit would still work since the withdraw amount has taken this into consideration.
There are 3 issues with the current implementation:
- the amount increase due to possible slippage is added on the entire amount, where, on GMX withdraws, the liquidity is returned in 50% short token (USDC) and 50% long tokens. As such, only the 50% liquidity in long token is swapped and should apply the slippage increase.
- the amount increase to equivalent fee taking is incorrectly added. Since out of the given amount a percentage % would be withdraw, we need have the amount increase as
amount = amount * BASIS / (BASIS - slippage)so that when the fee is applied, the exactamountvalue is returned.addSlippageincorrectly adds the %, resulting in less tokens.
Example scenario: Say the swaps reach the slippage limit of
0.5%, meaning that99.5%ofamountwas returned. For an1000initial amount, that was increased with0.5%means1005and99.5%of1005is999.975, less then1000. The correct way is to have the amount increased as:1000 * 100/(100 - 0.5) = 1005.025125and out of that amount, if we get only99.5%we then get exactly1000- The third issue is that, in order to correctly simulate a GMX withdraw (position close), a fee must also be added to also simulate this. The current GMX position open or close fee is
0.07%in thejGMcase, because it only deposits or withdraws one pair. This fee must also be taken into consideration for exact calculations by applying it in the same manner.
Because of the way the current
_getjGMToPayback, and implicit leverage is calculated, overall system appears more leveraged then it is in reality.Recommendation
After calculating the pure amount required to withdraw, increase it, using the above mentioned formula, to compensate the GMX fee. On half of the resulting amount increase, again using the above formula, the amount to compensate for the swap slippage requirement. Finally return the sum of first half and the increased half as the underlying.
-
H-04 High Depositing After Unwind Reverts Validation Resolved
Description
When the governance or keeper deleverages the system by calling
LeverageStrategy.unwind, a withdraw order is created, passing all thejGMbalance as shares.By the end of this operation, the
LeverageStrategycontract is left with nojGMshares. However, theLeverageVaultshares owned by the users are left untouched.Due to this reason, when an user follows through with a deposit, the operation will work as intended and eventually reach
depositCallback, where it will revert, as thejGMshares to be minted are multiplied by the total supply ofLeverageVaultshares, which won't be zero, and divided bycallback.underlyingjGM, which will be zero after an unwind.The contract is left at action state 2, and although the operator of
jGMIndex.soland the governance can solve this issue by directly minting some shares to the leverage strategy contract, and governance can enforce the action back to 1, deposits and withdrawals will stop working until then.Recommendation
Change the check at the provided line to the following:
if (callback.supply > 0 && callback.underlyingjGM > 0) { -
H-05 High Execution features may be disabled Validation Partially resolved
Description
The GMX Keeper has the authority to disable specific features when necessary, such as executing deposits and withdrawals. If these features are disabled, attempts by keepers to execute orders will result in reverts, causing the orders to be canceled. Subsequently, if the JGM keeper tries to recreate the deposit with altered parameters for a successful order, it will also fail due to the disabled feature.
This situation presents a critical issue as JGM users will be unable to interact with the protocol entirely, even if only one of deposits or withdrawals is disabled. This scenario is particularly concerning because if deposits are disabled, users should ideally withdraw their funds promptly. However, whether intentionally or unintentionally, a user can effectively halt all protocol operations by initiating a deposit or withdrawal while the execution functionality is disabled.
Recommendation
Implement checks in the deposit and withdrawal functions to verify that deposit executions are enabled for deposits and that withdrawal execution is enabled for withdrawals. These checks will ensure that users can only perform these actions when the respective execution features are active, preventing disruptions to protocol operations due to disabled functionalities.
-
H-06 High User can always use stale price Validation Partially resolved
Description
To deposit and withdraw funds as well as for many other functions the system receives a signed data param that includes price data for calculations. This data param is valid for a given amount of time after it was signed.
An attacker can abuse this by constantly fetching the signed data param until an event occurs that leads to dangerous stale data in less than 60 seconds.
Attack flow:
- Attacker constantly fetches a signed data param every few seconds
- A big price change occurs for example a trader takes a large profit on GMX (this could be the attacker or anyone else)
- The attacker uses the stale signed data param to deposit or withdraw funds and profits from the price change
This attack can always be done to extract value from the other users, however only profitable for the attacker when the gain from using stale data outweighs the fee charge for doing so.
Recommendation
Consider fetching price data on chain to ensure accurate pricing.
-
M-01 Medium Manipulated swap pool will cause DoS on withdrawals DoS Resolved
Description
When there is any price manipulation in a pool that is being used to swap the long token for the short token the swap attempt will revert. This is good in most cases, however when the swap fails in the
afterWithdrawalExecutionfunction it will revert and result in theoperationOnGoingvariable being stuck as true.An attacker can exploit this by performing a large swap between the creation and execution of the withdrawal. Causing the before mentioned revert and preventing any further operations. In addition to an attacker specifically targeting JGM, any large swap(s) will cause this situation. This includes non malicious swaps. As soon as there is a price deviation of 0.1% or greater the price manipulation check will fail.
Recommendation
Because the
_isInRangefunction will ensure enough underlying was received, use a swapper contract that does not check for manipulation, as the_isInRangefunction will ensure an acceptable amount of assets are returned. -
M-02 Medium GMRouter Deposits And Withdraws Are Not EIP712 Compliant Logical Error Partially resolved
Description
When initiating a deposit or withdraw through the
GMRoutercontract, a signature must also be passed that will be checked in the_verifyParamsto be from the correct signer which will also validate the provided input data.The router inherits the
EIP712Upgradeablecontract but it does not properly handler the signature verification in relation to the EIP-712 standard. The typeHash of theIGMRouter.Datastructure is never calculated when it is verified in_verifyParams:address _signer = ECDSA.recover(_hashTypedDataV4(keccak256(abi.encode(nonce, keccak256(abi.encode(params))))), signature);As such, while the operations work, the transaction lacks the clarity which comes to the EIP. Depending on the operator, it may be deter usage.
Recommendation
Correctly implement the EIP-712 verification by adding the typeHash while also adding the nonce in the signature. The structure itself can have the nonce and it is to be checked to be the "next up" after the validation succeeds. As the EIP indicates, the
typeHashof theIGMStrategy.GMDataitself must also be pre-calculated and included in theIGMRouter.Data. -
M-03 Medium Potential Tokens Trapped On GM Strategy Deposits Validation Partially resolved
Description
When an operator deposits into the GM strategy through the router, a specific amount of USDC and native ETH is transferred to the strategy and then the strategy is notified of the deposit amounts
///@notice Transfer Assets to Strategy USDC.transferFrom(msg.sender, address(strategy), _assets); ///@notice Trigger Deposit in Strategy strategy.strategyDeposit{value: msg.value}(_receiver, _assets, shares, data.gmData);The strategy then incorrectly never uses the
_amount(amount of USDC to been deposited) argument or themsg.valuereceived to determine if there were send more or less tokens.gmxRouter.sendWnt{value: _data[i].executionFee}(gmxDepositVault, _data[i].executionFee); gmxRouter.sendTokens(USDC, gmxDepositVault, _data[i].amount);If less then indicated native ETH (
msg.value) was sent or lessUSDCthe operation would revert, but if more is sent then the funds remain blocked in the strategy contract untilemergencyWithdrawis called by the contract owner. During this time, any potential yield from the leftovers is also lost.Recommendation
Before iterating through the
GMDataentries, in thestrategyDepositfunction, cache themsg.valueand_amountamounts and on each iteration decrease both cached values. If at the end of the entire function call these amounts are both not 0 then too much tokens were sent and the operation should either revert or send the extra tokens to thereceiver. -
M-04 Medium Executing Partial Or Premature Pending Operations Blocks Further Rebalances Logical Error Resolved
Description
When a rebalance is started, a number of GMX withdraws are first initiated. If any fail, then they are added to a pending withdraws array. Then they can be retried via the
executePendingWithdrawfunction. The same logic applies when, at the last stage of a rebalance, the new deposits are done but by callingexecutePendingDeposit.Both
executePendingWithdrawandexecutePendingDepositdo not clear the pending operations after created, as such if any one of the following situation happens then the pending operation cannot be further retried and funds remain blocked in the case of deposits:- the
executePending*functions are called before all operations that fail are stored
Example scenario: 4 deposits are attempted, 2 succeed, 1 fails, and before the last one will also fail
executePendingDepositis called. Because of the check thatpendings[i].amount != _deposits[i].amount, and the issue that thependingsarray is never cleared, another call will will revert since the pending[0].amount (which was already sent to GMX on the first call) is again attempted again to be sent to GMX, resulting in either a revert there or on the next pending deposit.- the
executePending*functions are called with a corresponding withdraw/deposit array less then the total pending operations.
Exactly as the above case, and due to a missing check that either
GMDepositorGMWithdrawarrays are the same length as the pending operations, calling the function once works but calling it a second time will revert it.In the case of withdraws,
withdrawCounterdoes not reach zero, and callingafterWithdrawalExecutionvia keeper to unblock rebalancing cannot be done since thekeyentry from thekeyToRebalancemapping has already been deleted inafterWithdrawalCancellation. Leaving the GM tokens blocked in the contract and no way to go to the next rebalance.In the case of deposits, similar as with withdraws,
pendingDepositsdoes not reach zero on its own and callingafterDepositExecutionby the keeper does not help as thekeyToRebalanceentry has already been deleted inafterDepositCancellation. In this case USDC also remains blocked in the contract as well as the rebalance blocked.In both cases funds are lost, rebalances are frozen leaving the team with the only option to initialize a contract upgrade.
Recommendation
In both
executePendingWithdrawandexecutePendingDepositfunctions, remove the element from the pending arrays. Add an emergency withdraw function in case similar, unforeseen situations appear. - the
-
M-05 Medium Trapped ETH On keeperPayBack Call Validation Partially resolved
Description
When a keeper calls the
keeperPayBackfunction from theLeverageStrategycontract, if the contract does not have enough stables, a GMX withdraw routine will be initiated. Within that GMX withdraw routine, several GM tokens may be withdrawn. For each GM token, the keeper must provide enough native ETH so that GMX's own keeper finalizes the withdraw.With regards to the way native ETH is handled in this function, there are 2 issues:
- there is not check that more ETH was not mistakenly sent, then the required
gmxIncentivevalue - if the function is called but, by coincidence there are enough stable tokens then the function secedes any ETH sent to it by the Keeper remains blocked, until it will be extracted via the
emergencyWithdrawfunction.
Recommendation
Before calling the
GMRouter.createWithdrawalfunction, validate that the msg.value is equal to the calculatedgmxIncentiveamount. If the functions determines that there are enough stables in it (expectedStables == 0) then either revert or send the native ETH back to the caller. - there is not check that more ETH was not mistakenly sent, then the required
-
M-06 Medium Leverage Down On Zero Underlying Leaves Action Uncleared Logical Error Resolved
Description
When a keeper calls the
leverageDownfunction, if the system has no underlying tokens, the execution of the function ends while leaving the action set to 5.Having the action uncleared blocks all other leverage strategy operations and requires governance intervention to fix via calling the
enforceActionfunction to reset the flag.Recommendation
Set the action state to 5 (leverage down) only just before calling the
GMRouter.createWithdrawalfunction. -
M-07 Medium New Leverage Incorrectly Calculated During Leverage Up Logical Error Resolved
Description
The keeper has the ability to increase the leverage by borrowing more tokens and depositing them into GMX via the
leverageUpfunction. The new leverage can not be smaller than the old leverage, and it can not be larger than the maximum leverage.During this function, the
oldLeverageis calculated with the current values, and thenewLeverageis calculated with the future values (expected values after the execution is completed).uint256 currentBalance = jGMIndex.balanceOf(thisAddress) + newjGM; if (stableDebt > 0) { uint256 jGMNeeded = _getjGMToPayback(stableDebt, _data.data.usdTotalValue); underlying = currentBalance > jGMNeeded ? currentBalance - jGMNeeded : 0; } else { underlying = currentBalance; } uint256 newLeverage = (currentBalance * BASIS_POINTS) / underlying; // 12 Decimals;currentBalance,stableDebtandusdTotalValueare three main values required while calculating thenewLeverage.The
currentBalanceis an updated value with the previewednewjGM. ThestableDebtvalue is also updated before this calculation. However, the_data.data.usdTotalValueis still the current value, not the future USD value after this execution is completed.The deposit of these borrowed funds will increase the
usdTotalVale, which will affect the actual leverage, but it is not considered.As a result of this, the calculated
newLeverageduring this function and the actualleverageafter executing this function will be different and may result in reverting the transaction when the old/new leverage validations are not passed.Recommendation
Use the expected
usdTotalValuewhile calculating thenewLeveragefor it to be precisely correct. -
M-08 Medium Value Extraction By Withdrawing After A Pay Back Logical Error Partially resolved
Description
Users create withdrawals by interacting with the
LeverageRoutercontract. Users provide how manyjGMshares to burn during withdrawal, and thecreateWithdrawalfunction calculates the correspondingjGMIndexamount, which will later be burned during GMX interaction and the user will get corresponding USDC.jGMIndexamount is calculated with this formula:if (supply > 0) { _jGMIndex = _shares.mulDivDown(leverageStrategy.getUnderlyingjGM(data.data.usdTotalValue), supply); // 18 decimals }The result of the
getUnderlyingjGMfunction directly impacts the amount user will get, and this function is directly affected by thestableDebt. Higher debt means less underlying tokens and vice versa.The
payBackfunction in the strategy contract decreases thestableDebt. Because of that, withdrawing an amount ofjGMshares after apayBackresults in more USDC compared to withdrawing before.An attacker can abuse this if he knows when a pay back will be made and attackers can predict when a pay back will happen.
The protocol has a
keeperPayBackfunction, which is called to increase stable token balance before paying the debt. Since this function withdraws stable tokens from the GMX, there will be a lag betweenkeeperPayBackand the actualpayBack.- Attacker sees the
KeeperPaybackevent is emitted duringwithdrawalCallback. - Immediately deposits in the next block.
- The Jones operator calls the
payBackand pays the debt. - Attacker withdraws after pay back and profits.
Recommendation
The lag between
keeperPayBackand thepayBackgives chance to attackers to time their actions and deposit in between. Removing this lag would prevent the issue. Invoke theIUnderlyingVault.payBackfunction during thewithdrawalCallbackwhen theaction == 8with appropriate parameters. - Attacker sees the
-
M-09 Medium Admin cannot change operationOngoing when stuck Logical Error Resolved
Description
When the protocol is put into a state in which
operationOngoingis stuck to true users will not be able to interact with the protocol. Additionally there is no guarantee that retying a deposit or withdraw will work. In which case users will not be able to interact with the protocol for a prolonged period of time.Recommendation
Consider implementing an admin function that allows the protocol to change
operationOngoingto true when the protocol is stuck and doing so is safe. However, it is important to note that the current deposit/withdraw would be lost as the following order will override the users callback data. -
M-10 Medium Jones does not collect from jonesRate when protocolRate is zero Logical Error Resolved
Description
When the
protocolRateis zero the protocol will not be able to collect fees fromjonesRateusers withdraw. Because of this, the protocol will loose out on fee revenue.The reason the fees are not collected when
protocolFeeis zero is because there is a check that requires bothprotocolRateandjonesRateto be non-zero. This is because the current calculation would setjonesRetentionto zero if either of the variables were zero.However, even if the protocol does not have a
protocolRate, it should still be receiving thejonesRateportion.Recommendation
To fix this add an else statement that calculates
jonesRetentionbased on justjonesRate.else { jonesRetention = _usdc.mulDivDown(jonesRate, BASIS_POINTS); } -
M-11 Medium Users cannot borrow when stable vault is low on borrowable funds Logical Error Partially resolved
Description
When a users deposit amount exceeds the amount of funds available in the stable vault deposits will not be possible. This situation can present itself when the stable vault is low on borrowable funds or when users make large deposits while
JGMis below target leverage.The issue with this is that there are still borrowable funds available and by reverting JGM will not be able to borrow when they should. And more importantly users would not be able to deposit into the protocol.
Recommendation
Instead of reverting when
stablesToBorrowis greater thanavailableForBorrowing. Truncate the amount borrowed to what is available.if (availableForBorrowing < stablesToBorrow) { stablesToBorrow = availableForBorrowing; } -
M-12 Medium Deterministic keeper actions can be gamed Logical Error Acknowledged
Description
Because actions such as calling the
harvestorcompoundfunctions are deterministic in the sense that a user knows at a certain threshold these function will be called. A user can extract value from other users by making timely deposits or withdrawals right before these functions are called.This is because both of these function alter the share price. When the
harvestfunction is called the share price will decrease as yield is paid to JUSDC. With the yield leaving the JGM protocol the value will decrease. A user can game this function by withdrawing funds right before the keeper callsharvestand then deposit right after. What this does is protect that user from the drop in share price by putting their burden on the other users.When
compoundis called it will increase the share price by converting the unaccounted for incentive token into more GM tokens. As the amount of GM tokens increase so will the share value. A user can game this by depositing right beforecompoundis called and withdraw right after. By doing this they are profiting off the compound function even though that yield should be going to the users who's deposits earned that yield.In both cases users can game the system and extract value from other users.
Recommendation
Ensure that keepers action are done at a time when performing such gaming strategies are not profitable. The thresholds for both
harvestandcompoundshould be low enough the fees outweigh the profit. -
L-01 Low Strategy will payback users underlying when there is insufficient debt Logical Error Resolved
Description
When a user withdraws from the stable vault, the
paybackfunction in theLeverageStrategycontract is called to decrease thestableDebtby the amount of USDC removed from the strategy. This process works correctly in most cases. However, if the amount being paid back exceeds thestableDebt, thestableDebtvariable is simply set to 0. Consequently, the full USDC amount is transferred to thestableVaultcontract, but a portion of that USDC comes from users' collateral instead of the originally borrowed funds. This results in a loss of funds for all users involved.Recommendation
Consider clearly documenting to users that in rare cases, the stable vault may utilize users' collateral when there's insufficient debt, particularly when a JUSDC user makes a withdrawal. This transparency helps users understand potential risks and ensures they are aware of how their funds may be utilized under specific circumstances.
-
L-02 Low Misconfigured handler addresses will lead to failed callbacks Validation Resolved
Description
In rare cases GMX can change the handler contracts if code needs to be added or removed. When this happens the handler address used by GMX will differ from the handler contract used by JGM. Because of this, deposit and withdraw creations will succeed, but the callback will fail leading to a temporary DoS until a keeper retries the callback.
In addition, in rare cases it is possible for GMX to have multiple handler contracts for a short period of time. When this happens if the handler contract that is not whitelisted performs the deposit execution the callback will fail as the
_onlyWithdrawHandlerOrKeepercheck will fail.Recommendation
Consider validating the role of the msg.sender in the RoleStore, e.g. RoleStore.hasRole(msg.sender, Role.CONTROLLER), this would check that the msg.sender is a valid handler.
-
L-03 Low Missing check if action does not equal 1 in Keeper PayBack Validation Resolved
Description
Every function in the
LeverageStrategycontract has a check at the beginning that there is no running action. Except for thekeeperPayBackfunction, which is missing this check.Under normal circumstances, this should not be a problem as the transaction should still revert because of the
operationOnGoingcheck in theGMStrategycontract. However, if theoperationOnGoingcheck does not work as expected the keeper bot could unintentionally overwrite another operation by callingkeeperPayBack.Recommendation
Add this check
if (action != 1) { revert InvalidAction(); }to thekeeperPayBackfunction. -
L-04 Low Swapper Incorrect Allowance Handling Logical Error Resolved
Description
The
UniswapV3Swappercontract is used when any token swap is needed in the protocol. It incorrectly uses thesafeIncreaseAllowanceandsafeDecreaseAllowancefunctions when working with tokens as if they would set the allowances, not increase/decrease it.The swap, initiated through the
swapfunction will always work, since on every call the allowance is increased with the given amountIERC20(tokenIn).safeIncreaseAllowance(address(V3_ROUTER), amountIn);but it will never decrement it, since it decreases it with 0, instead of setting it to 0.
IERC20(tokenIn).safeDecreaseAllowance(address(V3_ROUTER), 0);This results in an continuous growth of allowance in the swapper contract.
Recommendation
Use the SafeERC20.forceApprove function when setting and removing approval.
-
L-05 Low Protocol Withdraw Incentives Cannot Be Changed Logical Error Resolved
Description
On user withdraws, there are fees that are taken. These fees currently are only set on initialization and cannot be further changed. Depending on economical factors, the fees might need to be changed.
Recommendation
Add a
updateIncentivesfunction in theLeverageStrategycontract whereprotocolRate,jonesRateandincentiveReceivercan be changed. -
L-06 Low Keeper Pay Back Functionality Overwrites Current Action Logical Error Resolved
Description
When a keeper calls the
keeperPayBackfunction from theLeverageStrategycontract, if the required stable token amount is greater then available in the contract, a GMX withdraw is created.The function however, incorrectly, does not check if there is already an action pending. As such, calling the
keeperPayBackfunction during any already existing withdraw sets the action to 8 and when the first withdraw is finished, the action to be taken is that specific for thekeeperPayBack, which is just emitting an event.Also, the initial
keeperPayBackwithdraw would reach the withdraw callback with an action of 1 which will lead if on the code-path to the Liquidate behavior. Basically functionality is reversed.Recommendation
In the
keeperPayBackfunction, if theexpectedStablesare greater then 0, check that the current action is 1 (idle) otherwise revert the operation. -
L-07 Low Fee Rates Can Dos Protocol Logical Error Resolved
Description
Throughout the system there are fees, both on deposits and on withdraws. The fees can be errantly set to over 100% BPS leading to full protocol DOS via underflow calculations.
This applies to deposit rate fees, protocol date and Jones rate.
Recommendation
Whenever setting the fee rates, validate that they are not over the equivalent of 100% in basis points.
-
L-08 Low Deprecated Chainlink Staleness Check Logical Error Resolved
Description
Chainlink has deprecated the
answeredInRoundreturn variable for itslatestRoundDataprice feed function and its usage is now discouraged.This variable comes from a legacy version of Chainlink and it can be safely ignored.
Recommendation
Remove the check.
-
L-09 Low Possible Trapped ETH In Routers Logical Error Resolved
Description
The
LeverageRouterandGMRoutercontracts allow receiving of direct ETH through the existence of a payablereceivefunction. There is no functionality within the contract that uses any stored ETH. Also, there are no emergency withdraw functions in case ETH is wrongly sent to the contract.Recommendation
Remove the
receivefunction. -
L-10 Low Leverage Down Reverts On Zero Underlying Logical Error Resolved
Description
When the system is over leveraged, the keeper logic determines the need to leverage down and calls the
leverageDownfunction.If this function is called when the system is over-leveraged to the point that the entire strategy
jGMbalance would be needed to pay back the debt (leaving 0 underlying) or when the underlyingjGMbalance is directly 0, then the function reverts unexpectedly with a divide by 0.uint256 oldLeverage = ((currentBalance * BASIS_POINTS) / underlying);Depending on how the implementation of the
leverageDownfunction is intended, the above issue would either render it prematurely revert (instead of gracefully exit) or block such logic.Recommendation
Either change the way the old leverage is calculated or move the underlying and stable debt validation before using it.
-
L-11 Low Allowances Should Be Cleared While Updating GMX Variables And Swappers Logical Error Resolved
Description
During the initialization of the
GMStrategycontract, the swapper and the GMX router contracts were approved with infinite allowances to spend market tokens, long tokens and USDC.The owner of the
GMStrategycontract has the ability to update default swapper and GMX router. However, allowances of the previous contracts are not cleared during this update.The owner must separately call the
approveAssetsfunction with a 0 value to clear these previous allowances. In case of the owner not clearing these approvals manually, these allowances can be abused.Recommendation
Clear the previous contracts’ allowances inside the update functions.
-
L-12 Low Leverage Strategy Should Not Be Updated During An Operation Logical Error Resolved
Description
When a withdraw/deposit is done to the
GMStrategy, if there is a leverage strategy set, it will call specific callbacks on it. The owner of the contract can change this contract address via theupdateLeverageStrategyfunction.If the address is changed during a withdraw/deposit operation (
operationOnGoing) then users that may have initiated the action through the leverage contract will have their operations blocked and, in some cases, their leveragejGMnot minted to them, causing loss of funds.Recommendation
Do not allow the
updateLeverageStrategyfunction to be called when there is an operation on-going. -
L-13 Low Rebalance Purchases Can Be Done In Multiple Stages Validation Resolved
Description
When a rebalance reaches stage 3: Withdrawal Finish, then the
purchaseRebalancefunction can be called to initiate GMX deposits.The
purchaseRebalancefunction incorrectly allows calls from both stage 3 and stage 4: Start Purchase Stage. After the first call to stage 3, it takes the rebalance to the next stage, but any subsequent calls do not have stage 3 as a requirement, only stage 4.Because of the above implementation, multiple calls to the function can be done.
Recommendation
If the described behavior is intended clearly document it. If only a call within stage 3 is allowed then modify the function to require it being called in stage 3.
There also should be a mechanism to be sure that the
USDCthat was retrieved from the withdraws is not lost within the contract, thus the contract balance ofUSDCmust be 0 at the end of the call, otherwise revert. -
L-14 Low Incentive Not Accounted For on Payback Stables Availability Validation Resolved
Description
When the leverage strategy needs to pay back a part of its debt to the stable vault, the
payBackfunction is invoked by an operator. This function checks the stable token balance of the leverage strategy contract, checks if the balance is enough to pay the debt, and calls theIUnderlyingVault.payBack, which will transfer tokens.In the
LeverageStrategy.payBackfunction, theIUnderlyingVault.payBackis called with two parameters:amountanddata.gmxIncentive. Here, the transferred token amount fromleverageStrategycontract to stable vault isamount - data.gmxIncentive, not the wholeamount.The incentive amount is not transferred during this call. However, this incentive amount is not accounted while checking whether the contract has enough balance to pay the debt.
Example scenario:
amount: 1000 USDCstrategyStables: 950 USDCdata.gmxIncentive: 100 USDC
A payback call in this scenario will revert with
NotEnoughStableserror due to this check. However, amount to be transferred in this call is only 900 USDC, and the contract has enough balance to pay it.Recommendation
The incentive amount should be accounted alongside with the contract balance while checking the contract's ability to pay back its the debt.
-
L-15 Low Strategy Total Value Does Not Reflect Rebalancing Amounts Logical Error Resolved
Description
When a rebalance is initiated, the funds to be rebalanced are taken from the
GMStrategyand sent to theRebalanceStrategycontract until the rebalance is done, before sending them back to theGMStrategy.During this time, the GM token weight and total USD value of all GM tokens in the GM Strategy do not reflect reality. Any 3rd party integration relying on it will produce invalid results in their own logic.
Recommendation
Modify the
GMStrategycontract so that during a rebalance, the funds that left the contract are still accounted when determining the token weight and total USD value (via the functionstokenWeightandtotalValue).Specifically in the
rebalanceInputfunction, save the moved GM tokens in a mapping and reuse that mapping in thetokenWeightandtotalValuefunctions by adding the saved values to the current strategy balance. When a rebalance is finished, clear the mapping.
Remediation Review
20 findings · April 21 to 24, 2024-
C-01 Critical User Withdrawals Are Accounted As Stable Debt Payment After A Failed Swap Logical Error Acknowledged
Description
When a user initiates a withdrawal, necessary funds are withdrawn from GMX to
GMStrategycontract, the long token is swapped to USDC in theafterWithdrawalExecutioncallback and this value is stored. When all callbacks are done and thecallbackCounterreaches to zero, withdrawn funds are transferred to theLeverageStrategycontract, theLeverageStrategy.withdrawalCallbackis called, and finally, theoperationOnGoingflag is set to false.The swap is done in a try/catch block to prevent a DoS issue and
LeverageStrategyaction is set to 1 if the swap fails. However, setting the action to 1 will causes problems if the swap fails for any reason during a legitimate withdrawal by a regular user.If the swap fails when the
callbackCounterwas not zero, theLeverageStrategyaction will be set to 1, butoperationOnGoingin theGMStrategywill still be true, which creates a mismatch between two contracts. After this point, when the remaining callbacks completed, if the resulted USDC amount is in the slippage range,LeverageStrategy.withdrawalCallbackwill be called but due to the action was already set to 1, the transaction flow will enter this else block. User’s withdrawal will be considered as a debt payment instead of a withdrawal and all the funds withdrawn will be sent to the stable vault. If the USDC amount is not in the range, the function will revert, withdrawal can’t be completed and theoperationOnGoingflag will not be set to false.Recommendation
Because of the way GMX callback work in tandem with the
jGMreverts should be avoided altogether but, since swap reverts cannot be avoided, a safe compromise is:- in the catch branch of the try-catch
- emit an event (for off-chain logic)
- since the swap failed, the current logic is to revert but that simply blocks the protocol. Create a mapping with the non-swapped tokens amounts due to the user had allow the user, at will, to claim the tokens. An option is to also allow the keeper to call the function to send the failed-to-swap tokens to the user (or swap the before-hand), that can be done immediately. It is not a good design to simply revert, but our initial recommendation was faulty.
- also set in the try-catch a flag to skip the
_isInRangecheck, Without this extra step, the transaction will likely revert since after the final GM swap, the check will most likely fail
- in the catch branch of the try-catch
-
C-02 Critical Attacker Can Repurpose Signatures To Damage Protocol Logical Error Acknowledged
Description
The entire original description is still valid, with the observation that having amounts in the signature is not suffice, since deposits use USDC that has 6 decimals and withdraws use GM tokens that have 18 decimals, adding the operation type is also needed.
An attacker can still have a signature generated for one operation (e.g. deposits) and initiate it for withdraws, closing all the
GMStrategyGMX positions.Recommendation
From the original recommendation, the following are still required:
- add operation type (use an Enum for operations) to
IGMRouter.Datastructure (requirement) - in the
strategyDepositfunction from theGMStrategycontract, validate that the passedUSDC_amountis equal to that used to buy all the GM tokens (extra validation)
- add operation type (use an Enum for operations) to
-
C-03 Critical USDC Withdraw Amount Not Updated After Protocol Trim Logical Error Acknowledged
Description
When a user initiates a withdraw the
_isInRangefunction checks if the withdrawnUSDCamount after the GMX execution is above the ideal amount, and if so, the surplus is send to the protocol treasury.This function, however, incorrectly does not return the trimmed value in that case, and returns the same value as passed, the
_actualUSDC. Since the difference between the actual amount and idea was sent to the protocol treasury, after exiting the_isInRangecall, the incorrect USDC amount is then sent to the user meaning that that different is again taken from the contract.If the
GMStrategycontract does not have enough stables to do the withdraw, then theIERC20(USDC).transfer(data.user, data.usdc);transfer will revert, leaving theoperationOnGoingflag set to true and subsequent problems. An attacker can also time this abuse when the contract is low on stables effectively forcing a revert.Recommendation
In the
_isInRangefunction, after the USDC transfer to the treasury, set the_actualUSDCvariable with the_idealUSDCvariable. -
H-01 High GM Strategy Token Weight Is Errantly Determined Logical Error Acknowledged
Description
The GM index composition of the
jGMvault is monitored to ensure it remains within a predetermined range. This is crucial to marinating a healthy GM system and protocol robustness.The target weight is read directly from the
GMViewercontract using thegetGMWeightfunction. This function incorrectly calculates the weights and returns them as 30 decimals values, instead of less then 12 decimals, as they are intended.The error appears when multiplying the in-question Strategy GM token balance value, by omitting to divide by
1e18uint256 currentValue = IERC20(gm.token).balanceOf(address(this)) * GMPrice(gm.oracle, gm.stalePeriod);Since this value is instrumental in maintaining the balance of the system and triggering a rebalance, it is crucial to have it correctly determined.
Recommendation
Divide the
currentValuewith1e18before dividing it to the total value. -
H-02 High Wrong key used to check if execution is disabled. DoS Acknowledged
Description
Currently createWithdraw uses
CREATE_WITHDRAWAL_FEATURE_DISABLEDbut this only checks if the creation is enabled.Recommendation
Instead to prevent this attack use
EXECUTE_WITHDRAWAL_FEATURE_DISABLED -
H-03 High Attacker can donate USDC to halt rebalance. DoS Acknowledged
Description
Because the
purchaseRebalancefunction checks that the balance of the contract is 0 and reverts if this is not the case an attacker can drip a dust amount of USDC during a rebalance to prevent the rebalance from continuing leaving the protocol in a stuck period where no one can use it.Recommendation
Remove the check:
if (IERC20(USDC).balanceOf(thisAddress) != 0)Instead use the keepers to calculate the right amount to deposit based on the contracts balance.
-
M-01 Medium Incorrect Underlying jGM Calculations Due To Fees Logical Error Acknowledged
Description
Throughout the protocol, in the
LeverageStrategycontract there is a_getjGMToPaybackfunction which is used to estimate how muchjGMtokens should be withdrawn in order to get the passed amount of USDC (debt).function _getjGMToPayback(uint256 _debt, uint256 _totalUSDValue) private view returns (uint256) { uint256 _jGMIndex = GMViewer.getPreviewDeposit(_debt, _totalUSDValue) / 2; return (GMStrategy.addSlippage(_jGMIndex) + _jGMIndex).mulDivDown(10007, 10000); }This function retrieves the required amount and increases half of it with the set GM Strategy slippage (defaulted to 0.5%). This is done to ensure that deposit with swaps that reach the slippage limit would still work since the withdraw amount has taken this into consideration. Adds the other half and then increases the overall amount by 0.07%
Adding the
0.07%fee, specific for GMX deposits or withdraws on one pair is done incorrectly because the amount increase to equivalent fee taking is incorrectly added. Since out of the given amount a percentage % would be withdraw, we need have the amount increase asamount = amount * BASIS / (BASIS - slippage)so that when the fee is applied, the exactamountvalue is returned.mulDivDown(10007, 10000)incorrectly adds the 0.07%, resulting in less tokens.Because of the way the current
_getjGMToPayback, and implicit leverage is calculated, overall system appears very slightly more leveraged then it is in reality.Recommendation
Add the amount increase to compensate GMX fee in the manner of the above described formula. Also consider setting the GMX fee through a contract setter then hard-coding it.
-
M-02 Medium Deposits, withdraws And RetentionRefunds Are Not EIP712 Compliant Logical Error Acknowledged
Description
When initiating a deposit, withdraw or validating the retention fund GMX incentive amount, callers provide a signature must that will be checked in the
_verifyParamsto be from the correct signer which will also validate the provided input data.The contracts that use this functionality, both
LeverageStrategyandGMRouterrouter inherit theEIP712Upgradeablecontract but it do not properly handler the signature verification in relation to the EIP-712 standard.In case of the
GMXRouterthedataTypeHashis incorrect because it includes theuint256 usdTotalValuewhich has been removed from theIGMRouter.Datastructure. Also, within the same type string, change theIGMStrategy.GMData[] gmDatatoGMData[] gmData.On retention refunds, validation is done on a direct types, completely lacking a typeHash or structure to hold the information.
address _signer = ECDSA.recover(_hashTypedDataV4(keccak256(abi.encode(nonce + 1, gmxIncentives))), signature);Recommendation
Correctly implement the EIP-712 verification by adding a structure for the GMX incentive and create the
typeHashon it. Fix the underlying issues with thedataTypeHashas mentioned in the description. -
M-03 Medium jUSDC Payback Through keeperPayBack Does Not Use Strategy Balance Logical Error Acknowledged
Description
When a jUSDC payback is initiated through the leverage strategy
keeperPayBackfunction, only the amount resulted from withdrawing GM tokens is paid back, even though thekeeperPayBackitself checks and initiates the withdraw only for the amount that it lacks.Because of the above issue, coupled with the lack of a working direct
paybackfunction, the stable from the strategy can never be used in paying back. This severely impacts jUSDC functionality and leverage strategy debt management.Recommendation
In the
withdrawalCallbackon action 8, get the current contract USDC balance, subtract thegmxIncentivesfrom it, and pay it back. It already includes the_usdcamount, as it was transferred to the contract when the GMX callback was executed. -
M-04 Medium jUSDC Cannot Enforce Payback Logical Error Acknowledged
Description
The jUSDC vault has a functionality of enforcing the strategies that borrowed from it to payback a part of the debt. This is done through the
payBackcallback implemented in each strategy.The
LeverageStrategycontract has thepayBackfunction reduced to areturn 0;and all payback functionality was migrated through thekeeperPayBackfunction.This presents a severe issue within the Jones ecosystem as enforcing payback must also be allowed through the normal, jUSDC channel.
Recommendation
Add the payback code back to the function.
-
M-05 Medium Hardcoded slippage Logical Error Acknowledged
Description
Currently the
_getjGMToPaybackhardcoded the fee amount as 0.07%. GMX has the ability to change the fee and when that happens the_getjGMToPaybackwill be wrong leading to a misleading payback amount.Recommendation
Don't hardcode the fee so that admin can change it.
-
M-06 Medium Pool is not always 50/50 leading to bad calculations. Logical Error Acknowledged
Description
Currently the
_jGMIndexis calculated as followsuint256 _jGMIndex = GMViewer.getPreviewDeposit(_debt, _totalUSDValue) / 2;The issue here is that pools are often not 50/50. Because of this the calculation will either overestimate or underestimate the amount needed to payback.
Recommendation
Consider using the actual pool ratios instead of dividing by two. Otherwise monitor the pool ratio to ensure the
_getjGMToPaybackfunction is not skewed. -
M-07 Medium newTotalUSD is based on borrowing non truncated amount Logical Error Acknowledged
Description
Currently the
newTotalUSDis based on the amount that is expected to be leveraged up/down. However because the amount can be truncated if there is not enough to borrow, thenewTotalUSDwill not be accurate and overestimate the amount actual amount.Recommendation
Calculate
newTotalUSDbased on the actual amount being borrowed not the expected (pre-truncated) amount. -
M-08 Medium Execution feature disabled checks not implemented in admin flows Validation Acknowledged
Description
The GMX Keeper has the authority to disable specific features when necessary, such as executing deposits and withdrawals. If these features are disabled, attempts by keepers to execute orders will result in reverts, causing the orders to be canceled. Subsequently, if the JGM keeper tries to recreate the deposit with altered parameters for a successful order, it will also fail due to the disabled feature. This situation presents a critical issue as JGM users will be unable to interact with the protocol entirely, even if only one of the deposits or withdrawals is disabled.
Therefore the protocol implemented checks if the given action (deposit, or withdraw) is disabled on GMX. However these checks are only implemented in the user flows, any keeper/admin function does not have these checks implemented. Therefore the keeper bot could automatically execute a function like for example
leverageUporleverageDownas a threshold is reached and DoS the whole system by doing so.Recommendation
Implement the same checks in the keeper/admin flows as well. By putting the check inside the
GMRoutercontract instead of theLeverageRouter, the checks can already capture most of the flows (only compound and rebalance missing). -
L-01 Low Keeper Can Be Made To Lose Gas On Failing Pending Deposits Logical Error Acknowledged
Description
The
afterDepositCancellationfunction trigger the bot to retry a cancelled order by calling eitherexecuteSingleDepositbased on the emitted data from the cancellation. However, any user can set thecallbackContractto theGMStrategycontract on their own malicious contract.This allows an attacker to orchestrate a guaranteed-to-fail deposit with the
GMStrategyas the callback contract. Subsequently, when the cancellation callback is invoked, the keeper will retry the order with the attacker key and revert withUnauthorizeddue to the key check.This revert causes slight gas costs for the keeper (if he does not pre-validate if the function will revert).
Recommendation
Add the check that
keys[key]is different from 0 in theafterDepositCancellationfunction, similar to how it is done in theafterWithdrawalCancellationfunction. -
L-02 Low Potential Native ETH Trapped Logical Error Acknowledged
Description
Whenever a user deposits or withdraws from the protocol they need to pass native ETH that will be used as an execution fee for the GMX keepers. This amount is only checked to be enough, but extra ETH, if sent, is considered donated or remains blocked in the strategy contract until retrieved via emergency withdraw.
msg.value < data.data.executionAmountRecommendation
Change so that the
msg.valuemust be equal toexecutionAmounton checks. -
L-03 Low Trapped ETH On keeperPayBack Call Logical Error Acknowledged
Description
When a keeper calls the
keeperPayBackfunction from theLeverageStrategycontract, if the contract does not have enough stables, a GMX withdraw routine will be initiated. Within that GMX withdraw routine, several GM tokens may be withdrawn. For each GM token, the keeper must provide enough native ETH so that GMX's own keeper finalizes the withdraw.With regards to the way native ETH is handled in this function, there is an issue that if the function is called but, by coincidence there are enough stable tokens then the function secedes any ETH sent to it by the Keeper remains blocked, until it will be extracted via the
emergencyWithdrawfunction.Recommendation
If the functions determines that there are enough stables in it (
expectedStables == 0) then either revert or send the native ETH back to the caller. -
L-04 Low rebalanceAmounts not implemented in getGMWeight function Logical Error Acknowledged
Description
When a rebalance is initiated, the funds to be rebalanced are taken from the
GMStrategyand sent to theRebalanceStrategycontract until the rebalance is done, before sending them back to theGMStrategy.During this time, the GM token weight and total USD value of all GM tokens in the GM Strategy do not reflect reality. Any 3rd party integration relying on it will produce invalid results in their own logic.
To fix this issue
rebalanceAmountswere added into a mapping and applied to thegetTotalValue, but they are still missing in thegetGMWeightfunction.Recommendation
Implement the
rebalanceAmountsinto thegetGMWeightfunction calculations. -
L-05 Low Attacker can still DoS even with minimum amounts being applied Logical Error Acknowledged
Description
Even with a minimum users can still make deposits and withdrawals at a constant rate DOSing the protocol. This only makes the attack a little more expensive.
Recommendation
Consider removing a global lock however this would become a large change to the whole system. If no action is taken then this should be monitored and if this were to happen temporarily changing the fee % would help make this attack not feasible.
-
L-07 Low Funds still trapped from overspending as well as refund from GMX DoS Acknowledged
Description
Users can still send excess
msg.valueand have those funds locked in the protocol.Users also do not get the excess execution fee returned to them, resulting in excess WETH locked in the GMStrategy.
Recommendation
Require that msg.value is == to the execution fee. Not msg.value is >= to the execution fee.
Consider refunding the excess WETH that is returned after a deposit/withdrawal is executed. Otherwise document that excess execution fee will NOT be refunded.
No findings match.
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.
