2.0 RC docsView 1.x docs
Concept · Gas fees

Who sets the per-claim fee

A creator cannot choose their campaign's claim fee, only refuse one they consider too high.

v2.0.0-rc.1 release candidate
The API is frozen and later candidates carry fixes only, except the receipt-free and Safe create surface of /fhe-airdrop, which is @beta. Published on the next dist-tag; latest stays on 1.6.0 until 2.0.0.

Every claim against an airdrop instance pays a per-claim fee in native ETH, and that fee is frozen into the instance at creation for the rest of its life. The creator is the one who sends create*, but they are not the one who picks the number: it resolves from the factory's own fee configuration, which belongs to the factory's fee manager. What the creator gets is a veto.

Why the param is required#

CommonAirdropParams.maxAcceptedGasFeehas no default, and that is deliberate rather than an oversight. Two facts make a default the wrong answer: the fee is immutable once frozen, and the value that resolves is chosen by the factory's fee manager rather than by the creator. A default would quietly hand that decision back to whoever is not making it - either silently accepting any fee the factory happens to charge, or silently refusing a campaign that would have been fine.

So the SDK makes the creator state a bound. The factory resolves their fee at execution time and reverts GasFeeNotAccepted if it lands above that bound, surfaced as GasFeeNotAcceptedError. Pass 0n to accept only a zero-fee campaign, or the exported UINT96_MAX to accept whatever resolves - an escape hatch rather than a recommendation.

The knobs#

MethodWhoWhat it controls
maxAcceptedGasFeeCampaign creator, at createThe highest per-claim fee this creator will accept being frozen into the new instance. Required. Not a fee - a bound on someone else's fee.
setDefaultGasFee(gasFee)FEE_MANAGER_ROLEThe per-claim fee new instances get when their creator has no override. Applies to future creates only; never rewrites a deployed instance.
setCustomFee(creator, gasFee)FEE_MANAGER_ROLEA per-creator override that wins over the default. disableCustomFee drops it and reverts that creator to the default.
setMaxGasFee(maxGasFee)DEFAULT_ADMIN_ROLEThe factory's ceiling on every fee the two setters above may configure, and on the fee resolved at create. Deliberately not the fee manager's to move.
resolveGasFee(creator)Anyone (read)The number maxAcceptedGasFee is actually checked against: the creator's enabled override, else the default. A snapshot, not a quote.
gasFee()Anyone (read, on the instance)What a deployed instance froze at init. Immutable for the instance's lifetime. The SDK reads it fresh and attaches it as value on every claim.

Reading the fee before you bound it#

resolveGasFee(creator) folds the override-over-default rule for you and returns the single number the bound is compared against, which makes it the right thing to show a creator before they choose. It is a snapshot, not a quote: FEE_MANAGER_ROLE can move the fee between that read and the create landing, which is precisely the race the bound exists to catch. Reading it and passing the result verbatim is therefore a reasonable default for a UI, not a way to opt out of the check.

In React, useResolveGasFee wraps the read, and useFactoryFees returns maxGasFee alongside feeCollector and defaultGasFee. The three fee mutations - useSetDefaultGasFee, useSetCustomFee and useDisableCustomFee - invalidate the resolved-fee key too, so a mounted useResolveGasFee does not sit on a stale answer after an admin moves a fee.

The ceiling, and who holds it#

maxGasFee()is the factory's own bound on every fee the setters may configure and on every fee resolved at create. setMaxGasFee is gated by DEFAULT_ADMIN_ROLE and deliberately not by FEE_MANAGER_ROLE: the fee manager is meant to move fees around inside a range the admin controls, so letting them raise the range would make the ceiling decorative.

Tightening the ceiling does not clamp what is already set#

A fee setter that would exceed the ceiling reverts GasFeeExceedsMaximum, which the SDK surfaces as an InvalidArgumentError on gasFee carrying both numbers. What the ceiling does not do is rewrite a fee that was configured while it was higher: lowering maxGasFee under an existing defaultGasFee or per-creator override does not bring that fee down with it.

A create over the ceiling reports the ceiling#

A stale fee above the ceiling fails the next create*on the ceiling, not on the creator's bound. The factory checks maxGasFee first, and so do the create methods and preflightCreate: a resolved fee above both the ceiling and maxAcceptedGasFee throws the ceiling's InvalidArgumentError (on gasFee), so raising your own bound would not have helped.

ETH on a zero-fee campaign#

On a fee-charging instance the ETH it holds is fee revenue, swept with withdrawGasFee. On a zero-fee campaign there is no fee to collect, so ETH sent there by mistake is recovered with rescueNativeToken({ recipient }) (RESCUER_ROLE). Calling it on a fee-charging campaign throws NativeRescueRequiresZeroFeeError.

See also