Build
Publish
Registering lists your factory in PooModuleDirectory, and it is what makes your module installable: PooLauncher installs only a factory its own directory lists. Registering is open to every developer — no fee, no allow-list and no admin — and decided entirely by checks the directory runs on chain.
What makes a factory installable
PooLauncher.isInstallable(factory) is the check a launch makes of every module factory it names, before Pons is ever called. It's true only when all three hold:
- the launcher's own directory —
directory(), fixed for good when the two were deployed — lists your factory (NotRegisteredotherwise); - your factory's deployed code is still the code it registered with (
NotRegisteredtoo, once it changes); - your factory's
launcher()still answers that same launcher (WrongLauncherotherwise).
A creator can name your factory in a launch from the moment it's registered.
Registering
function deployAndRegister(bytes initcode, bytes32 salt) external returns (address factory, uint256 index)
function publish(address factory) external returns (uint256 index)
function check(address factory) external view returns (bytes32 manifestHash)
function predictFactory(address developer, bytes32 salt, bytes32 initcodeHash) external view returns (address)
function launcher() external view returns (address)
function count() external view returns (uint256)
function listingAt(uint256 index) external view returns ((address factory, uint64 publishedAt, address developer, bytes32 manifestHash, bytes32 codehash))
function listings(uint256 start, uint256 limit) external view returns ((address factory, uint64 publishedAt, address developer, bytes32 manifestHash, bytes32 codehash)[] page)
function isPublished(address factory) external view returns (bool)
function isRegistered(address factory) external view returns (bool)
function matchesPublished(address factory) external view returns (bool)deployAndRegister deploys your factory with CREATE2 and registers it in the same transaction; predictFactory(you, salt, keccak256(initcode)) gives you its address first, and the salt is bound to your own address, so nobody else can land a factory there. publish registers a factory you already deployed yourself. Either way the call reverts unless:
- your factory answers
IPooModuleFactorythrough ERC-165 (NotAModuleFactory); - its
launcher()is the launcher this directory is bound to —launcher()on the directory tells you which (WrongLauncher); - its manifest passes the rules
poo checkalready ran for you locally — every boundModuleManifestCheckLibenforces, checked again on chain; - its
create, called by the directory itself — read-only, with an empty context and empty config, capped at 50,000 gas — reverts with exactly the 4-byteNotLauncher()error (LauncherNotEnforced). Acreatethat succeeds, reverts with anything else, runs out of gas, or writes state before its launcher check fails this, so keep that check ascreate's very first statement (The factory); - the caller is exactly your factory's own
developer()(NotDeveloper); - it hasn't been registered before (
AlreadyPublished).
check(factory) runs the first four without registering anything, so a refusal shows up by name before you send. A passing registration stores your factory's address, the block's own timestamp, its developer(), a hash of its manifest and a hash of its deployed code, then emits Published.
What the check doesn't prove
The probe proves one thing: in the single call the directory makes — read-only, from the directory itself, capped at 50,000 gas, with an all-zero context and empty config — your factory refuses every caller but launcher(). It can't prove your factory refuses every caller in every circumstance.
A factory built to get around it still registers: one that refuses only when the caller is the directory, only inside a staticcall, only under some gas ceiling, or only for one tx.origin; one that keeps launcher() in mutable storage or behind a proxy — the proxy's own codehash never changes, so isRegistered never notices; or one that exposes a second, unchecked way to create an instance that the probe never calls.
What that costs is narrow. A factory built this way can only ever compromise its own creator's copy of launcher — it can never reach the real PooLauncher, its splitters, POO.MEME's own treasury, or any other creator's launch, because every one of those checks a factory's launcher() and its code live, not what was true when it registered. And a registered factory that later points launcher() elsewhere, or whose deployed code changes, is refused by the real launcher from that moment on (WrongLauncher, NotRegistered) even though it stays listed in the directory.
What the check does hold: a factory that forgot the guard, or that runs a state write or a reentrancy guard before it, fails to register at all (LauncherNotEnforced). Registering is not a certification of your module's honesty beyond this one guard — that's still yours to carry, same as publishing any other open-source contract.
Listing, staying current, and no namespace
Entries are appended in the order they're registered — count() and listingAt(index) read one, listings(start, limit) a page of them. There's no removal, no re-ordering and no curation: nothing here ranks, hides or features one listing over another.
isRegistered(factory) answers false the moment your factory's deployed code (codehash) changes, and from then on a launch refuses it. matchesPublished(factory) answers false for that too, and also the moment what manifest() returns changes. There's no way to edit a listing already made — register a fresh factory address to bring a reader back in sync.
handle is a label the directory checks for shape alone — 1 to 32 characters of lowercase letters, digits and -, never starting or ending with one — and nothing else. Registering doesn't claim it: nothing stops a second factory from registering under the same handle.
Verify what you deployed
Registering does not verify anything on an explorer; submit your source yourself. Your factory is one contract. If its constructor deploys a module implementation for clones (How the factory makes an instance), that implementation is a second one (the first contract the constructor creates sits at the factory's first CREATE address), and it is the only module contract to submit: the clones have no source of their own. If create uses new, every instance is a contract of its own and needs its own submission with its constructor arguments.
Registering from the SDK
poo init scaffolds script/Publish.s.sol, plain forge script: it dry-runs by default and only sends a transaction with --broadcast.
forge script script/Publish.s.sol --rpc-url <url> # dry run
DIRECTORY=0x… forge script script/Publish.s.sol --rpc-url <url> --broadcast --private-key <key>DIRECTORY names the PooModuleDirectory to register with — its address is in the contracts table. Without FACTORY, the script deploys and registers your factory in one deployAndRegister call, bound to the launcher that directory answers for, at the address predictFactory(you, SALT, keccak256(initcode)) gives — SALT is optional and defaults to zero, and if that address already holds your factory the script says it's already registered and sends nothing. Pass FACTORY if you already deployed your own factory and only want to publish it. Try it first against a devkit chain of your own (forge script script/Poo.s.sol --tc Dev --rpc-url <anvil> --broadcast), where nothing is at stake, before you ever point it at a real directory.
