A confirmed-obligation receivables desk, on-chain. The buyer confirms the debt before it's financed, and Cleanverse verified identity prices it.
Live: https://covenant.unitynodes.com · desk at /desk · deck at /pitch
Standard invoice factoring only checks the financier; the payer is trusted on paper. Covenant requires the obligor (the party who owes the money) to hold a Cleanverse A-Pass and to actively confirm the debt on-chain — "I owe this amount, to this supplier, not pledged elsewhere" — before a supplier can tokenize and finance it. That single requirement moves this out of generic on-chain factoring (Centrifuge, Huma, Polytrade) into confirmed/approved payables finance, the institutional category behind Taulia (acquired by SAP) and PrimeRevenue, where the buyer's affirmative confirmation, not a financier's trust in paper, is what de-risks the trade.
How deeply the obligor is verified then prices the instrument: the same $100,000 invoice returns 97,000 cvaUSD against a subTier-90 obligor and 88,000 against a subTier-30 one, live, on-chain — honestly labeled a compliance/eligibility proxy, never a credit score.
Built for the Cleanverse "Trusted Assets" Hackathon (supported by Monad Foundation), Track 1 — RWA tokenization with compliance embedded from issuance.
Both halves are live on Monad testnet and neither is a stand-in.
CVI — identity is read on-chain, with no oracle in between. Cleanverse's A-Pass registry (0xbA82D1…C58B9) exposes getAPassData(address), returning status, tier, subTier, group, subGroup and expiry. CleanverseIdentityRegistry reads it directly in the same transaction as the gate it protects, and holds no identity state of its own. Covenant's registry is also registered as a pool with Cleanverse's compliance validator (0xaC7e51…61792) under a RuleV2 of minSubTier: 19 and a US country whitelist, so isVerified requires both an active A-Pass and Cleanverse's own rule engine to say yes. That is load-bearing twice over: 0x…dEaD holds an active A-Pass with subTier 0 and is denied, and a wallet with a perfectly valid subTier-60 pass issued against a Singapore passport is denied on country alone.
The rule reads 19 rather than 20 on purpose. min_sub_tier is documented as strictly greater-than, so a floor of 20 would deny the wallet sitting exactly on Covenant's own band boundary — the two would disagree by one, in the direction that silently blocks a legitimate counterparty.
And the same field name means something different on the A-Token. cvaUSD carries its own rule, readable through atoken/rules, which also spells the field min_sub_tier — but there it is inclusive. The token is launched at 20 and accepts 20. One convention does not cover both surfaces, so assuming either would have been off by one at the exact value the whole pricing story rests on, in one direction or the other depending on which way it was guessed.
It is measured rather than assumed. npm run boundary issues a throwaway wallet a real A-Pass at exactly subTier 20 and then asserts, on-chain, that the registry reports 20, that the validator's RuleV2 admits it, that the curve prices it at 8800, and that the A-Token's own policy accepts a mint to it. All five pass — and the mint was also sent for real, 0x5d6152f5…2b8fd88b, taking the wallet from 0 to 1.000000 cvaUSD in block 52,207,261. Re-run it after any rule change.
CVA — the settlement asset is a real Cleanverse A-Token. cvaUSD (0x01926a…ACF0B) was issued by Cleanverse through atoken/launch, under a rule Covenant chose. Its policy — not Covenant's — decides who may hold it: canTransfer returns true between two verified wallets, reverts naming the offending party when either side lacks a pass, and refuses to mint to an unverified wallet at all. Covenant cannot override it.
An earlier design mirrored query_apass into on-chain storage through a permissioned relayer, on the assumption that CVI had no on-chain read surface. It does. IdentityRegistry survives only for local devnets and chains where CVI is not deployed; both implement IIdentityRegistry, so nothing downstream knows which one it is talking to.
Cleanverse assigns tier itself. All four live demo wallets came back as tier 50, so it cannot carry an application's own risk banding — an advance-rate curve keyed on tiers 1/2/3 worked against seeded fixtures and reverted against every real identity. subTier (0–99) is the axis the integrator sets at issuance, which is Cleanverse's own documented guidance, so that is what Covenant prices against:
| subTier band | Advance rate |
|---|---|
| ≥ 80 · institutional | 97.00% |
| ≥ 50 · established | 93.00% |
| ≥ 20 · basic | 88.00% |
| below 20 | blocked — no band, originate() reverts |
Set in one place, src/CovenantDefaults.sol, which the deploy script, the demo narrative and the test suite all read.
- The obligor confirms.
ObligationRegistry.confirmObligation(invoiceHash, supplier, faceValue, maturity)— callable only by an A-Pass-verified wallet. Keyed onkeccak256(invoiceHash|obligor|supplier|faceValue|maturity); a second confirmation of the same debt revertsAlreadyConfirmed. A gaslessconfirmObligationBySigvariant (EIP-712) lets the obligor sign off-chain and a relayer submit on their behalf. - The supplier originates, only against an existing, unconsumed confirmation.
ReceivableNote(ERC-721) snapshots an immutable compliance envelope at mint — minimum financier subTier, required jurisdiction, and the advance rate read off the obligor's subTier as confirmed — and mints one note per invoice. - A verified financier funds it, paying the discounted advance in cvaUSD to the supplier and receiving the note.
- The obligor settles at maturity, paying face value to whoever holds the note, which burns.
Three independent checks on every cash leg, not one. ReceivableNote._update enforces who may hold the note. FactoringEscrow._requireCompliant independently re-checks both settlement parties immediately before moving money, reverting NonCompliantSettlementParty. And cvaUSD's own Cleanverse policy refuses the transfer inside the token itself. Removing any one of the three still leaves the other two enforcing.
| Contract | Role |
|---|---|
CleanverseIdentityRegistry.sol |
Production identity source. Reads Cleanverse CVI directly on-chain, optionally gated by Cleanverse's own RuleV2. Stateless, and fails closed on every path. |
ObligationRegistry.sol |
The core mechanic: obligor confirmation, direct or EIP-712 signed. AlreadyConfirmed on replay. Snapshots the obligor's tier and subTier at confirmation. |
ReceivableNote.sol |
ERC-721 Compliant Receivable Note. originate / fund / settle, subTier rate bands, compliance-gated transfer hook. |
FactoringEscrow.sol |
Independent re-check on every cash leg — a second gate distinct from the note's. |
CovenantDefaults.sol |
The desk's default risk policy in one place, so scripts and tests cannot drift from what is deployed. |
IdentityRegistry.sol |
Relayer-written CVI mirror. Devnets and chains without CVI only. |
CompliancePolicy.sol / MockAToken.sol |
Devnet settlement asset and its policy, shaped like the real A-Token's canTransfer(token, from, to, amount). Not in the money path on Monad, where the asset is Cleanverse's own. |
Cleanverse's contracts (read and settled through, not deployed by Covenant):
| Address | |
|---|---|
| CVI A-Pass registry | 0xbA82D189540CaC9DC6FF46B6837CaC1BFdEC58B9 |
| CVI compliance validator | 0xaC7e5179C2C7f03f209136886c172eb34F161792 |
| CVA settlement token · cvaUSD | 0x01926a2Fb91BFE75347a5495123fbC1A85aACF0B |
Covenant's contracts. All five deployed contracts are verified on Sourcify at exact_match — compiled bytecode and metadata hash both agree with the source — so each address resolves to readable Solidity, not just bytecode:
| Address | ||
|---|---|---|
| CleanverseIdentityRegistry | 0xdFC23c4c2353251E1C9887d5c2F77733f59A5f01 |
source |
| ObligationRegistry | 0x37e7359352907FbaCca86deEa8DA82b681102726 |
source |
| ReceivableNote | 0xAa15aBFC21424B842B5272BdeD23158D180E785A |
source |
| FactoringEscrow | 0x0FeB657764b2eDaD9bEf1Dd5A786d71b74528836 |
source |
| CompliancePolicy | 0x7BfbF09D62e27835E7201B2037B1A3AEDbD56051 |
source |
The deck and the one-pager list the four in the money path; CompliancePolicy is the
devnet asset's policy, deployed by the same script but wired to nothing on Monad, where
the settlement asset is Cleanverse's own.
Re-run or re-check with python3 docs/verify-contracts.py --status.
Cleanverse acting on Covenant's request:
| Transaction | |
|---|---|
| A-Pass issued into CVI's registry (subTier 90) | 0xfd728936…d20a0257 |
| cvaUSD launched by Cleanverse | 0xf36903df…c72c094e |
| Registry registered as a validator pool | 0x530e97fd…b7b72240 |
One full lifecycle, four separate wallets:
| Step | Transaction |
|---|---|
| Confirm · obligor subTier 90 | 0x0b4d9a6f…0f529774 |
Originate · advanceRateBps 9700 |
0x1aab2935…c12bf181 |
Originate, subTier-30 obligor · advanceRateBps 8800 |
0x685eb99d…9c0a91dc |
| Fund · 97,000.00 cvaUSD to the supplier | 0xb618579a…e952e7f3 |
| Settle · 100,000.00 cvaUSD, note burned | 0xdc369af7…cb06117e |
The supplier's balance moved by exactly the advance and the financier's by exactly the discount; ownerOf(1) now reverts. The runner asserts all three and fails if any is off — see relayer/src/liveLifecycle.ts.
Nothing in the compliance path has to be operated.
-
No oracle to keep alive. Identity is read from CVI in the same transaction as the gate — no relayer key to rotate, no staleness window, no process to restart. Onboarding a counterparty is issuing them an A-Pass.
-
Another chain is a deploy, not a rewrite. Cleanverse's validator answers at the same address on Monad testnet, Base Sepolia and BSC testnet — verified by calling
isRegisteredon all three. (Not present at that address on Arbitrum Sepolia or Polygon Amoy, so "every chain" would be an overstatement.) -
Mainnet is the same deploy, and the contracts already behave correctly there. Monad mainnet (chainId 143) runs the production A-Pass registry at the same address as testnet, behind a different implementation. It answers:
hasAPassreturns false andgetAPassDatareverts0xfb524a44for every wallet, because no A-Pass has been issued on Monad mainnet yet. The compliance validator is not deployed there — which is exactly whyvalidatoris optional and owner-settable rather than a constructor argument. A Covenant deployed to mainnet today reads the real registry, verifies nobody, and blocks every action: failing closed, with no code change, until Cleanverse issues the first mainnet pass. -
Every write is O(1) in the size of the book. No function in any deployed contract loops over notes, obligors or holders — the only loop in the system walks the rate curve, which is three entries. Confirming, originating, funding and settling touch a fixed number of storage slots whether the desk holds twelve notes or twelve million.
-
What a trade actually costs, measured on-chain rather than estimated. Every leg of a complete lifecycle on Monad testnet, including the two ERC-20 approvals a real counterparty has to sign:
Step Signer Gas confirmObligationobligor 346,414 originatesupplier 564,780 approve(advance)financier 71,677 fundfinancier 731,057 approve(face)obligor 71,665 settleobligor 552,142 One complete trade 2,337,735 That is dearer than a bare ERC-721 mint-and-transfer, and deliberately so: funding is the costliest leg because it clears three independent compliance checks — the note's holder gate, the escrow's own party check, and the A-Token's policy — before any value moves. The gas is the guarantee. At the prevailing 102 gwei that is 0.238 MON per trade, it does not grow with the book, and it does not grow with the invoice: the live book spans 5,600 to 156,000 cvaUSD and every trade in it cost the same.
-
~64 complete trades fit in one Monad block. The block gas limit is 150,000,000; a full lifecycle is 2,337,735. Ten confirmations signed up front and fired without waiting were submitted in 117 ms and all mined in a single block (52,235,358) with zero failures — nothing serialised. Submit-to-receipt latency across one trade's six legs: p50 246 ms, p95 249 ms. Method in docs/SCALE.md; every transaction hash, including all ten from the burst, in docs/load-report.json.
-
Room to grow inside the same contracts.
ReceivableNotecompiles to 11,235 bytes against the 24,576-byte EIP-170 ceiling; the other three sit between 6% and 19% of it. -
Reads scale by batch, not by row. The desk's registry folds its note reads into a single
eth_callthrough Multicall3 (deployed at its canonical address on Monad testnet), so the whole register costsceil(n / 50)requests rather than one per note — measured at 2 RPC round trips and 97 ms for the 27 live notes, and 4 for two hundred. It used to be a sequential scan with a hard ceiling at 200. -
Revocation is instant and global. Freezing one A-Pass disables that party wherever CVI is read, with nothing to do on Covenant's side.
-
Liquidity scales by participants. A confirmed obligation is a composable primitive, not a row in one platform's database; any verified financier can compete for the note.
Covenant consults Cleanverse's rule engine on every check, which means Cleanverse can revoke and Covenant obeys in the same transaction. Tested on a wallet holding a live subTier-60 US A-Pass:
| Step | query_apass.status |
isVerified on-chain |
|---|---|---|
| Issued | 1 | true |
update_status → freeze |
2 | false |
update_status → unfreeze |
1 | true |
Full adversarial review, including six owner-only findings that were deliberately not shipped and why: docs/REVIEW.md. Slither reached the most serious of them independently — docs/STATIC-ANALYSIS.md.
forge build && forge test -vv75 tests across three kinds:
- 49 example tests — happy-path lifecycle, subTier repricing,
AlreadyConfirmed/AlreadyOriginatedreplay guards, escrow and token-level compliance gates, EIP-712 signed confirmation (valid / expired / wrong-signer), and the live identity adapter against a mock CVI that reproduces the real contract's revert-on-missing-pass behaviour. - 17 property tests (
test/Fuzz.t.sol) — 256 randomised runs each. The rate curve is monotonic across all 256 subTiers and never prices above face; two different obligations never share a key; a whole lifecycle conserves value for any face value and any maturity; no unverified address anywhere in the address space can hold a note. - 9 system invariants (
test/Invariant.t.sol) — 12,800 randomly ordered calls each, from nine wallets spread across every band, with identity revocation and time skips mixed in. The escrow's balance is zero at every step, value is conserved, and no note is ever priced off the published curve.
Coverage: 97.5% of lines and 94.1% of branches across the five deployed contracts (forge coverage). ObligationRegistry — the core mechanic — is at 100% on every metric.
Static analysis: Slither 0.11.6, 102 detectors over 41 contracts, every finding triaged in docs/STATIC-ANALYSIS.md. Its top finding is the same one the manual review reached independently, which is the useful part.
test/KnownBehaviour.t.sol is worth calling out separately: it pins every finding in docs/REVIEW.md as an executable assertion against the deployed bytecode. Those findings were not fixed, because editing verified source un-matches it — so instead of describing the edges in prose, the suite proves what the live contracts actually do at each one, and a later fix has a failing test to flip rather than a paragraph to re-read.
Deploy against a chain where Cleanverse CVI is live:
CLEANVERSE_APASS_ADDRESS=0xbA82D189540CaC9DC6FF46B6837CaC1BFdEC58B9 SETTLEMENT_ASSET_ADDRESS=0x01926a2Fb91BFE75347a5495123fbC1A85aACF0B forge script script/Deploy.s.sol --broadcast --rpc-url <rpc_url> --private-key <key>Leave both unset and the script deploys the relayer mirror plus its own settlement token instead, so a bare devnet run is self-contained.
Cleanverse-side tooling lives in relayer/: npm run apass issues the demo cast their A-Passes, registerPool.ts registers a contract with the validator, liveLifecycle.ts drives the whole trade from four keys, npm run revoke zeroes any settlement-asset allowance left standing against the escrow (see Finding 3 in docs/REVIEW.md), and npm run load runs the measured book behind docs/SCALE.md — --plan prices it without sending anything.
Said explicitly here because they belong in the pitch, not buried:
- This does not stop invoice fraud broadly. It prevents re-confirming or re-tokenizing the same confirmed obligation on Covenant. It does not stop a fabricated invoice (the obligor could dishonestly confirm a fake debt) or the same real invoice being financed on a different, non-Covenant platform.
- subTier is a compliance/eligibility proxy, not a credit score. It measures how thoroughly a party was verified, not payment history — and Covenant sets it at issuance, which is exactly what Cleanverse recommends integrators do.
- Jurisdiction is enforced by Cleanverse, not by Covenant's own field. The pool rule whitelists
US, so a wallet holding an active subTier-60 A-Pass issued against a Singapore passport is denied —isVerifiedreturns false on country alone.ReceivableNote.requiredJurisdictionis a separate, older mechanism reading the A-Passgroup, which Cleanverse leaves empty; notes are still originated withrequiredJurisdiction = 0because enforcing on a field nobody populates would be theatre. The enforcement that matters happens one layer up, in CVI's own rule engine. - The desk hands out A-Passes.
/api/onboardissues a visitor a real subTier-60 pass and mints them test cvaUSD, so the live desk is usable by anyone. That is a testnet convenience; a real deployment would have counterparties bring their own KYC'd pass. - Obligor payment at maturity is an off-chain trust assumption; default, recourse and credit insurance are out of scope.
- The on-chain note represents an off-chain assignment; production would need a legal wrapper / true-sale opinion alongside the on-chain confirmation.
Solidity 0.8.24, Foundry, OpenZeppelin 5. Next.js dashboard talking to window.ethereum directly, no wallet SDK. Node/TypeScript Cleanverse tooling on viem. Target chain: Monad testnet (chainId 10143).