Skip to content

Put several fee rights in one token

A Pons fee right, a pools.trade fee NFT and v4 positions in one token: predictStreamToken, the Pons door, then tokenizeStreams.

On this page

One revenue token can take its income from several fee rights at once, each a stream of its own: here a Pons fee right, a pools.trade fee NFT and Uniswap v4 positions, minted together with a market in one tokenizeStreams call. The doors for outside fee rights are made first, for the token's address before the token exists.

In the app

  1. Open /tokenize, then By id and its Combine streams card. Add each fee right with its button: Add pools.trade fee NFT takes the vault and the fee NFT's id, Add Pons fee right the Pons token, and Add Uniswap v4 positions the position ids. Your wallet opens the same card with what it lists ticked, and the Pons and Long tabs are the same card for one fee right each.
  2. Set the terms. The card shows Hookr's leg for these streams.
  3. Press Tokenize the streams. For each fee right the wallet sends what it needs first, skipping anything already in place: for the Pons fee right, the Pons door's creation and the hand-over of the fee right to it; for the fee NFT, its approval; for the positions, the PositionManager approval. Then it sends tokenizeStreams.

The calls

  1. Fix the terms first. extraAssets lists every currency the streams pay besides ETH, at most three: here the positions' other currency. A stream that pays a currency the list leaves out is refused with AssetNotListed.
  2. predictStreamToken(creator, t) gives the token's address. It is worked out from the creator and the token's name, symbol, supply, assets (ETH and extraAssets), release period, tip and termsHash; the recipients and the amounts do not move it.
  3. The Pons door: create(revenueToken, memecoin) on the Pons door factory, from the launch's creator fee recipient, then transferCreatorFeeRecipient(memecoin, door) on the Pons launch factory. A move of the fee right that Pons already has pending blocks the mint with SourceRevocationPending until it lapses or is cancelled.
  4. approve(tokenizer, tokenId) on the fee NFT's vault, and setApprovalForAll(doorFactory, true) on the PositionManager, or a permit in positionPermits.
  5. tokenizeStreams(p, t, l, m). The tokenizer checks every outside door: that it names this token, holds its fee right, lists only the token's assets and asks a gas stipend of 100,000 to 3,000,000. It makes a door per fee NFT and one for the v4 positions, seals every door into the token, opens the market and mints.

At most eight doors, counting the market's Rules ledger and family door. Their stipends, plus 300,000 each, must stay within 28,000,000.

The token made here has five doors: the fee NFT door, the v4 position door, the Pons door, the market's Rules ledger and the family door, and streamsOf lists each with how it ends. The Pons stream shows as one Pons can move away, the others as forever.

In code

With client.ts and abis.ts:

import { createWalletClient, http, keccak256, parseUnits, toHex, type Address } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { robinhood } from "viem/chains";
import { client, FOREVER, TOKENIZER } from "./client";
import { feeNftVaultAbi, launchFeeAbi, ponsDoorFactoryAbi, ponsLaunchFactoryAbi, positionManagerAbi, tokenizerAbi } from "./abis";

const creator = privateKeyToAccount(process.env.CREATOR_KEY as `0x${string}`);
const wallet = createWalletClient({ account: creator, chain: robinhood, transport: http() });
const mined = (hash: `0x${string}`) => client.waitForTransactionReceipt({ hash });

const PONS_DOOR_FACTORY: Address = "0x…"; // from Contracts
const MEMECOIN: Address = "0x…"; // a Pons launch quoted in ETH, whose creator fee recipient you are
const VAULT: Address = "0x…"; // the pools.trade vault
const FEE_NFT = 123456n; // your fee NFT
const POSITIONS = [111n]; // your Uniswap v4 positions, here in an ETH/HOOKR pool
const HOOKR: Address = "0x…"; // the positions' other currency: every currency a stream pays besides ETH is listed

