The Trusted Signer
The signer is the off-chain half of a spend-gated launch. It decides how much ETH a wallet may spend against a pool, and says so in an EIP-712 signature the calculator verifies on every swap.
Nothing about the signer is Flaunch-specific. It is a key, a rule for deciding allowance, and an endpoint that hands out signatures. In Game Mode the rule is "how well did you shoot"; it could as easily be "did you answer the question", "were you on the allowlist", or "did you win the raffle".
The authorization
bytes32 constant SPEND_AUTHORIZATION_TYPEHASH = keccak256(
'SpendAuthorization(address buyer,bytes32 poolId,uint256 deadline,uint256 maxSpendWei,uint256 nonce)'
);buyer
The wallet this authorization belongs to. Binding it is what stops a front-runner copying the message out of public calldata.
poolId
The pool it is valid against. A signature for one pool is worthless on another.
deadline
Expiry timestamp. Keep it short — it is the window in which a leaked signature is useful.
maxSpendWei
The maximum native input the swap may consume. Measured against the swap's real ETH input.
nonce
Distinguishes two otherwise-identical authorizations, so a retried buy is not mistaken for a replay of the first.
The EIP-712 domain is:
const domain = {
name: 'FlaunchSpendGate',
version: '1',
chainId, // the chain the pool is on
verifyingContract, // the SpendGatedSignerFeeCalculator
};Do not reimplement the digest. Derive it from the contract:
function hashSpendAuthorization(
address _buyer, bytes32 _poolId, uint _deadline, uint _maxSpendWei, uint _nonce
) public view returns (bytes32 digest_);That function is public for exactly this reason. A domain mismatch is otherwise a silent InvalidSigner at swap time — a failure that surfaces in a user's wallet, minutes and one gas payment away from the signing bug that caused it.
Why the domain matters
Earlier revisions signed an EIP-191 personal-sign over keccak256(abi.encodePacked(...)). That preimage committed to the five fields and nothing else — no chain id, no verifying contract.
Flaunch deploys deterministically across chains, so the same poolId genuinely recurs on more than one of them. One signing key shared between two environments therefore made an authorization issued for one valid on the other. The EIP-712 domain closes it by construction.
Signing
Nonces
The chain only needs each nonce to be unique — it feeds the digest so two otherwise-identical claims stay distinct. Replay protection is the contract's consumed-signature ledger, not the nonce, and ordering carries no meaning.
A time-major layout keeps nonces unique across process restarts with no persistence at all:
Encoding the swap
The authorization travels as Uniswap V4 hookData, behind the referrer field Flaunch already puts there:
Installing the signer
Two routes, and they differ in how many transactions they cost:
At launch, by naming
signerin the tagged gate params. The gate is live the moment the pool exists. This is the single-transaction path and the one Game Mode uses.Afterwards, via
setTrustedPoolKeySigner(poolKey, signer)— callable only by the pool's settler or the contract owner. Until it lands, nobody can produce a signature the pool accepts.
There is also a protocol-wide trusted signer set (addTrustedSigner), administered by the contract owner, used by pools that do not nominate their own. A per-pool signer overrides it entirely.
Zeroing the per-pool signer is the gate's off-switch: enforcement stops and the pool trades openly. That is how a round settles early.
Running one in production
Clamp what you issue to what the chain will accept. Read walletSpentWei[poolId][buyer] and cap each authorization at walletCapWei − walletSpentWei. Otherwise you will hand out allowance the gate refuses with WalletCapExceeded — users earning spend they cannot use, which reads to them as a broken product rather than a cap working as designed.
Keep deadlines short. The signature is public calldata the moment it is submitted. The buyer binding means a copy is useless to anyone else, but a short deadline bounds the damage if your own key or your own issuance logic misbehaves.
Persist issuance. If your server forgets which authorizations it has already handed out, a restart re-offers the same allowance — and now two live signatures exist against one balance. The contract's replay ledger stops the same signature being used twice; it does nothing about two distinct signatures you never meant to issue.
One key, one chain. The domain binds chainId and verifyingContract, so a key used on two deployments is safe in a way it was not before. Sharing state between them is still not: pool ids, addresses and spend records mean nothing outside their own chain.
Reconcile. The calculator emits SpendRecorded(poolId, buyer, ethSpent) on every enforced swap, including sells, where the amount is zero. Comparing that stream against what you issued is the only way to notice a divergence between your ledger and the chain's.
Last updated
Was this helpful?