Moolam

API Reference

VerifierPolicy and MoolamReceiver

Auf dieser Seite

The contracts around the registry: the one that says who may attest, and the receivers a Chainlink workflow writes through. The policy lists three today. MoolamReceiver, below, is the door for Chainlink's network, back on the list since 2026-10-02. Two SimulationReceiver contracts, one per workflow, take the test runs from Chainlink's simulator, and have since 2026-09-27.

VerifierPolicy

0x54e8Ed8c2c3Cf2A36F8B3AC4c7f02acFD2455821 on Monad mainnet. ABI in packages/contracts/abi/VerifierPolicy.json.

The only mutable piece of Moolam. It holds the list of addresses allowed to write attestations, the dispute resolver, the treasury and the bond size. Every change that widens trust waits 24 hours in the open. Removing a receiver is immediate.

Constants

ConstantValue
CHANGE_DELAY()24 hours
OP_ADD_RECEIVER()keccak256("ADD_RECEIVER")
OP_SET_RESOLVER()keccak256("SET_RESOLVER")
OP_SET_TREASURY()keccak256("SET_TREASURY")
OP_SET_DISPUTE_BOND()keccak256("SET_DISPUTE_BOND")

Reads

FunctionReturns
isReceiver(address who)True when that address may call attest on the registry
resolver()The address allowed to settle disputes
treasury()Where a rejected challenger's bond goes. Since 2026-10-02 it is the burn address 0x000000000000000000000000000000000000dEaD
disputeBond()The exact bond a flag must carry, in wei
bootstrapped()False until the setup window is closed. Once true it can never go back
getPendingChange(bytes32 id)The queued change. A zero executeAfter means nothing is queued under that id

The setup window

function bootstrapAddReceiver(address who) external   // onlyOwner
function finalizeBootstrap() external                 // onlyOwner

bootstrapAddReceiver lists a receiver at once, during the setup window only. finalizeBootstrap closes that window for good. The deploy script does both in the same batch as the deploy.

RevertsWhen
BootstrapClosedThe window is already closed
ZeroAddresswho is the zero address
AlreadyReceiverThe address is already listed

Emits ReceiverAdded and BootstrapFinalized.

Removing a receiver

function removeReceiver(address who) external   // onlyOwner

No delay. A compromised verifier has to stop writing in the same block the owner notices. Reverts NotAReceiver when the address is not listed. Emits ReceiverRemoved.

The 24 hour queue

function queueAddReceiver(address who)      external returns (bytes32 id)   // onlyOwner
function queueSetResolver(address who)      external returns (bytes32 id)   // onlyOwner
function queueSetTreasury(address who)      external returns (bytes32 id)   // onlyOwner
function queueSetDisputeBond(uint256 bond)  external returns (bytes32 id)   // onlyOwner
function execute(bytes32 id)                external                        // onlyOwner
function cancel(bytes32 id)                 external                        // onlyOwner

The change id is keccak256(abi.encode(op, data)), so the same change cannot be queued twice.

queueSetTreasury probes a candidate that has code with an empty zero value call before it can be queued, which catches the common mistake of pointing the treasury at a contract with no way to take native token at all. It is a heuristic and nothing more: a contract can pass this probe and still refuse a real payout. The registry's withdrawTo is the actual escape hatch.

RevertsWhen
ZeroAddressAn address argument is zero
AlreadyReceiverqueueAddReceiver on an address already listed
TreasuryCannotReceiveThe treasury probe failed
ChangeAlreadyQueuedThat exact change is already in the queue
UnknownChangeexecute or cancel on an id with nothing queued
ChangeNotReadyexecute before executeAfter

Two changes that ran on 2026-10-02 came through this queue. MoolamReceiver was listed again at 07:08:18 UTC, block 109,830,535, in transaction 0x446c3051…ff9f. The treasury was set to the burn address, change id 0x87571be2e5c590b8fa1e9f2921d8a3807c8e210f216c32fbc1f29b069030f3a0, at 07:27 UTC, block 109,834,440, in transaction 0x45700954…371d. Rejected bonds are burned.

Each queue* call emits ChangeQueued. execute emits ChangeExecuted plus the event for the specific change (ReceiverAdded, ResolverSet, TreasurySet or DisputeBondSet). cancel emits ChangeCancelled.

Ownership uses Ownable2Step.

MoolamReceiver

0x0d69055c43EAcb3B1ca687ca2263A049Bc7Eff04 on Monad mainnet, listed in the policy. ABI in packages/contracts/abi/MoolamReceiver.json.

The door Chainlink's network writes its verdicts through. It listens to Chainlink's production Keystone Forwarder, 0x76c9cf548b4179F8901cda1f8623568b58215E62, and takes a report only from the workflow author and name it is pinned to. It inherits Chainlink's ReceiverTemplate unchanged, so the forwarder check and the workflow identity checks are Chainlink's, not Moolam's.

