Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · August 2025

Fee Automation

for GMX

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

10 resolved · 6 acknowledged · 5 pending

Scope

1 file in scope · 137 nSLOC
FilenSLOCLines
contracts/contributor/ContributorHandler.sol137182

Findings 21

  1. 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" and isBridgingCompleted === 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 in processV1 fees. https://github.com/gmx-io/gmx-automation/blob/7a628a30ccdf708c1630994fba24ade15da4ec53/src/domain/fee/feeDistributionService.ts#L130-L131

  2. 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 initiateDistribution instead of relying on the keeper’s balance, or be aware that in this case, keepers could theoretically always claim up to FEE_DISTRIBUTOR_KEEPER_COSTS

  3. 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.

  4. M-03 Medium Bridged GMX Trigger Can Be Abused DoS Acknowledged

    Description

    The FeeDistributorBridgedGmxReceived automation 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.

  5. L-01 Low FeeDistributor Can Permanently Stuck Logical Error Pending
    Location
    Global

    Description

    The FeeDistributor contract uses validateReadResponseTimestamp to 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 the Initiated state. In this state:

    • initiateDistribute cannot be called again (requires state None).
    • 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.

  6. L-02 Low Missing Token Validation Validation Resolved

    Description

    The function setMaxTotalContributorTokenAmount does not verify whether the provided token exists in the DataStore. As a result, an arbitrary token can be passed and a max value set, effectively bypassing the intended max validation logic. _validateMaxContributorTokenAmounts This contrasts with setContributorAmount, where the token existence check is correctly enforced — suggesting the validation was unintentionally omitted in setMaxTotalContributorTokenAmount.

    Recommendation

    Add a token existence check in setMaxTotalContributorTokenAmount.

  7. L-03 Low Missing Action In Timelock Config Logical Error Resolved

    Description

    The function setMaxTotalContributorTokenAmount can only be called by the controller, but it does not have a corresponding set action in the TimelockConfig. Hence, this parameter is not configurable. A similar concern applies to:

    • setMinContributorPaymentInterval

    Recommendation

    Consider adding corresponding timelock functions

  8. 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 FeeDistributionDataReceived is the correct event but isBridgingCompleted === 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.

  9. 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 * * 3 for Wednesday). If WED is 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 0 or 1 in numeric cron.
  10. I-01 Informational Redundant Check In initiateDistribute Superfluous Code Pending

    Description

    The initiateDistribute function checks that getAddress(Keys.FEE_RECEIVER) equals address(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.

  11. I-02 Informational Invalid NatSpec Superfluous Code Resolved

    Description

    The NatSpec of the withdrawFees function talks about a marketTokens parameter that does not exist.

    Recommendation

    Consider removing that comment.

  12. I-03 Informational Treasury Funds Risk Exposure Warning Resolved
    Location
    Global

    Description

    In discussion, the GMX team indicated that the vault address in ContributorHandler could 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
  13. 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.

  14. I-05 Informational Static Absolute Contributor Amounts Configuration Pending
    Location
    ContributorHandler.sol

    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:

    1. Calculate contributor amounts based on what you intend to distribute in the upcoming payment.
    2. Call setContributorAmount for all contributors (multicall is fine).
    3. Call sendPayments.
    4. (Optional but recommended) Reset all contributor amounts to zero afterward.
  15. I-06 Informational Missing LayerZero Read Channels Configuration Acknowledged
    Location
    Global

    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.

  16. I-07 Informational On-Chain Test Runs Required Best Practices Acknowledged
    Location
    Global

    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 Remediations Check returnDataSize behaviour onchain, +8 requirement Check bridgeGMX flow, when layerzeroOFT = gmx

  17. I-08 Informational Fee Distribution Period Overlap Warning Resolved

    Description

    The current check in FeeDistributor.sol only 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 + fundsBridged
    

    because 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.
  18. I-09 Informational Fee Distribution Requires Synchronous Execution Warning Resolved
    Location
    Global

    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.
  19. 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.

  20. 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, fromTimestamp can be overwritten by another chain’s run. This could lead to one’s chains data used by other, inside FeeDistributionCompleted event processing

    Recommendation

    Consider configuring one Gelato task per chain. Also consider validating the the stored chainId before use/ If you must share a task, namespace all keys by chain id.

  21. I-12 Informational Reliance On Gelato Key-Store Values Warning Acknowledged
    Location
    Global

    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.

More from GMX

All 52 reports
  1. Incentives Contracts

    17 findings 17 findings: 3 medium, 3 low, 11 informational
  2. Donation Contracts

    25 findings 25 findings: 1 medium, 6 low, 18 informational
  3. V2.3 Pre-Review

    7 findings 7 findings: 7 informational
  4. Risk Oracle

    13 findings 13 findings: 2 medium, 4 low, 7 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.

Get a quote