Spend-Gated Launches
A spend-gated launch is a Flaunch pool that refuses swaps unless they carry a signature from a nominated off-chain signer, and that signature names the maximum ETH the swap may spend. It is the mechanism behind Game Mode, and it is general: anything that can decide how much a given wallet should be allowed to spend can drive it.
This page covers the architecture. The two pages beside it go deeper on the signer and on enforcement inside the V4 hook.
The problem: one calculator slot
Flaunch's PositionManager has exactly one feeCalculator, set by the protocol owner. Every pool routes through it. That is fine while there is one fee policy, and blocking the moment you want a gated launch sitting alongside ordinary ones.
FeeCalculatorDispatcher occupies that single slot and routes per pool:
┌─ StaticFeeCalculator (every ordinary pool)
PositionManager ─> FeeCalculatorDispatcher ─┤
└─ SpendGatedSignerFeeCalculator (gated pools)A launch opts into a sub-calculator by prefixing its feeCalculatorParams with a routing marker:
bytes32 constant ROUTING_PREFIX = keccak256('flaunch.dispatcher.route.v1');
feeCalculatorParams = abi.encode(ROUTING_PREFIX, address subCalculator, bytes subParams);A launch without the prefix routes to the default calculator and behaves exactly as it did before the dispatcher existed. Pools configured before the dispatcher was installed keep working too — the dispatcher does not forward setFlaunchParams to the default calculator, which is safe precisely because that default is stateless.
Only calculators the dispatcher's owner has registered can be routed to.
The flow
player ──────> game server ──────> signed SpendAuthorization
│
▼
player's wallet ─── swap(…, hookData) ──> PositionManager
│ afterSwap
▼
FeeCalculatorDispatcher
│ routed by poolId
▼
SpendGatedSignerFeeCalculator.trackSwap
│
├─ recover signer, check against the pool's signer
├─ bind the authorization to its submitter
├─ measure real ETH input against maxSpendWei
├─ accumulate against the per-wallet cap
└─ burn the signatureEnforcement runs in afterSwap, so it measures what the swap actually did rather than what it claimed it would do.
Configuring the gate at launch
Gate settings are written once per pool, at flaunch time, through setFlaunchParams. Two encodings are accepted:
The full form. This is the only one that can enable an enforcing gate.
Naming the signer at launch is what lets a working gate be installed in a single transaction — otherwise the signer arrives in a follow-up call, and until it lands nobody can produce a signature the pool accepts.
Kept decodable for existing callers. It cannot express an expiry, and an enforcing gate must have one, so in practice this form launches ungated.
Anything else reverts with UnrecognisedGateParams.
The tag is not decoration. feeCalculatorParams is caller-controlled, and length alone is not a discriminator — any other encoding that happened to be six words long would be reinterpreted field for field. A dynamic type sitting in the signer's position decodes its ABI offset as an address, which installs a permanent per-pool signer that nobody holds the key to.
setFlaunchParams is a launch-time hook and runs at most once per pool. A second call reverts with GateAlreadyConfigured, which bounds the damage a compromised POSITION_MANAGER role could do to pools that have not launched yet.
Three roles, and what each one cannot do
Creator (holds the Flaunch ERC-721)
Choose the window, the cap, the signer and the settler at launch
Touch the signer afterwards — not even to retire it
Signer
Issue authorizations that the pool accepts
Change any pool setting, or authorize past endsAt
Settler
Rotate the signer, or zero it to open trading early
Extend the window, or move the cap
The creator's exclusion is the surprising one, and it is deliberate. In Game Mode the launching player is the memecoin's creator — that is what makes the round theirs. Zeroing the signer is how that player would buy their own fair launch ungated, for as much as they liked, while everyone else was still earning allowance a point at a time. Nothing downstream would catch it: an ungated swap is indistinguishable from an authorized one to a watcher polling Swap.
Nor could the power be pinned to the launching player even if you wanted to. IMemecoin.creator() reads the ERC-721 owner live, so the role transfers with a secondary sale of the NFT — it was sellable mid-round to someone the round had never met.
Expiry is the guarantee
An enforcing gate must carry an endsAt, and it must be within MAX_GATE_DURATION (30 days) of launch. Past that timestamp the calculator stops enforcing outright: no signature demanded, no spend tracked, the pool is an ordinary pool.
This exists because neither the launcher nor the signer is vetted, and they are routinely the same party. flaunch() is permissionless and feeCalculatorParams is caller-supplied, so a launch can name itself as both its own signer and its own settler. Since the gate enforces on sells as well as buys, such a launcher would otherwise hold a permanent veto over every holder's exit — issue buy authorizations, then simply stop signing, and the position is trapped for as long as the key stays silent.
The expiry makes the exit a property of the pool. It needs no transaction from the launcher, the settler, or the contract owner.
A launch that names a signer but no settler is refused outright (SettlerRequired), because the signer is the gate and the settler is the only party who can lift it early. One reverted flaunch is a much cheaper failure than a coin nobody can open, discovered when a round tries to settle and cannot.
Deployments
Game Mode runs on Robinhood chain (4663) and Base (8453), with a test stack on Base Sepolia (84532). Each chain's current stack is the Flaunch v1.3 generation, which measures spend in the pool's own paired token: ETH, or any token the chain's PairedTokenRegistry has approved.
PositionManager
0x588C683EcC450F8b2aAdb13D7f63792b840425DC
0x588C683EcC450F8b2aAdb13D7f63792b840425DC
0x8D346f24278C5CD786309161aAC0fC2bbe4c25dc
FeeCalculatorDispatcher
0xe3fDDf48E305dAB9A74BFa5b9858Db1aAc1D80F8
0xdbc2f399bbac8cd766f20c9b917a9a6ecad5bc4b
0xd381f8ea57df43c57cfe6e5b19a0a4700396f28c
SpendGatedSignerFeeCalculator
0xB246b270bB05d9Fa76c4456408ce3e8600d916bf
0xd8e46a2ca31915d9b76cc8e6b365b7ed46b77b01
0x54cdcf0bcbc3a33f470e07134c10582f93058a32
PoolSwap (approved router, reports msgSender())
0x8476ED156f731335ECA8Cc8A8eE759330ee4A91f
0x1B8065a099AdcD7aa7c5e241e3596B56ec98bA5a
0xf0f388a31a1745a5e2378b812ed51525f70595be
PairedTokenRegistry
0xC3F4E72DE4D37988F12C101b0766Fd8462F6Faf9
0x26958422636655b5a4eCE23a062e2EB61332c6da
0x23cb441d18CA75c6a14964B06806dF668d45A1C6
The PositionManager is a CREATE3 deploy, which is why Robinhood and Base share its address; every other contract is chain-specific. The dispatcher is installed on the PositionManager, with the chain's StaticFeeCalculator as its fallback — every non-game launch passes through untouched. The PositionManager names its registry on chain (pairedTokenRegistry()), so a gate discovers the approved pairings and their price calculators without configuration.
Limitations worth knowing before you build
Spend is measured in the pool's paired token. On the current stack that is ETH or any token the chain's
PairedTokenRegistryapproves, and every signed amount is in that token's base units —maxSpendWeikeeps its name for signature compatibility but is not always wei. Only the retired first-generation Robinhood calculator assumed an ETH-paired pool and refused anything else withInvalidPoolKey.Exact-output swaps are refused. They cannot be measured safely — see the enforcement page for why a zero-measured buy would otherwise be repeatable.
One gate, one signer, one pool. There is no notion of multiple concurrent signers per pool; the per-pool signer overrides the protocol-wide trusted set entirely.
Last updated
Was this helpful?