It was the first receiver, deployed with the registry. It came off the policy on 2026-09-26, when simulated reports got receivers of their own, and went back on the list on 2026-10-02 at 07:08:18 UTC (block 109,830,535, transaction 0x446c3051322060339d8e95bcc2938ca6db6692e8d583d360ebab6e626ea1ff9f). The first verdict that came through it from Chainlink's network is block 109,833,597, transaction 0x760dcb5e1b83929201d8fe299ce9c3be2cf8981502117c5288386d7da3438882. Every attestation it wrote before 2026-09-26 stays on chain under its address. Its sealed twin 0x7b9eF7cD5e40Af9c29d8b7d0D22E54B80947E45A stays off the list.

Since 2026-09-27 14:18 UTC the policy also lists two SimulationReceiver contracts, for runs in Chainlink's simulator: 0x4aD66c77f961884E9e866EEEc3EafF1EdB5BAa95 for moolam-verifier, listed in transaction 0xac642cc5c89290a2659e0ee7aaa78d7b729643f22138725815590dd2abbc0bfb, and 0x52b8fE549B432920eeB061c26cAc5c2D527657f6 for moolam-sealed-verifier, listed in transaction 0x0e1e2937ee4593cb8aad78d4bb6696b14584080b216d4c3c028f0c32eecf457d. They take the same onReport call and payload as below, from Chainlink's MockKeystoneForwarder, and add one lock in front of the payload checks: the transaction must be signed by Moolam's CRE wallet 0xC79620AF233a4434b03f6B57239F8A9E71B3C178, or it reverts WrongSendingWallet. A report more than an hour ahead of the block reverts ReportFromTheFuture. The full account is in Chainlink CRE workflow, "The receiver, and what a simulation needs". ABI in packages/contracts/abi/SimulationReceiver.json.

onReport

function onReport(bytes calldata metadata, bytes calldata report) external

Called by the Chainlink KeystoneForwarder. Three locks run before anything is written, the first two from the template:

  1. msg.sender must be the configured forwarder, or it reverts InvalidSender.
  2. The report metadata must carry the workflow author and workflow name this receiver is pinned to, or it reverts InvalidAuthor, InvalidWorkflowName or InvalidWorkflowId.
  3. The decoded payload must be for this chain and newer than the last report for that passport.

The payload is:

abi.decode(report, (uint256 chainId, uint64 executedAt, bytes32 passportId,
                    bool matched, uint16 distance, bytes32 recomputed))

chainId and executedAt are inside the signed payload because Chainlink's own consumer guidance says a report whose first transmission reverted can be sent again, and the same signed report can be delivered on another chain. Both are checked before anything is written, which turns a replay into a revert instead of a duplicate attestation.

RevertsWhen
InvalidSenderThe caller is not the configured forwarder
InvalidAuthorThe report author is not the pinned one
InvalidWorkflowNameThe workflow name is not the pinned one
InvalidWorkflowIdThe workflow id is not the pinned one
WrongChainchainId is not block.chainid
StaleReportexecutedAt is not newer than the last one recorded for that passport
Whatever the registry refusesNotReceiver, PassportNotFound, AttestationCapReached

On success it calls attest on the registry, which emits VerificationAttested.

Reads

FunctionReturns
REGISTRY()The registry this receiver writes into, immutable
lastExecutedAt(bytes32 passportId)The executedAt of the newest report accepted for that passport, zero when none has landed
getForwarderAddress()The forwarder allowed to call onReport
getExpectedAuthor()The pinned workflow author
getExpectedWorkflowName()The pinned workflow name, bytes10
getExpectedWorkflowId()The pinned workflow id

Owner-only

function setForwarderAddress(address _forwarder) external
function setExpectedAuthor(address _author) external
function setExpectedWorkflowName(string calldata _name) external
function setExpectedWorkflowId(bytes32 _id) external

Chainlink's template setters. The workflow author and name are not set in the constructor because the template stores them through these owner-only setters, so the deploy script pins them in the same transaction batch and script/ConfigureReceiver.s.sol can repin later. Each one emits an event: ForwarderAddressUpdated, ExpectedAuthorUpdated, ExpectedWorkflowNameUpdated, ExpectedWorkflowIdUpdated.

None of the four setters checks its argument; each reverts only OwnableUnauthorizedAccount for a caller who is not the owner. A workflow name pinned without an author is accepted by the setter and makes every later onReport revert WorkflowNameRequiresAuthorValidation. A zero forwarder is accepted too, with a SecurityWarning event, and turns the forwarder check off. InvalidForwarderAddress is raised only by the constructor.

The VerifierPolicy list is a separate control. It decides which receivers the registry will listen to at all, and it is not a second lock on this door.