On-chain escrow and settlement for Morcast creator campaigns on Base.
A brand escrows a campaign budget in USDC or MOR. After the campaign ends, Morcast publishes the measured results and settles the campaign once. The contract then pays the protocol fee, lets creators claim their share against a Merkle root, and returns the unspent budget to the brand. If the campaign is not settled within its settlement window, the brand can withdraw the full budget.
Protocol v1 is released as version 1.0.0 and deployed on Base (Deployments). Morcast's staging runs on Base Sepolia. Not audited.
| Component | State |
|---|---|
Escrow contract (MorcastEscrow) |
Implemented. Unit, invariant and Base fork tests |
Settlement arithmetic (SettlementMath) |
Implemented, unit- and fuzz-tested |
| Deployment scripts | Implemented. Used for the Base deployment and the Base Sepolia staging deployment (Deployments) |
| TypeScript SDK: settlement, payouts, metrics | Implemented, tested against the shared vectors |
| TypeScript SDK: document hashing, payout Merkle trees | Implemented. Trees verified against the contract |
TypeScript SDK: result dataset and verifier (morcast-verify) |
Implemented. Verified end to end on a local node |
| Security | Internal review completed, no critical, high or medium findings. External audit pending |
A brand approves the escrow for the budget and creates the campaign:
IERC20(usdc).approve(address(escrow), budget);
uint256 id = escrow.createCampaign(usdc, budget, target, startAt, endAt, manifestHash);Between Day 5 and Day 10 after endAt, the settler settles the campaign with the recognized total from the published result dataset:
escrow.settle(id, recognized, merkleRoot, resultHash);Then each party collects its share:
escrow.claim(id, wallet, amount, proof); // creator payout; anyone may submit it
escrow.withdrawFee(id); // protocol fee to the treasury; anyone may call it
escrow.withdrawBrand(id); // unspent budget back to the brandIf the campaign is not settled before Day 10, the brand calls withdrawBrand(id) from Day 10 to recover the whole budget.
The owner, a wallet Morcast holds, operates the escrow. It can replace the settler and the treasury, choose tokens and pause creation for new campaigns, void a funded campaign (the budget goes back to its brand) and recover tokens sent to the escrow by mistake. It can never move a campaign's funds anywhere else.
Anyone can check a published result dataset, and compare it with the escrow, with the SDK package from a release:
npx --package=https://github.com/Morphora-Lab/morcast-protocol/releases/download/v1.0.0/morcast-protocol-1.0.0.tgz \
morcast-verify result.json --rpc-url https://mainnet.base.orgA brand escrows 100,000 USDC for a target of 1,000,000 qualified views, and Morcast recognizes 640,000:
| Party | Receives |
|---|---|
| Morcast treasury (fee, 20% of spent) | 12,800 USDC |
| Creators (pool, 80% of spent) | 51,200 USDC |
| Brand (refund) | 36,000 USDC |
The brand pays for 64% of the target, so 64,000 USDC is spent.
- Escrow contract: roles, timeline, functions, payout leaves and security properties.
- Settlement arithmetic: creator scores, the budget split and creator payouts.
- Metrics: exact decimals and the Integration retention metric.
- Hashing and Merkle trees: canonical JSON for
manifestHashandresultHash, and payout tree construction. - Result dataset: the published result format, reason codes and what the verifier checks.
- Security: trust model, internal review findings and verified properties.
- Deployment: deploying to Base mainnet, other networks and a local Anvil node.
- Versions and upgrades: how a new version is released and what off-chain systems must support.
- TypeScript SDK: the protocol arithmetic for off-chain services and verifiers.
Requirements: Foundry 1.8 or later.
git clone --recurse-submodules https://github.com/Morphora-Lab/morcast-protocol.git
cd morcast-protocol
forge build
forge testDependencies are Git submodules pinned in foundry.lock: forge-std v1.17.0 and OpenZeppelin Contracts v5.7.0.
The TypeScript SDK lives in sdk/ and has its own instructions.
| Suite | Location | Checks |
|---|---|---|
| Unit | test/*.t.sol |
Every function, revert path and time boundary. |
| Fuzz | testFuzz_* functions |
Settlement and payout properties for random inputs. |
| Invariant | test/invariant/ |
Across random multi-campaign sequences, escrow balances equal outstanding obligations and settled campaigns follow the formula. |
| Fork | test/fork/ |
Full lifecycle with the real USDC and MOR contracts on Base mainnet. Skipped unless BASE_RPC_URL is set. |
| Vectors | vectors/ |
Numeric examples from the specification, shared with off-chain implementations. |
| End-to-end | scripts/e2e-local.sh |
On a local Anvil node: deploy, create the example campaign, settle it from examples/result-dataset.json, verify it on-chain and claim every payout with SDK proofs. |
forge test # unit, fuzz and invariant tests
BASE_RPC_URL=https://mainnet.base.org forge test --match-path "test/fork/*"
(cd sdk && pnpm install && pnpm build) && scripts/e2e-local.shVersions follow semantic versioning. A tag such as v1.0.0-rc.1 marks a pre-release. Pushing a version tag on main runs the release workflow. It tests and packs the SDK, attests the package's build provenance and publishes a GitHub release with the package attached. The workflows pin every action to a commit hash, and Dependabot proposes updates to them and to the SDK's packages weekly.
| Release | State |
|---|---|
v1.0.0-rc.1 |
Release candidate of protocol v1: feature-complete, internally reviewed. External audit pending, not deployed |
v1.0.0-rc.2 |
Adds the escrow's code hash to the SDK (ESCROW_CODE_HASH, isGenuineEscrow) |
v1.0.0-rc.3 |
A creator may take part in a campaign with several accounts. Each creator in a result dataset lists its accounts, and each account belongs to one wallet |
v1.0.0-rc.4 |
The contract takes the product's name, MorcastEscrow. Its rules and its ABI are the same. Its code hash is new, so a deployment of an earlier release is not this release's escrow |
v1.0.0 |
The first release of protocol v1, deployed on Base. The contract is the one of v1.0.0-rc.4, so its code hash is the same. No external audit yet |
The escrow's VERSION() names the contract version a deployment runs. See Versions and upgrades.