Developers

Publish

Publishing records your factory, probe and manifest as a registry entry creators can pick.

This page publishes a token module, through publishTokenModule — open to anyone. A launch module publishes through publishLaunch instead, and the registry refuses that call to everyone but POO.MEME until its owner opens launchPublishingOpen() on the chain you’re on (Two kinds).

The factory

src/MyModuleFactory.sol
constructor(address moduleRegistry) {
    if (moduleRegistry.code.length == 0) revert NotAContract(moduleRegistry);
    developer = msg.sender;
    address standIn = IPooProbeHost(moduleRegistry).probeStandIn();
    probe = _spawn(
        TokenModuleContext({token: standIn, quote: standIn, pair: standIn, router: standIn}), _probeConfig()
    );
}

function create(TokenModuleContext calldata ctx, bytes calldata config)
    external override returns (address tokenModule)
{
    tokenModule = _spawn(ctx, config);
}
  • create deploys a full contract with the probe’s exact code — no proxy, no clone. Keep per-instance values in storage.
  • The token has no code yet when create runs; use onInstalled for anything that needs it.
  • Anyone can call create. Guard hooks on the token, and onInstalled on the token’s factory.
  • Publish from the factory, or have its developer() or owner() return your wallet.

The probe

  • A real instance, built in your factory’s constructor. Every future instance must match its code.
  • It is built on the registry’s stand-in, IPooProbeHost(moduleRegistry).probeStandIn(), which answers as its token and pair and makes its hook calls.
  • It must answer every read your manifest names.
  • The registry calls each declared hook on it, within your gas cap.

Passing the probe allows publishing. It does not prove the module is safe.

Test it

forge test stands POO.MEME up in memory through the devkit’s PlatformHarness.install: the venue registry, module registry, token factory, metadata, and Uniswap V2 at its canonical addresses. Your test then publishes to the real registry:

test/MyModule.t.sol
function test_theRegistryAcceptsThisModule() public {
    uint32 entryId = platform.moduleRegistry.publishTokenModule(IPooTokenModuleFactory(address(factory)));
    assertGt(entryId, 0, "the registry refused the manifest");
}

A refused manifest fails with the registry’s error name. To test a hook alone, initialize an instance with your test contract as its token and call the hook yourself, as the template’s _standalone does.

Create a token

To see your module inside a token, create one through the real factory. The devkit’s ConformanceLaunchFixtureFactory is the smallest launch module the registry accepts. script/Poo.s.sol creates its token this way:

script/Poo.s.sol
(, bytes32 signedBlueprint) = p.tokenFactory.blueprint();

TokenModuleChoice[] memory chosen = new TokenModuleChoice[](1);
chosen[0] = TokenModuleChoice({entryId: g.moduleEntry, supply: 0, config: moduleConfig(), buyBps: 0, sellBps: 0});

CreatorTaxTerms memory tax;
tax.payeeCap = TAX_PAYEE_CAP;

return p.tokenFactory.create(
    tokenTerms(p, g.quote),
    Metadata({description: "", website: "", x: "", telegram: ""}),
    LaunchChoice({entryId: g.launchEntry, supply: TOTAL_SUPPLY, config: "", buyBps: 0, sellBps: 0}),
    chosen,
    new SupplyRow[](0),
    tax,
    signedBlueprint
);

The factory refuses a creation that breaks any of these:

RuleRefused As
The launch’s supply, every token module’s supply and every supply row total totalSupply exactlySupplyNotWhole
At most 8 supply rows, none zero, no recipient twice, none to a moduleTooManySupplyRows, SupplyRowEmpty, SupplyRowRepeated, SupplyRowIsAModule
A row to the creator or to 0x…dEaD has no name; every other row has one of 1–31 bytesSupplyRowNameInvalid
At most 6 token modules, each meeting its entry’s requirementsTooManyTokenModules
A row paying the creator has no name; every other payee row has one of 1–31 bytes, and none pays a modulePayeeNameInvalid, PayeeIsAModule
payeeCap is at most 13 and at least the rows writtenTaxCapAboveMaximum, TaxCapBelowRows
A payee contract with the Receive hook answers a receiveGas() of 1 to 265,000ReceiveGasUnreadable, ReceiveGasZero, ReceiveGasAboveCeiling
The token’s heaviest leg fits the transaction gas capSellGasOverTxCap
expectedBlueprint is the factory’s current token codeBlueprintMismatch

What to test

  • Publishing through the real registry, and creation through the real factory with your minimum shares and a config at each bound.
  • Every hook on a real token: the trades that reach it, a starved gas cap, a quote asset that reverts.
  • For Operate, what a run does to the next seller’s price.
  • Every error your module declares, and every action’s minimum and deadline.

script/Poo.s.sol is the same setup as a script, and the two commands run it differently: poo dev broadcasts it onto a local chain, while poo preview runs its Preview contract in memory — forge script --tc Preview, no chain and no key — and reads your manifest from there. Its token terms and your module’s config sit at the top of the file; change them when your config changes.

Send it

Try it on your own poo dev chain first. Without --broadcast it only checks, and it checks six things: the factory has code, its manifest is this checkout’s byte for byte, the registry would accept it, you are its developer, the handle is free or yours, and this factory is not published yet. The last one is why a new version needs a new factory — a factory the registry already lists is refused with the entry it holds.

publishFee() is a plain number the registry’s owner can change at any time, paid in the chain’s native coin — BNB, ETH, whatever the chain you’re on uses, never a token. Read it before you broadcast:

cast call <registry> "publishFee()(uint256)" --rpc-url <url>

cast prints the wei amount; a round number also gets a bracketed scientific reading beside it (5000000000000000 [5e15]) — the number before the bracket is what you send. The dry run below prints the same figure on its own fee line.

poo publish --rpc <url> --registry 0x… --factory 0x… --broadcast -- --ledger

Everything after -- is handed to cast untouched — the SDK never sees a key. Sign with whatever cast send accepts: --ledger or --trezor for a hardware wallet, --account <name> for a keystore under ~/.foundry/keystores, or --private-key/--mnemonic for a raw key. With --broadcast, the SDK prints the call itself, then hands your --rpc-url and everything after -- straight to cast to sign and send — this is what runs:

cast send <registry> "publishTokenModule(address)" <factory> --value <fee> --rpc-url <url> --ledger

It sends exactly the fee you read above; any other amount reverts WrongFee. Once that transaction is mined, the entry is live — no separate step. poo publish reads it straight back and reports the entry id, and the handle is now yours for every later version.

Handles and versions

  • Your manifest’s handle names your module family. The first wallet to publish under it holds it; later versions must come from that wallet.
  • Versions are numbered v1, v2, … in the order they are published. The catalogue shows the newest.
  • The module page reads, for example, Version v2 – Sep 14, 12:13, and links the other versions. A token page links the version that token uses.
  • Entries never change. Any code or manifest change means a new factory and a new version, and the tokens already carrying the old one keep carrying it.

Before you publish

  • forge build --sizes: factory and runtime fit EIP-170.
  • forge test: publishing and creation pass.
  • poo check: every import is on the published surface.
  • poo preview: the page reads as you meant.
  • The handle is the one you want to keep.
  • The description names every power, exemption path and run.
  • ERC-165 matches your hooks; caps come from measured worst cases.
  • requires accepts exactly the shares you use.
  • Security obligations answered.
  • Published to your own poo dev chain first.
  • publishFee() read on the chain you’re publishing to, and you hold that much of its native coin.
  • Your signing wallet ready — the flags cast send takes after --.