Skip to main content

Platform Enlistment

Source: Full executable TypeScript in contract/src/examples/00-platform-enlistment/. This page summarizes the flow — read the source for complete per-step code.

The Story

NovaPay is a digital marketplace that helps independent sellers accept payments online. They want to integrate Oak Protocol to offer their merchants on-chain payment processing and crowdfunding capabilities. Before any campaign can be created or treasury deployed on their platform, NovaPay must first be enlisted as a platform on the protocol.

Platform enlistment is a coordinated process between two roles:

  • The Protocol Admin (the Oak Network team) — who governs the GlobalParams contract and must approve every new platform joining the protocol
  • The Platform Admin (NovaPay's operations wallet) — who will manage the platform's day-to-day configuration once enlisted, such as treasury registration, fee settings, and payment operations

There is no self-service signup. NovaPay contacts the Oak support team, provides their admin wallet address, and agrees on a fee structure. The Protocol Admin then records the enlistment on-chain in a single transaction.

Once enlisted, NovaPay registers the treasury implementation contracts they want to use. A platform can register as many or as few treasury models as they need — even a single model is enough to get started. Each registration enters a "pending" state and must be explicitly approved by the Protocol Admin before it can be used to deploy treasuries.

Platform Hash

Every platform on Oak Protocol is identified by a bytes32 value called the platform hash. It is the keccak256 hash of the platform name and remains fixed for the lifetime of the platform.

const platformHash = keccak256(toHex("NOVAPAY"));

Implementation ID Layout

Each platform maintains its own mapping of implementation ID to treasury contract inside TreasuryFactory:

Implementation IDTreasury ModelUse Case
0nAllOrNothingCrowdfunding — backers get a full refund if the goal is not met
1nKeepWhatsRaisedCrowdfunding — the creator keeps whatever is raised, even if the goal is not met
2nPaymentTreasuryE-commerce — structured payments with no time restrictions
3nTimeConstrainedPaymentTreasuryE-commerce — same as PaymentTreasury but enforces launch time and deadline on-chain

Role Reference

FunctionWho can callContract modifier
enlistPlatformProtocol AdminonlyOwner
delistPlatformProtocol AdminonlyOwner
updatePlatformAdminAddressProtocol AdminonlyOwner
updateProtocolFeePercentProtocol AdminonlyOwner
addTokenToCurrency / removeTokenFromCurrencyProtocol AdminonlyOwner
setPlatformAdapterProtocol AdminonlyOwner
setPlatformLineItemType / removePlatformLineItemTypePlatform AdminonlyPlatformAdmin
updatePlatformClaimDelayPlatform AdminonlyPlatformAdmin
addPlatformData / removePlatformDataPlatform AdminonlyPlatformAdmin
All get* / check* readsAnyone(read-only)

Steps

Step 1: Enlist the Platform

The Protocol Admin calls enlistPlatform on GlobalParams. This single transaction sets the platform hash, admin address, fee percent, and adapter. Any other caller reverts with GlobalParamsUnauthorizedError.

const platformHash = keccak256(toHex("NOVAPAY"));
const platformAdminAddress = process.env.NOVAPAY_ADMIN_ADDRESS! as `0x${string}`;
const platformFeePercent = 250n; // 2.5% in basis points
const noAdapter = "0x0000000000000000000000000000000000000000" as `0x${string}`;

const txHash = await globalParams.enlistPlatform(
platformHash,
platformAdminAddress,
platformFeePercent,
noAdapter,
);

const receipt = await oak.waitForReceipt(txHash);
console.log(`NovaPay enlisted at block ${receipt.blockNumber}`);

Step 2: Verify the Enlistment

After the Protocol Admin enlists NovaPay, anyone with a read-only client can verify the on-chain state. No private key is needed — these are pure view calls against GlobalParams.

const platformHash = keccak256(toHex("NOVAPAY"));

const isListed = await globalParams.checkIfPlatformIsListed(platformHash);
const adminAddress = await globalParams.getPlatformAdminAddress(platformHash);
const feePercent = await globalParams.getPlatformFeePercent(platformHash);
const adapter = await globalParams.getPlatformAdapter(platformHash);

console.log("Platform listed:", isListed);
console.log("Admin address:", adminAddress);
console.log("Fee percent:", Number(feePercent), "bps");

Step 3: Register a Treasury Implementation

Now that NovaPay is enlisted, the Platform Admin registers the treasury model they want to use. Each model goes into a numbered slot (implementationId) on TreasuryFactory. Registrations are not immediately active — they sit in a pending state until the Protocol Admin approves them.

const platformHash = keccak256(toHex("NOVAPAY"));
const allOrNothingImpl = process.env.ALL_OR_NOTHING_IMPL! as `0x${string}`;

const txHash = await treasuryFactory.registerTreasuryImplementation(
platformHash,
0n, // slot 0
allOrNothingImpl,
);
await oak.waitForReceipt(txHash);
console.log("AllOrNothing registered at slot 0 — awaiting approval");

Step 4: Approve the Implementation

The Protocol Admin explicitly approves the registered implementation. This is a safety gate — the protocol team verifies the implementation contract before allowing the platform to deploy treasuries from it. Each registered slot requires its own approval call.

const platformHash = keccak256(toHex("NOVAPAY"));

const txHash = await treasuryFactory.approveTreasuryImplementation(platformHash, 0n);
await oak.waitForReceipt(txHash);
console.log("AllOrNothing approved (slot 0)");

Step 5: Verify Full Setup

Before going live, NovaPay's admin runs a final check to confirm every piece of the onboarding is in place: the platform is listed, admin and fee values match agreed terms, and treasury implementations are registered and approved.

const isListed = await globalParams.checkIfPlatformIsListed(platformHash);
const adminAddress = await globalParams.getPlatformAdminAddress(platformHash);
const feePercent = await globalParams.getPlatformFeePercent(platformHash);

// Derive current registrations by replaying Registered vs Removed events
const registeredLogs = await treasuryFactory.events.getImplementationRegisteredLogs();
const removedLogs = await treasuryFactory.events.getImplementationRemovedLogs();
const approvalLogs = await treasuryFactory.events.getImplementationApprovalLogs();

// ... filter by platformHash, check the latest approval state per implementation

Step 6: Optional Configuration

After core onboarding, a platform can configure additional features. These are all optional and independent — skip any you don't need.

SectionFeatureWho CallsApplies To
ALine Item TypesPlatform AdminPaymentTreasury only
BClaim DelayPlatform AdminPaymentTreasury only
CPlatform Data KeysPlatform AdminAll treasury types
DPlatform AdapterProtocol AdminAll treasury types
EProtocol Admin FunctionsProtocol AdminProtocol-wide

Example — register a line item type for "product" (counts toward goal, refundable):

const productTypeId = keccak256(toHex("product"));
await globalParams.setPlatformLineItemType(
platformHash,
productTypeId,
"product",
true, // countsTowardGoal
false, // applyProtocolFee
true, // canRefund
false, // instantTransfer
);

Multi-token currencies

GlobalParams holds currencyToTokens: bootstrapped in initialize(currencies, tokensPerCurrency), then the protocol owner can addTokenToCurrency / removeTokenFromCurrency; anyone can read getTokensForCurrency(currency). When CampaignInfoFactory creates a campaign, it reads the current token list and stores a snapshot on CampaignInfo; treasuries validate every paymentToken / pledgeToken against that cache.