Guardian's review of GMX Swapper for Abracadabra Money, published April 2025. The report records 16 findings across 2 review rounds, including 16 low.
- Published
- Review window
- April 2 to 7, 2025
- Rounds
- Main Review, Remediation Review
- Language
- Solidity
- Chains
- Arbitrum
- Sector
- Lending, Perpetuals
- 0 Critical
- 0 High
- 0 Medium
- 16 Low
- 0 Informational
Scope
Findings 16
Main Review
15 findings · April 2 to 4, 2025-
L-01 Low Interface Not Inherited Warning Acknowledged
Description
The
GMSwappercontract does not explicitly inherit theISwapperV2interface, which is inconsistent with the other swapper implementations.Recommendation
Consider inheriting the ISwapper interface.
-
L-02 Low Execution Fee Param Always Zero Optimization Resolved
Description
The
CreateWithdrawalParamssets the execution fee toaddress(this).balance, but the entire contract’s balance was already sent to the ExchangeRouter:exchangeRouter.sendWnt{value: address(this).balance}(withdrawalVault, address(this).balance);Consequently, the execution fee for the params is always 0. This is not a large issue because GMX will automatically reset theparams.executionFeeto however much ETH was sent to the ExchangeRouter.Recommendation
Set the
executionFeedirectly to 0 within the params. -
L-03 Low Value Not Forwarded To Swapper Warning Resolved
Description
For the GM withdrawal to succeed, the swapper must provide a native token execution fee to GMX. The tests pass because they send value directly through the payable
GMSwapper.swapfunction to make native token available for the execution fee. However, theswapdoes not actually receive any native asset from the Cauldron. Consequently, a liquidator would need to directly send ETH to theGMSwappercontract before commencing liquidation, and if this is not done atomically, that ETH is at-risk of being stolen by another user.Recommendation
Clearly document this behavior and ensure ETH is sent to the swapper in the
cookfunction prior to liquidation. -
L-04 Low GMSwapper Incompatible with GMXV2.2 Warning Acknowledged
Description
The current call to
executeAtomicWithdrawaluses the GMXv2.1 CreateWithdrawalParams, but in v2.2 the params are different:// Before struct CreateWithdrawalParams { address receiver; address callbackContract; address uiFeeReceiver; address market; address[] longTokenSwapPath; address[] shortTokenSwapPath; uint256 minLongTokenAmount; uint256 minShortTokenAmount; bool shouldUnwrapNativeToken; uint256 executionFee; uint256 callbackGasLimit; } // After struct CreateWithdrawalParams { CreateWithdrawalParamsAddresses addresses; uint256 minLongTokenAmount; uint256 minShortTokenAmount; bool shouldUnwrapNativeToken; uint256 executionFee; uint256 callbackGasLimit; bytes32[] dataList; }Consequently, this GMSwapper will not function with GMXv2.2.** **
Recommendation
Be aware of this limitation for when GMX upgrades.
-
L-05 Low Potential DoS For Tokens Requiring Zero-Reset Approvals DoS Resolved
Description
Within the
GMSwappercontract, certain tokens such as USDT need an explicit reset to zero before updating their allowance to a new nonzero value. Currently, theGMSwapperuses a check to see if the allowance is alreadytype(uint256).maxif not, it callssafeApprove(..., type(uint256).max). However, if the preceding aggregator or external call (swapDatum.to.functionCall(swapDatum.data)) modifies the allowance to a non-zero amount other thantype(uint256).max, a subsequent attempt to approvetype(uint256).maxmay fail for tokens like USDT. This mismatch can cause a DoS for users attempting to swap these tokens, since the contract is unable to re-approve them without first resetting to zero.Recommendation
Replace the current approval pattern with a safe retry mechanism that explicitly handles tokens requiring a zero allowance reset before setting a new nonzero allowance. This can be done using a utility function such as safeApproveWithRetry which:
- Sets the allowance to zero (if not already zero).
- Approves the target address for the desired type(uint256).max amount.
-
L-06 Low Potential Reentrancy In swap Function Warning Acknowledged
Description
Inside the
GMSwappercontract, eachswapData[i]entry specifies a target contract (swapDatum.to) and a arbitrary call data (swapDatum.data). When the code executes:swapDatum.to.functionCall(swapDatum.data);that call can perform any function call on
swapDatum.to. An attacker could exploit this call to grant allowances from theGMSwapperto himself, allowing him to transfer tokens out of theGMSwapper’s balance. In futureswapcalls if the attacker receives the control during the flow, he could drain all the tokens remaining (just theremainingBalance, more would cause a revert) in theGMSwappercontract before the different legitswapDatum.to.functionCall(swapDatum.data)calls are completed.Recommendation
Considering adding the
nonReentrantmodifier to theswapfunction. -
L-07 Low Lack Of Exact Amount Tracking Before functionCall Execution Warning Acknowledged
Description
Within the
GMSwappercontract’s loop overswapData[], the function calls:swapDatum.to.functionCall(swapDatum.data);directly after withdrawing tokens from GMX’s
executeAtomicWithdrawal, without knowing precisely how many tokens were actually received. As a result, the code and the actual caller can not precisely determine the exact token amount available for theswap, leaving the called aggregator contract (swapDatum.to) to infer or handle the balance in an ad-hoc manner. Therefore, this is an important limitation for users using theGMSwapperas they do not know exactly the amounts they can operate with beforehand to properly build an optimalswapData.Recommendation
Inform the users of this limitation as addressing this issue would require a major update to the current
GMSwappercode. -
L-08 Low Missing Custom Error When shareReturned < shareToMin Best Practices Resolved
Description
At the end of the
GMSwapper.swapfunction, the code calculates:extraShare = shareReturned - shareToMin;This line risks an underflow if
shareReturned(the total MIM shares actually deposited) ends up less thanshareToMin(the user’s minimum expected shares). In Solidity ^0.8.x, this triggers an automatic underflow revert that would not clearly indicate that the user’s slippage tolerance was violated.Recommendation
Before performing the subtraction, compare
shareReturnedandshareToMin. IfshareReturned < shareToMin, revert with a clear revert message or custom error indicating that the slippage requirement was not met. For example:if (shareReturned < shareToMin) { revert SlippageExceeded(shareReturned, shareToMin); } extraShare = shareReturned - shareToMin; -
L-09 Low Missing Event Emissions In The swap Flow Best Practices Acknowledged
Description
Within the
GMSwappercontract’sswapfunction, there are no dedicated events emitted to indicate critical state changes such as the finalshareReturnedamount deposited inBentoBoxor the token balances obtained afterexecuteAtomicWithdrawal.Recommendation
Consider emitting events for key steps:
- Post-withdrawal balances: Immediately after
executeAtomicWithdrawal, emit an event detailing the new balance of tokens in the contract (e.g.,LogTokensReceived(fromToken, amountReceived, ...). - Final MIM shares: Once the final deposit into
BentoBoxcompletes, emit an event such asLogSwapResult(recipient, shareReturned, extraShare).
- Post-withdrawal balances: Immediately after
-
L-10 Low Unimplemented Function For Calculating Required Execution Fee Best Practices Resolved
Description
In the
GMSwapper, there is a comment outlining the formula to compute the minimum execution fee for atomic withdrawals. However, this calculation is not implemented directly in any helper function. Developers or integrators must replicate the logic themselves, pulling the same parameters fromdataStoreand performing the required calculations.Recommendation
Create a dedicated utility function that does the work directly for the users. For example:
function getMinExecutionFee(uint256 oraclePriceCount, uint256 currentGasPrice) public view returns (uint256) { uint256 estimatedGasLimit = dataStore.getUint(Keys.withdrawalGasLimitKey()); uint256 baseGasLimit = dataStore.getUint(Keys.ESTIMATED_GAS_FEE_BASE_AMOUNT_V2_1); baseGasLimit += dataStore.getUint(Keys.ESTIMATED_GAS_FEE_PER_ORACLE_PRICE) * oraclePriceCount; uint256 multiplierFactor = dataStore.getUint(Keys.ESTIMATED_GAS_FEE_MULTIPLIER_FACTOR); uint256 gasLimit = baseGasLimit + Precision.applyFactor(estimatedGasLimit, multiplierFactor); return gasLimit * currentGasPrice; } -
L-11 Low
shareToMinslippage protection is not enough Warning AcknowledgedDescription
The
shareToMinparameter guarantees that when the MIM tokens are deposited after all the operations back in the degenbox on the behalf of the user, the shares received will be at leastsharesToMin. While this minimizes the risk of getting sandwiched, the degenbox share price can still change due to strategy performance. For example:- 1000 shares and 1000 assets (1:1)
- The user specifies
sharesToMin = 100, which is equal to100 assetsat that time - Before the transaction is executed, the strategy reports a loss of 500 assets and the new ratio becomes
2:1. - Now
sharesToMin = 100is actually50assets.
Recommendation
Be sure to document this risk and enforce proper slippage values in your frontend for the swap data as well.
-
L-12 Low Execution Fee Param Always Zero Warning Resolved
Description
The
CreateWithdrawalParamssets the execution fee toaddress(this).balance, but the entire contract’s balance was already sent to the ExchangeRouter:exchangeRouter.sendWnt{value: address(this).balance}(withdrawalVault, address(this).balance);Consequently, the execution fee for the params is always 0. This is not a large issue because GMX will automatically reset theparams.executionFeeto however much ETH was sent to the ExchangeRouter.Recommendation
Set the
executionFeedirectly to 0 within the params. -
L-13 Low Swap Function Temporary DoS DoS Resolved
Description
The swap function allows users to perform external swaps through
functionCall, allowing them to set custom to and data. However, a user could exploit this to reset the maximum MIM token approval granted to the box contract in the constructor. As a result, no further swaps can be executed until the approval is restored, causing a temporary denial of service.Recommendation
Consider imposing restrictions on the external swap call to prevent changes to the MIM token approval on the box contract.
-
L-14 Low Unused Variable dataStore Best Practices Resolved
Description
The
dataStorevariable is assigned in the constructor but is only referenced in commented-out code.Recommendation
Remove the
dataStorevariable if it serves no purpose. -
L-15 Low Non-Atomic swap Calls Can Lead to Fund Theft Warning Resolved
Description
The
swapfunction is intended to be called atomically followingCauldron.removeCollateral(i.e., within the same transaction). However, if executed in separate transactions, any tokens sitting idle in the swapper contract become vulnerable. Additionally, swap relies on user-suppliedswapDatato convert withdrawn tokens into MIM. IfswapDatais misconfigured to receive a different token (e.g., not MIM), those tokens are left unhandled in the contract. Since swap allows arbitrary input data, a malicious user could simply craft aswapDatapayload to drain all tokens left behind by a prior failed or partial call.Recommendation
- Document clearly that the
swapfunction should only be used as part of an atomic flow. - Consider reverting if MIM balance is zero after the swap (before
box.deposit) to prevent misconfiguredswapDatafrom succeeding.
- Document clearly that the
Remediation Review
1 finding · April 7, 2025-
L-01 Low Fee Underestimated for Less than 3 Oracles Validation Resolved
Description
The
getMinExecutionFeefunction calculates the required execution fee using user-suppliedoraclePriceCountandcurrentGasPrice. However, iforaclePriceCount < 3, the function will underestimate the required fee. In practice, GMX always uses a minimum of 3 oracle prices for withdrawals, as enforced byGasUtils::estimateWithdrawalOraclePriceCountsource and validated during fee checks inWithdrawalUtilssource. Accepting values below 3 ingetMinExecutionFeecan mislead integrators or frontends into providing insufficient fees, resulting in failed transactions and poor UX.Recommendation
Update
getMinExecutionFeeto validate thatoraclePriceCount >= 3, or hardcode the minimum internally to align with GMX’s expectations.
No findings match.
More from Abracadabra Money
All 19 reports-
MagicGLP Upgrade
11 findings1 high 11 findings: 1 high, 10 low -
PurrSwap Aerodrome
105 findings9 high 105 findings: 9 high, 23 medium, 45 low, 28 informational -
PurrSwap ve33
74 findings5 critical · 10 high 74 findings: 5 critical, 10 high, 20 medium, 39 low -
Hyperliquid OFT Deployment
3 findings 3 findings: 1 low, 2 informational
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.