// 1. The terms come first: the token's address follows from the creator and the token's name, symbol, supply, assets,
//    release period, tip and termsHash.
const supply = parseUnits("1000000000", 18);
const seed = parseUnits("5000000", 18);
const legBps = await client.readContract({ address: TOKENIZER, abi: tokenizerAbi, functionName: "HOOKR_LEG_BPS_OUTSIDE" });
const terms = {
  name: "Example Several Revenue",
  symbol: "rSEVERAL",
  supply,
  streamSeconds: 86_400,
  tipBps: 100,
  extraAssets: [HOOKR],
  recipients: [creator.address],
  amounts: [supply - (supply * BigInt(legBps)) / 10_000n - seed],
  termsHash: keccak256(toHex("rSEVERAL: Pons, a fee NFT and v4 positions")),
};
const revenueToken = await client.readContract({ address: TOKENIZER, abi: tokenizerAbi, functionName: "predictStreamToken", args: [creator.address, terms] });

// 2. The Pons door, made for that address, then the Pons fee right handed to it.
const made = await client.simulateContract({ account: creator, address: PONS_DOOR_FACTORY, abi: ponsDoorFactoryAbi, functionName: "create", args: [revenueToken, MEMECOIN] });
await mined(await wallet.writeContract(made.request));
const ponsDoor = made.result;
const ponsLaunchFactory = await client.readContract({ address: PONS_DOOR_FACTORY, abi: ponsDoorFactoryAbi, functionName: "launchFactory" });
await mined(await wallet.writeContract({ address: ponsLaunchFactory, abi: ponsLaunchFactoryAbi, functionName: "transferCreatorFeeRecipient", args: [MEMECOIN, ponsDoor] }));

// 3. The fee NFT and the positions may move.
await mined(await wallet.writeContract({ address: VAULT, abi: feeNftVaultAbi, functionName: "approve", args: [TOKENIZER, FEE_NFT] }));
const [positionManager, doorFactory] = await Promise.all([
  client.readContract({ address: TOKENIZER, abi: tokenizerAbi, functionName: "positionManager" }),
  client.readContract({ address: TOKENIZER, abi: tokenizerAbi, functionName: "doorFactory" }),
]);
await mined(await wallet.writeContract({ address: positionManager, abi: positionManagerAbi, functionName: "setApprovalForAll", args: [doorFactory, true] }));

// 4. One call seals every door into the token. The market's family and the position door are locked for good.
const sqrt = (n: bigint) => {
  let x = n;
  let y = (x + 1n) / 2n;
  while (y < x) [x, y] = [y, (y + n / y) / 2n];
  return x;
};
const market = await client.readContract({ address: TOKENIZER, abi: tokenizerAbi, functionName: "defaultMarket", args: [sqrt(1_000_000n << 192n), seed] });
const launcher = await client.readContract({ address: TOKENIZER, abi: tokenizerAbi, functionName: "launcher" });
const [launchFee] = await client.readContract({ address: launcher, abi: launchFeeAbi, functionName: "launchFee" });
const streams = {
  launchParams: "0x" as const,
  feebs: [{ vault: VAULT, tokenId: FEE_NFT }],
  positionIds: POSITIONS,
  positionPermits: [],
  v3PositionIds: [],
  v3Permits: [],
  sources: [ponsDoor],
  unlockAt: FOREVER,
};
const { request } = await client.simulateContract({
  account: creator,
  address: TOKENIZER,
  abi: tokenizerAbi,
  functionName: "tokenizeStreams",
  args: [streams, terms, { unlockBlock: FOREVER }, market],
  value: launchFee,
});
await mined(await wallet.writeContract(request));

StreamParams takes a launch too, as the encoded LaunchParams with every royalty pointed at the predicted token, and Uniswap v3 positions with their v3 permits; the walkthrough leaves both empty. With a launch, extraAssets lists its pools' quotes and the launched token, the call carries the launch's budgets and fee as launchAndTokenize does, and l.unlockBlock locks the launch's family as well as the market's. unlockAt is the position doors' term.

Before the mint

Until a token holds the Pons door as one of its doors, the wallet that made it can take the fee right back with abort() on the door: Ways in.

Fee

A Pons fee right and a fee NFT pay Hookr nothing through the Hookr hook, so the mint gives Hookr's leg, 5% of the supply, to the leg collector. The code above reads the figure from HOOKR_LEG_BPS_OUTSIDE() and takes it out of the recipients' amount.