Fee policies
Pick what happens to your LP fees at launch: burn, vest, stake, buy-and-burn, or a redeemable ETH floor. Bound immutably to the token.
Your pool earns fees in two assets — WETH and your own token — and at launch you decide, once and permanently, what happens to each. That is the fee-policy system: a small registry of modules that the fee receiver routes into, chosen with two fields in LaunchParams.
#Choosing at launch
Both fields take abi.encode(uint16 policyId, bytes data). Empty bytes means “no module” — the token side splits in kind, the creator side is paid straight to you.
// token side: burn everything the pool earns in your token
tokenFeeConfig = abi.encode(uint16(1), bytes(""));
// token side: vest it out at 10% of the vault per day, to msg.sender
tokenFeeConfig = abi.encode(uint16(2), abi.encode(uint16(1000), address(0)));
// creator side: accrue WETH and buy your own token back with it
creatorFeeConfig = abi.encode(uint16(2), bytes(""));
// either side: pay stakers. Pick it on both and one pool pays both assets.
creatorFeeConfig = abi.encode(uint16(3), bytes(""));The launcher resolves the id to a module, calls its onRegister hook, records the result, and marks the resulting venue as exempt from the token’s wallet cap so fee routing can never be blocked by the sniper guard. From that moment the token is bound to that module contract — not to the policy id — so a later protocol upgrade cannot change how your token behaves.
#Token-side policies
| id | Policy | What it does |
|---|---|---|
| — | Default (empty) | Split creator/protocol in kind, like the WETH side. |
| 1 | Burn | Every token the pool earns is destroyed the moment it is collected. |
| 2 | Vesting | Tokens accrue in a vault that pays out a fixed share of its balance, once a day. |
| 3 | Staking | Tokens become rewards for holders who stake the token. |
#Burn — policy 1
The simplest module in the system. Fees land on it and it calls burn() in the same transaction, permanently reducing totalSupply. It tracks totalBurned[token] so the lifetime figure is readable on-chain, and it has no admin, no storage of value, and nothing to claim. Nobody receives these tokens, including you.
#Vesting — policy 2
Fees accrue in a vault. claim(token) is permissionless — anyone can call it, and it always pays the configured recipient — but it succeeds at most once every 24 hours, releasing dailyReleaseBps of the current balance.
abi.encode(uint16 dailyReleaseBps, address recipient)CLAIM_INTERVAL), enforced on-chain#Staking — policy 3
The token side becomes staking rewards. Pick it on this side only and stakers earn more of your token; pick it on both sides and the same pool pays them in your token and in ETH. See Staking pools.
#Creator-side policies
| id | Policy | What it does |
|---|---|---|
| — | Default (empty) | Your WETH share is paid to your recipient address on every collect. |
| 2 | Buy back & burn | Your WETH accrues, then buys your token on its own pool and burns what it bought. |
| 3 | Staking | Your WETH share streams to holders who stake your token. |
| 4 | Backing vault (FLOOR) | Your WETH collects in a per-token vault that anyone can burn tokens against for ETH. |
#Buy back & burn — policy 2
WETH accumulates per token in the module. A buyback spends it on the token’s own launch pool — on whichever venue that token launched on, resolved from the launcher registry at buyback time — and burns everything it buys.
- The token owner can trigger one at any time, for any size, with their own
minTokensOut. - Anyone can trigger one after 90 days of inactivity — measured from the last buyback, or from registration if there has never been one. A public call must spend the full pending balance, and any successful buyback resets the clock.
- There is no admin and no operator. Pending WETH has exactly one exit: a buyback that burns.
Buybacks are deliberately not triggered by fee collection. If they were, anyone could crank collect() to force a buy at a moment of their choosing and sandwich it. Because the trigger is owner-discretionary, the timing and size are unpredictable.
minTokensOut and could sandwich their own call. We took that over the alternative of ETH stuck permanently, and the full-balance rule stops dust calls from resetting the clock while leaving the pot hostage.#Staking — policy 3
Your WETH share is paid into the token’s staking pool as a reward stream. This is the same module and the same pool as the token-side staking policy — a launch that picks staking on either side, or both, gets exactly one pool.
#Backing vault (FLOOR) — policy 4
Instead of being paid out, your WETH share collects in a per-token vault. Anyone may burn tokens against that vault to redeem their pro-rata slice of the ETH inside it. The token’s own trading volume therefore buys it a hard, permissionless floor — one that is redeemable in ETH rather than merely asserted.
backing = the vault's ETH balance + its WETH balance
circulating = totalSupply - tokens still sitting inside the locked launch position
floorPrice = backing / circulating // ETH per whole token
redeem(a) = backing * a / circulating * 0.99 // priced pre-burn; 1% stays behind#Why the denominator is circulating supply
This is the part that makes the number mean something. At a hood.dev launch ~100% of the supply is minted straight into the launch position, which is locked in the FeeLocker forever. That supply is not held by anyone, so it can never be redeemed — and dividing the backing across it would quote a floor far below what the tokens that actually exist are entitled to.
So the vault subtracts it. Take a 1 000 000 000 supply token with 950 000 000 still in the launch position and 10 ETH in the vault: dividing by total supply gives 1e-8 ETH per token, while dividing by the 50 000 000 in circulation gives 2e-7 — 20× higher, and it is the number that is true. The 50M tokens people hold are collectively entitled to all 10 ETH.
#The floor moves in both directions
Because the denominator is the float, the floor is not monotonic, and it would be dishonest to present it as a number that only goes up.
| Event | Circulating supply | Floor per token |
|---|---|---|
| Buy | Up — supply leaves the launch position | Falls |
| Sell | Down — supply returns to the position | Rises |
| Fee accrual or donation | Unchanged | Rises |
| Burn (anywhere) | Down | Rises |
| Redemption | Down by the amount burned | Rises, strictly |
A buy lowering the per-token floor is not a loss and not a bug: the vault’s ETH is unchanged, it is simply spread across more claimants — and the buyer paid market price for the same claim you hold. What the design does guarantee is that no third party can dilute your claim by any means except buying tokens.
REDEEM_SKIM_BPS is a constant in the deployed bytecode, not a setting — there is no admin on the vault to change it. It is also why every redemption strictly raises the floor rather than merely holding it flat.#Redeeming
Approve the vault for the amount you want to burn, then call redeem. The vault burns your tokens with burnFrom — it never custodies them — and pays you in native ETH, unwrapping WETH only when its loose balance is short.
function redeem(uint256 amount, uint256 minEthOut, address to)
external returns (uint256 ethOut);
// reads
function backing() external view returns (uint256); // ETH + WETH held
function circulatingSupply() external view returns (uint256); // totalSupply - position inventory
function launchPositionInventory() external view returns (uint256);
function floorPrice() external view returns (uint256); // wad, ETH per whole token
function previewRedeem(uint256 amount) external view returns (uint256 ethOut); // exact, net of the skim
uint256 public constant REDEEM_SKIM_BPS = 100; // 1%
// on the module
function vaultOf(address token) external view returns (address);previewRedeemis exact — it is the same computationredeemperforms, against the same pre-burn circulating supply, so what it quotes is what you get.minEthOutis ordinary slippage hygiene. The payout only moves against you if the backing shrinks or someone buys in the same block.- Pricing uses the pre-burn circulating supply, which is what makes the slice genuinely pro-rata: your own tokens are still counted when your share is computed.
receive() is deliberately open, so anyone — a creator, a community, a bot — can donate ETH straight into the backing and lift the floor for every holder.One soundness precondition: the token must be non-mintable, since a mint would dilute the floor without adding backing. Every launchpad token is a fixed-supply HoodToken, which qualifies by construction.
#How the registry works
Two FeePolicyManager instances back the whole thing — one per fee asset. The token-side instance has feeAsset = address(0), meaning “the launched token”; the creator-side instance has feeAsset = WETH.
onTokenLaunched — launcher-only, once per token, reverts with PolicyAlreadySet on a second attempt.handleFee(token, amount) — permissionless. It pulls the asset from the caller into the policy’s venue before notifying the module, so every accounting entry is backed by tokens that actually arrived. A call from anyone other than the fee receiver is simply a donation to that policy.setModule(id, module) — owner-only, and it affects future launches only. Tokens keep the module contract they registered with, forever.#Deployed modules
Older tokens may be bound to earlier generations of these modules — most notably the Uniswap-only buy-burn module used before the multi-venue launcher shipped. That is the system working as designed. The authoritative answer for any token is its policy record in the getPolicy(token) on the relevant manager — or its indexed policy record, see Data & indexing.
