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
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);
}createdeploys 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
createruns; useonInstalledfor anything that needs it. - Anyone can call
create. Guard hooks on the token, andonInstalledon the token’s factory. - Publish from the factory, or have its
developer()orowner()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:
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:
(, 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:
| Rule | Refused As |
|---|---|
The launch’s supply, every token module’s supply and every supply row total totalSupply exactly | SupplyNotWhole |
| At most 8 supply rows, none zero, no recipient twice, none to a module | TooManySupplyRows, SupplyRowEmpty, SupplyRowRepeated, SupplyRowIsAModule |
A row to the creator or to 0x…dEaD has no name; every other row has one of 1–31 bytes | SupplyRowNameInvalid |
| At most 6 token modules, each meeting its entry’s requirements | TooManyTokenModules |
| A row paying the creator has no name; every other payee row has one of 1–31 bytes, and none pays a module | PayeeNameInvalid, PayeeIsAModule |
payeeCap is at most 13 and at least the rows written | TaxCapAboveMaximum, TaxCapBelowRows |
A payee contract with the Receive hook answers a receiveGas() of 1 to 265,000 | ReceiveGasUnreadable, ReceiveGasZero, ReceiveGasAboveCeiling |
| The token’s heaviest leg fits the transaction gas cap | SellGasOverTxCap |
expectedBlueprint is the factory’s current token code | BlueprintMismatch |
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 -- --ledgerEverything 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> --ledgerIt 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.
requiresaccepts exactly the shares you use.- Security obligations answered.
- Published to your own
poo devchain 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 sendtakes after--.
