Guardian's review of Fee Automation for GMX, published August 2025. The report records 21 findings, including 1 high and 3 medium.
- Published
- Review window
- August 19 to 25, 2025
- Language
- Solidity
- Chains
- Arbitrum, Avalanche
- Sector
- Perpetuals, Staking
- 0 Critical
- 1 High
- 3 Medium
- 5 Low
- 12 Informational
Scope
1 file in scope · 137 nSLOC
| File | nSLOC | Lines |
|---|---|---|
contracts/contributor/ContributorHandler.sol | 137 | 182 |
Findings 21
-
H-01 High Weekly Settlement Window Misalignment Rewards Resolved
Description
The Web3Function ends the accounting window at the trigger block timestamp, not at a fixed weekly boundary.
- When
eventName === "FeeDistributionDataReceived"andisBridgingCompleted === true, the script computes rewards up to the LZ receive/process time, not the intended week end. - When
eventName === "FeeDistributionBridgedGmxReceived", it cuts the window at the GMX-bridge arrival time. This drifts from the Wednesday→Wednesday cadence and produces partial/incorrect weekly rewards.
Recommendation
Consider using canonical
{from, to}weekly window (e.g., Wed 00:00 UTC → next Wed 00:00 UTC) like its done inprocessV1fees. https://github.com/gmx-io/gmx-automation/blob/7a628a30ccdf708c1630994fba24ade15da4ec53/src/domain/fee/feeDistributionService.ts#L130-L131 - When
-
M-01 Medium Keepers Can Steal Fees Logical Error Pending
Description
Since the keeper’s raw ETH balance is used for comparison against the target, they can move funds to claim more than what is actually attributed to them. Additionally, because keepers are responsible for performing certain on-chain actions, they always need to maintain some funds to run those operations. The calculation target – current balance fails to account for the operating balance required for those actions.
Recommendation
Consider sending the amounts directly in
initiateDistributioninstead of relying on the keeper’s balance, or be aware that in this case, keepers could theoretically always claim up toFEE_DISTRIBUTOR_KEEPER_COSTS -
M-02 Medium Zero Block Confirmations For Event Triggers Configuration Resolved
Description
GMX automation scripts configure Gelato Web3 Function triggers with
blockConfirmations = 0. If a chain reorg occurs, events may be replayed or missed, leading to duplicated distributions or inconsistent state.Recommendation
Set a chain-appropriate number of block confirmations for Gelato triggers to reduce reorg-induced duplication. Note that if block confirmations are used, it would trigger web3 function with small delay, hence it could create inconsistency if exact timestamp is used in calculations.
-
M-03 Medium Bridged GMX Trigger Can Be Abused DoS Acknowledged
Description
The
FeeDistributorBridgedGmxReceivedautomation is triggered by any GMX transfer into the fee distributor vault. Since there is no minimum amount check, an attacker could grief the keeper by sending 1 wei of GMX, repeatedly draining Gelato keeper balances through redundant executions.Recommendation
Add a threshold check in event trigger (e.g., require at least X GMX received) before allowing the trigger to fire. This ensures malicious micro-transfers only result in wasted donations rather than draining keeper resources.
-
L-01 Low FeeDistributor Can Permanently Stuck Logical Error Pending
Description
The
FeeDistributorcontract usesvalidateReadResponseTimestampto enforce that LayerZero (LZ) read responses are received within the configured maximum delay (FEE_DISTRIBUTOR_MAX_READ_RESPONSE_DELAY). Similarly, Gelato keeper calls are expected within this time frame. If a response or keeper call arrives late, the corresponding method (e.g.processLzReceive) will revert, leaving the contract stuck in theInitiatedstate. In this state:initiateDistributecannot be called again (requires stateNone).- No other function transitions state back to
None. - As a result, the distribution process cannot be restarted. This creates a deadlock scenario where a single delayed or missing read/keeper execution can halt fee distributions indefinitely. Weekly fee distribution may become permanently blocked if one LZ read or Gelato execution is delayed. Recovery is impossible without a contract upgrade or manual intervention.
Recommendation
Consider allowing a reset of the state back to None if the system has been in Initiated (or ReadDataReceived) longer than FEE_DISTRIBUTOR_MAX_READ_RESPONSE_DELAY. This could be implemented as a restricted function callable by the fee distribution keeper or governance.
-
L-02 Low Missing Token Validation Validation Resolved
Description
The function
setMaxTotalContributorTokenAmountdoes not verify whether the provided token exists in theDataStore. As a result, an arbitrary token can be passed and a max value set, effectively bypassing the intended max validation logic._validateMaxContributorTokenAmountsThis contrasts withsetContributorAmount, where the token existence check is correctly enforced — suggesting the validation was unintentionally omitted insetMaxTotalContributorTokenAmount.Recommendation
Add a token existence check in
setMaxTotalContributorTokenAmount. -
L-03 Low Missing Action In Timelock Config Logical Error Resolved
Description
The function
setMaxTotalContributorTokenAmountcan only be called by the controller, but it does not have a correspondingsetaction in the TimelockConfig. Hence, this parameter is not configurable. A similar concern applies to:setMinContributorPaymentInterval
Recommendation
Consider adding corresponding timelock functions
-
L-04 Low Unclear Log: “No relevant event” Best Practices Resolved
Description
Current branch returns:
{ canExec: false, message: `No relevant event found: ${eventName}` }even when
FeeDistributionDataReceivedis the correct event butisBridgingCompleted === false. This masks the true state (i.e., waiting for bridge completion).Recommendation
Add a dedicated branch and log:
{ canExec: false, message: "FeeDistributionDataReceived seen; bridging not completed — waiting" }Reserve “No relevant event” strictly for unknown topics.
-
L-05 Low Cron Expression Compatibility Best Practices Resolved
Description
The init automation trigger uses:
trigger: { type: TriggerType.CRON, cron: "0 0 * * WED", // @audit check }While
"WED"is valid in some cron implementations, Gelato’s docs only show numeric syntax (0 0 * * 3for Wednesday). IfWEDis unsupported, the function may never trigger.Recommendation
- Confirm with Gelato whether day-of-week abbreviations are supported.
- Run a test execution to ensure
initiateDistribute()fires as expected. - If uncertain, switch to numeric syntax (
0 0 * * 3). - Verify whether Gelato treats Monday as
0or1in numeric cron.
-
I-01 Informational Redundant Check In initiateDistribute Superfluous Code Pending
Description
The
initiateDistributefunction checks thatgetAddress(Keys.FEE_RECEIVER)equalsaddress(feeDistributorVault)and reverts otherwise. Checking a configured variable against another configured variable is a redundant operation.Recommendation
Consider saving the address only once and always use the same.
-
I-02 Informational Invalid NatSpec Superfluous Code Resolved
Description
The NatSpec of the
withdrawFeesfunction talks about amarketTokensparameter that does not exist.Recommendation
Consider removing that comment.
-
I-03 Informational Treasury Funds Risk Exposure Warning Resolved
Description
In discussion, the GMX team indicated that the
vaultaddress inContributorHandlercould potentially be the DAO treasury, a multisig, or one of the v1 timelocks. If the vault is set to the DAO treasury (or any address holding more funds than those intended for contributor distribution), all of those funds would effectively be exposed to the contributor keeper, controller, and contributor distributor. Since keepers are expected to be managed by automated scripts, this greatly increases the risk surface. A similar pattern exists in the FeeDistributor:- In
finalizeWntForTreasury(L557–L558), the GMX treasury would need to approve balances to the FeeDistributor.
Thus, both components share the same underlying problem: core treasury funds are unnecessarily exposed through distribution mechanisms.
Recommendation
- Explicitly restrict the vault to a dedicated distribution contract or dedicated multisig, which should only hold the funds required for periodic distributions.
- Avoid using the DAO treasury (or any address with significant unrelated balances) directly in contributor or fee distribution
- In
-
I-04 Informational Blacklisted Token Transfers Warning Acknowledged
Description
Some ERC20 tokens revert on zero-balance or blacklisted transfers, meaning a single problematic contributor could cause all payments for all tokens and contributors to fail, though this risk is mitigated as contributor keeper can remove such contributors.
Recommendation
Beware of this scenario.
-
I-05 Informational Static Absolute Contributor Amounts Configuration Pending
Description
In ContributorHandler, contributor payments are currently set using absolute token amounts. This design is rigid and introduces problems under varying vault balances: If the vault balance is less than the total contributor amounts, sendPayments would revert. If the vault balance is greater than the total contributor amounts, the surplus remains unallocated. Since setContributorAmount applies immediately (pre-action) and persists post-action, it would be applicable for time spent from last payment to present.
Recommendation
Execute the flow below for the current code:
- Calculate contributor amounts based on what you intend to distribute in the upcoming payment.
- Call
setContributorAmountfor all contributors (multicall is fine). - Call
sendPayments. - (Optional but recommended) Reset all contributor amounts to zero afterward.
-
I-06 Informational Missing LayerZero Read Channels Configuration Acknowledged
Description
As per our communication with GMX, they have future plans for using Botonix alongside other deployments for fee calculation and distribution. However, review of the LayerZero deployments shows that Botonix does not have read channels enabled. In addition, Botonix is not listed as a target data chain for either AVAX or ARB under any DVN provider.As a result, cross-chain read communication between these networks is currently not supported:
- ARB/AVAX cannot read data from Botonix.
- Botonix cannot read data from ARB/AVAX. This limitation means that critical reads, such as fetching total staked or supply values, cannot be performed under current LayerZero read channel support.
Recommendation
Beware that Botonix state can’t be used for this atomatic fee distribution until read channels are supported there.
-
I-07 Informational On-Chain Test Runs Required Best Practices Acknowledged
Description
Given the involvement of third-party integrations with opaque code, such as LayerZero DVNs, it is critical that the flow is first tested on testnet (or on mainnet with trial configs and restricted access). Certain configurations can only be reliably validated through on-chain traces, making this step essential.
Recommendation
- Conduct a trial run of fee distribution on testnet/mainnet for both AVAX and ARB.
- Share the resulting on-chain traces with us for review and verification of correctness.
- Pay particular attention to gas limits for LayerZero actions, as these are best validated directly on-chain.
Notes For Guardian Team During RemediationsCheck returnDataSize behaviour onchain, +8 requirement Check bridgeGMX flow, when layerzeroOFT = gmx -
I-08 Informational Fee Distribution Period Overlap Warning Resolved
Description
The current check in
FeeDistributor.solonly verifies that the calculated amount meets the minimum bridging requirement:// if the calculated amount doesn't meet the min bridging requirement, revert if (feeAmountGmxCurrentChain < minRequiredGmxAmount) { revert Errors.BridgedAmountNotSufficient(minRequiredGmxAmount, feeAmountGmxCurrentChain); }However, this check does not account for scenarios involving overlapping fee distribution periods:
- Suppose there are two periods: x → y and y → z.
- Distribution for x → y is initiated but insufficient funds are available on-chain.
- While waiting for bridged GMX to arrive, the system progresses into y → z, during which new fees may already have been deposited into the vault. This can lead to situations where:
vault balance > fundsOnChain + fundsBridgedbecause the vault balance now includes funds from a future period. While not a critical vulnerability, it could cause confusing or inconsistent assertions when reviewing balances and tracing distributions.
Recommendation
- Be aware of this behavior when designing assertions and flows.
- Alternatively, document this limitation clearly, as it may not be a significant issue but could affect operational clarity.
-
I-09 Informational Fee Distribution Requires Synchronous Execution Warning Resolved
Description
The current fee distribution design assumes that all steps are executed synchronously across all chains for a given state. This reliance on synchronous execution across various chains introduces operational risk if one chain or keeper on one chain experiences downtime
Recommendation
- Always execute fee distribution actions in sync across all chains.
- Establish a contingency plan for scenarios where one chain (e.g., Arbitrum) experiences downtime or delayed execution.
- Share this contingency plan with us for review to ensure consistency and operational safety.
-
I-10 Informational Bridging Excess GMX Requires Native Fee Warning Pending
Description
In
FeeDistributor.sol(L479–L480), bridging excess GMX requires a native fee to be paid by the contract itself. This means the contract must always hold some native ETH balance (or the relevant native token on other chains) in order to successfully perform cross-chain sends.Recommendation
Ensure that the contract maintains a minimum buffer of native ETH to cover bridging fees.
-
I-11 Informational Cross-chain Overrides/Read Configuration Acknowledged
Description
Gelato’s KV storage is per task, not auto-namespaced by chain. If the same task is configured for AVAX and ARB, keys like
distributionData,wntPrice,gmxPrice,fromTimestampcan be overwritten by another chain’s run. This could lead to one’s chains data used by other, insideFeeDistributionCompletedevent processingRecommendation
Consider configuring one Gelato task per chain. Also consider validating the the stored
chainIdbefore use/ If you must share a task, namespace all keys by chain id. -
I-12 Informational Reliance On Gelato Key-Store Values Warning Acknowledged
Description
GMX automation relies on key-store values held by Gelato for Web3 Function execution. If an attacker gains access to the Gelato secret store (via leaked secrets, web2 compromise, or Gelato breach), they could claim all fees for themselves.
Recommendation
Beware of this risk or consider adding safeguards in script and in contracts for all transfers. (That at max only certain amount could be transferred for each payment, reducing exposure) Same risk applies for Contributor Payments. Or in general fund source of funds, with only whats required for 2 weeks. So that transfer out is capped.
No findings match.
More from GMX
All 52 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.
