Releases: dziuba0x/delicti
Release list
v0.16.0 — 2026-09-27 — watch pool v2 (the seal) and amendment v1.2 (Conatus)
Deployed on Coston2 on 2026-09-27 (docs/DEPLOYMENTS.md, deployments/coston2-v0.16.json).
- Addresses:
Vault0x76305Ef7…21b270,VaultSumma0x274e8aa1…184E54,SummaMeter0x39aa9b12…1FaB1D,MandateFacilitator0xa3C41FfD…238b6B. - Sources: verified on the explorer, a first for this project.
- SDK:
@delicti-protocol/sdk0.16.0. - v0.15: mandates bonded there keep their rules for ever.
Deployment and SDK
script/DeployV016.s.sol: the whole layer over the live core in one broadcast. It deploysJudgeEvm,JudgeXrpl,VaultandBondLens; thenJudgeSummaandVaultSumma, with the v0.15 price map row for row (read back from the liveJudgeSumma); thenSummaMeterv1.2,MandateFacilitatorv1.2 andSummaLens. The registry, anchor log, spend meter andAgentRefsare reused. It writesdeployments/<network>-v0.16.json.- Rehearsed on a Coston2 fork held at Coston2's fees (
lancea/scripts/coston2-fork-proxy.mjs): 19.97 M gas, 12.98 C2FLR at 650 gwei. - With forge's own fee guess the same deploy paid 1500 gwei, 29.96 C2FLR. The fees are now passed explicitly.
- Rehearsed on a Coston2 fork held at Coston2's fees (
- SDK, watch pool v2:
seal.ts:claimKeyOf,sealOf,sealClaims,payClaims,requestAttestations,KIND_CLAIM.Fdc.requesttakes{ vault, salt }(v0.16) or a bare Vault (v0.15, through the frozenabi-v015.ts).- Both watchers seal their requests before a challenge's own commitment, so one
commitLeadwait covers both. - A request someone else already holds goes straight to FdcHub: the proof is the same, the stipend is theirs.
- New feature flag:
sealedClaims. - 5 new offline tests, 33 in all.
- Run against the real v0.16 bytecode on the fork: paying before
commitLeadis refusedCommittedTooLate; after it,claimantOfis the watcher; a copier's own seal is refusedAlreadyClaimed; the holder re-sends without a new seal.
abi.tsregenerated from the v0.16 build.- SDK 0.16.0,
networks.ts:- the current deployment is v0.16, with
sealedClaims; - v0.15 moves into
history, so its mandates are still read and watched through the unsealed path; summais the v0.16 stack, and the newsummaHistorykeeps the v0.15 one.
- the current deployment is v0.16, with
Watch pool v2: the seal
Two holes in the watch pool (§8.4), both proven with proof-of-concept tests against 5fd2925 before any fix was written:
- MEDIUM: a mempool copier held the stipend. Flare orders transactions by priority gas auction. A copier who outbid a watcher's pending
requestAttestation(request)becamerequesterOf; the watcher's call still paid FdcHub, for nothing. - MEDIUM: a made-up MIC held the stipend, no mempool needed.
deedKeyleaves the MIC out, so a request no provider will ever attest, sent before the verifier has even indexed the deed, claimed the key of the valid request.
Fix: watch pool v2 (docs/v2/watch-pool.md). A stipend is paid to the first address that sealed the exact request a filed proof answers — commitmentFor(watcher, 0, KIND_CLAIM = 0, keccak256(request), salt), at least commitLead and at most COMMIT_TTL old — and paid for it through requestAttestation(request, salt). Judges rebuild the exact request from the proof: the FDC's MIC is keccak256(abi.encode(response with votingRound = 0, "Flare")), measured bit for bit on real EVMTransaction and BalanceDecreasingTransaction requests (test/FdcKey.t.sol). A second sealed payer of the same bytes is refused AlreadyClaimed and keeps its fee; the holder may re-send its own request. Vault.paysStipends lets judges skip the key computation where no pool is funded.
Vault:requestAttestation(request, salt),claimantOf,claimKeyOf,paysStipends,KIND_CLAIM, eventAttestationClaimed;requesterOfand the unsealedrequestAttestation(request)are gone.DelictiErrors.AlreadyClaimed.Deeds:requestEvm/requestBdt/requestPaymentandclaimKey*. All three judges key stipends by claim key (computed in a second pass, off the filing loops' stack).- Tests: 269 (was 261). New: the two PoCs as regressions, a different request for the same deed, the claimant's resend, the seal's clock, real-data claim-key pins, SUMMA stipends on both rails. The invariant handler seals every attestation it buys.
- Named for what it is: v2 of the watch pool, v1 for everything else. It narrows a rule §14 froze, so by §14 it is not an amendment. The migration is the one §14 names, new mandates bonded in a v0.16 Vault; the registry stays, because nothing it stores changes (docs/v2/watch-pool.md P.6).
Amendment v1.2: Conatus
docs/amendments/v1.2-conatus.md. The brake used to refuse and forget. MandateFacilitator.recordAttempt now records a refused authorisation when the agent signed it to the facilitator, it is still live at the token, and it breaks the umbrella's budget against the tally as it stood at validAfter, at 99 % of its value. Spend noted later does not count, so an honest authorisation that lost a race, or that a seller sat on, is not an attempt; a cancelled one is a withdrawn attempt and cannot be recorded. A recorded nonce never settles (Recorded). No new kind and no slashing: the consequence is a strike on the tripwire.
SummaMeter:setTripwire/rearm(principal),strike(any declared effector),tripped. A tripped umbrella makeswouldExceedanswer yes on every rail, so one recorded attempt on Flare stops the XRPL co-signer too, with no change to it.notekeeps recording. Effectors gain no new power: they could already stop an umbrella by noting spend.MandateFacilitator:recordAttempt,attemptedAt,attempts, eventAttempted;settlerefusesTrippedandRecorded. The EIP-3009 views it reads (DOMAIN_SEPARATOR,authorizationState, the receive typehash) were checked on USD₮0 on Flare mainnet.- Tests: 280 (11 new).
Also
- Batch (XLS-56), resolved on paper and in emulation: Flare's indexer stores every transaction the
ledgercall returns and MCC 4.5.0 derives spent amounts from metadata alone; on a real devnet Batch the inner transactions come back fromledgerwith their own ids and metadata, and the verifier's logic attests 2 000 000 and 3 000 000 drops to them and the 4-drop fee to the outer one (docs/research/batch-fdc-2026-09-26.md). SPEC §10 annotated. A live attestation waits for the testnet; mainnet activation is no earlier than 2026-10-09 (majority since 2026-09-25 14:46 UTC).
Contracts, addresses and live transactions: README · docs/DEPLOYMENTS.md. Testnet only, unaudited.
v0.15.0 — 2026-09-24 — v0.14, reviewed adversarially and fixed before it shipped
Before v0.14 left the sandbox, a fresh adversarial review was run against it: a separate agent with the diff, the sources, the SPEC and permission to write proof-of-concept tests. It found one high, two medium and one low issue, and confirmed three of them with working exploits. All are fixed here. v0.14 stays on Coston2 for mandate #13, is superseded, and was never pushed as a release on its own.
- HIGH: a copier could take every watch-pool stipend. Recordings below the budget need no commitment, and an FDC proof works for whoever submits it. A copier lifts a watcher's proofs from the mempool or the DA layer, files first, and the watcher's own filing reverts
NothingNew. Fix: the stipend now goes to whoever paid for the attestation.Vault.requestAttestation(request)forwards the fee to FdcHub and records the first payer under a key the judge rebuilds from the proof,keccak256(abi.encode(type, source, keccak256(abi.encode(requestBody)))).test/FdcKey.t.solpins the request-side and proof-side keys together on real verifier requests and DA proofs from 24.09. Copying a filing now only pays the gas to deliver someone else's stipends. Because the key is claimed on payment, a second watcher can readrequesterOfbefore buying the same attestation, which removes the attestation-fee race §10 used to list. - MEDIUM: the principal could drain the agent's watch funding. The agent funds, the principal raises
perDeedto the whole pool, and a sock puppet files one deed. Fix: only the principal funds, and only the principal is refunded. - MEDIUM/LOW: one act split into many paid deeds (an XRPL offer consumed in many fills). This is bounded by the terms, at most
perDeed / minValueper unit of value, and each piece costs its requester an attestation fee. It is documented in SPEC §8.4, with guidance on setting the terms. It is not a code change. - LOW: a crossing could spend its commitment and take nothing. This happened when a proportional increase was still under the 10 % floor already taken. Fix: docket judges ask
Vault.wouldTake(the verdict's own arithmetic, read-only) and record without spending the commitment when it is zero. That check also subsumes v0.14's high-water check. Verdicts are strict again. - LOW: the pool could close while deeds were still provable. Fix: it closes once no bond is left, or
WATCH_TAIL(14 days) after the cooling window. - Tests:
test/WatchPool.t.solwas rewritten (17 tests: copier, first payer, bought elsewhere, zero value, minimum, empty pool, crossing paid twice, funding, terms, both closing paths, and wouldTake keeping the commitment). The §6.8 stipend is tested inXrpl.t.sol, and FdcKey has 2 tests. The invariant handler buys attestations through the Vault (etched FdcHub stub) and runs a paid watcher; a canary confirms stipends are reached. Campaign: 15 × 1,500 × 200, 0 failures. 227 Solidity tests. - Deployed (production timers):
Vault0xB15f5041…9a24aF,JudgeEvm0x463042fb…4d42cFf2,JudgeXrpl0x9201272e…9d765940,BondLens0x960A0e68…89D3Bf3D. - Live, mandate #14 (XRPL): the watcher paid for four attestations through the Vault, and a copier filed the watcher's proofs first:
0xc7c8620e…. Stipends: watcher +0.15 C2FLR, copier +0.sdk/examples/copier-demo.tsreproduces it. The sentinel then convicted #14 with every attestation bought through the Vault (0xb9e2d89a…,bondOf1 → 0.5832). - SDK: watchers buy attestations through the Vault on v0.15+ (
paidRequestsfeature), andnetwork.historycarries v0.14. TheselfWatchedfacet is gone, because the agent can no longer fund.
Contracts, addresses and live transactions: README · docs/DEPLOYMENTS.md. Testnet only, unaudited.
v0.14.0 — 2026-09-24 — who watches, and why they would · SPEC v1.0 frozen
Security in DELICTI needs one honest party to bring a case. This release is about that party: who it is, what it is paid, and how it sees. docs/research/watchers.md compares seven earlier watcher layers (Lightning watchtowers, Forta, keeper networks, UMA/Kleros, rollup challengers, liquidation bots, contributor-split oracles) and records what each learned the hard way. Every decision below cites that note.
The watch pool (Vault, SPEC §8.4) — as first built; see v0.15 above for what an adversarial review changed
- A crossing bounty pays watchers nothing while the agent behaves: the watchtower deterrence paradox. Now the principal, and the agent if it wants to show confidence, can fund a pool, and the principal sets
perDeedandminValue. Every docket judge pays the filer of each new, value-moving deed from it, on recordings and crossings alike. - Only principal and agent can fund. An outsider's money in a pool whose rate the principal sets would be a prize for collusion (agent moves value to itself, sock puppet files, rate goes up). This was found in review before any deploy.
- Terms only improve for watchers once set. Zero-value deeds earn nothing:
transferFrom(agent, x, 0)succeeds for anyone on a standard token, and anyone can send XRP to the agent. That also corrected §6.11's soundness text, which had called everyTransferout of the agent "the agent's act". - Refund is pro rata once the mandate is dead past the cooling window, and the first refund closes the pool.
watchPoolis part of the Vault's balance invariant. - 12 unit tests (
test/WatchPool.t.sol); the invariant handler funds, sets terms and refunds;refundExceededFundingghost.
A docket that could not record (fixed in all three judges)
- Found by the new §6.8 invariant track. Once another path had convicted a mandate (the one-shot §6.8 challenge, or a receipted §6.3 case), a docket filing that went past the budget but not past that verdict's high-water mark reached
Vault.verdict, which refused itNothingNew. The docket then could not record at all until a single filing outran the mark, and on XRPL a deed not filed within ~14 days is lost for ever. - Now such a filing is a recording: no commitment, no reward. The crossing verdict is non-strict, so a crossing after the whole base is taken still records.
test_paymentDocketStillRecordsBelowAnotherPathsVerdictfails on the v0.13 judges and passes on v0.14.
The §6.8 path is fuzzed
- The last route to the Vault the campaign did not drive now runs on the same Vault as everything else: receipted XRP payments, kind-3 and kind-4 receipts anchored or not, the one-shot challenge and the payment docket interleaved. New invariant:
invariant_paymentDocketIsTheSumOfItsFiledPayments. - Campaign: 14 invariants × 1,500 runs × depth 200, with all eight kinds, three dockets, the surety rule and the watch pool on one Vault. 0 failures. 219 Solidity tests.
Deployed: v0.14 on Coston2 (production timers)
Vault 0x9bF9e418…72566fE, JudgeEvm 0x361730A0…d29a2C36, JudgeXrpl 0xE9E6eD9E…4a14E688, BondLens 0xA73f7403…5500BE1b. Core and AgentRefs unchanged.
SDK: the XRPL watcher, the sentinel, the public score
XrplHistory: reading an account's history the way the FDC will. Public XRPL nodes keep little history; the testnet endpoint reachable here keeps ~1,300 ledgers. But every change to an account's XRP modifies its AccountRoot, which records the previous transaction that did (PreviousTxnID). The history is a linked list. The watcher walks it backwards through the FDC verifier's own index (~15 days of full transactions with metadata). What it finds is exactly what is still provable, including offers taken in other accounts' transactions.XrplOutflowWatcher(§6.10). It finds the XRPL account behindagentReffrom theExclusiveProvenevent: statement tx id → signer from the verifier's index →keccak256(signer) == agentRef. It then records or commits and convicts.Sentinel+delicti-watch sentinel. Discovers every mandate from state, classifies it (§6.11 / §6.10 watchable, receipted, unwatchable), observes, prices each plan (attestation fees + gas against stipends +BondLens.penaltyFor), and acts per policy (observe,profit,altruist). It knows every past Vault a mandate may name (network.historywith per-version features).- Public score (SPEC §11.2), per agent and deliberately not one number. Facets: standing (
breach-unjudgedis the alarm), verdicts and value taken across every Vault, bond at stake, worst budget use, watched and self-watched mandates, and flags for unfiled and lost deeds. JSON plus a self-contained HTML report. DelictigainedproveXrplStatement,setWatchTerms,fundWatchandrefundWatch;statusshows both exclusivities and the watch pool.FdchandlesEVMTransaction,BalanceDecreasingTransactionandPayment, retries the XRP verifier's index lag, and reads fees the way the Vault does.- Vitest: 28 offline plus 1 online. The online one sends a real proof from #12 back to its v0.13 judge (#11 no longer has a bond to judge against: the sentinel took it).
Live (Coston2 + XRPL testnet), all by the sentinel
- Deeds the humans missed (#8, #9). Walking the AccountRoot chains, the sentinel found three outflows the 23.09 script never filed: a
TrustSetfee, anOfferCreatefee, and the exclusivity statement payment itself. It convicted both mandates again, nested, taking exactly the quoted difference (0x45cb946a…,0xcc2bc0ba…). - Two exclusive promises over one token (#11). #11 and #12 were declared exclusive by the same address over the same token with overlapping windows. The sentinel counted #12's settlements against #11 too, as SPEC §10 says it must, and took the rest of #11's bond (
0x79bd7b9f…). - The watch pool, end to end (#13, XRPL). The sentinel recorded three payments and was paid 0.15 C2FLR in stipends (
0x950d5acb…). After an offer taken by the counterparty and one more payment, it committed and convicted:bondOf1 → 0.5832 (0x7a83ce5f…), with 2 more stipends and the reward. It then claimed 0.2917 C2FLR. docs/DEPLOYMENTS.md has the full run and the sentinel's books.
SPEC v1.0 — frozen
§14 says what is bound (the mandate, the leaf, kinds 1–8 and the commitment encoding, the consequence rules, the docket semantics) and how it may change: numbered additive amendments only, v2 for anything else, nothing ever changed under an existing mandate. It also says what freezing is not: an audit. New sections: §8.4 (watch pool) and §11.2 (sentinel and score). §6.11's soundness text is corrected, and §10 is updated.
SDK and the §6.11 watcher (merged after v0.13.0, shipped in 0026)
sdk/—@delicti/sdkand thedelicti-watchbot. TypeScript on viem.Delicticovers commit, acknowledge/declareExclusive, post/postFor, withdraw, claim and status.Fdccovers prepare, request (with the fee), the voting-round clock, and DA polling with decoding.ExplorerLogSourceexists because Flare's RPC serves 30 blocks pereth_getLogs.planErc20is a pure planner: window, already-filed per log, ascending hashes, 50 logs per request, and record vs convict.Erc20OutflowWatcherruns one cycle: look, plan, and either record or commit → wait for the round pastcommitLead→ attest → file. ABIs are generated from the Foundry build.- Live, mandate #12: the watcher recorded three payments, then committed, waited and convicted on the next two by itself (
bondOf1 → 0.75). Its books are in docs/DEPLOYMENTS.md: 0.78 C2FLR spent, mostly testnet gas at 650 gwei, against 0.025 earned. A 1-C2FLR bond is not worth watching at testnet prices, which is the "small bonds are not watched" limit, now measured. - Tests (vitest, 17): the planner (11 cases); the commitment encoding pinned to a value the live Vault computed; a real FDC proof from mandate #11, decoded and, with
DELICTI_ONLINE=1, sent back to the live judge, which runsFdcVerificationand every check and answersNothingNew.
Contracts, addresses and live transactions: README · docs/DEPLOYMENTS.md. Testnet only, unaudited.
v0.13.0 — 2026-09-24 — stablecoins: the agent that signs and never sends
BlackRock's thesis of the week is that AI agents will drive demand for stablecoins and blockchain payments. The agent that thesis describes pays in USDC or USDT0 by x402. It signs an EIP-3009 authorisation, a facilitator sends it, and it writes no receipt. Until now DELICTI reached that agent only through a staked accusation, one deed at a time (§6.4).
§6.11 — gross ERC-20 outflow on a docket (JudgeEvm.fileErc20Outflow, kind 8)
- Applies to exclusive mandates whose asset is an ERC-20. Every live
Transfer(agent → anyone)of that token inside the window counts, whoever sent the transaction, burns included (FXRP redeemed to XRPL). Proven by FDCEVMTransactionwith events. No receipt, no meter. - Keyed per log, not per transaction. The FDC lets a requester list any subset of a transaction's logs, including none. A per-transaction docket could be buried: file a transaction with no logs listed and its outflow is counted at zero for ever.
eventFiled[mandate][tx][logIndex]closes that; a filing that shows no new log revertsNothingNew. - Below the budget a filing only records, needs no commitment and pays nothing. The crossing filing is committed and convicts. Nested in the budget bucket with §6.2, §6.3, §6.8 and §6.10.
- Reimbursement counts proofs, not logs. Found while writing the docs: the first draft passed the number of new logs to the Vault, which reimburses per attestation, so one proof carrying five logs would have paid the filer for five. Fixed before any deploy;
test_reimbursementCountsProofsNotLogs. - Confirmations:
minConfirmationsis 64 on Ethereum (a reorged block would convict an agent of an outflow that never happened) and 1 on Flare. - The FDC's EVM verifier has no lower timestamp limit on Flare and Songbird, so here the docket is not about the verifier's memory. It exists for the receipt-less conviction and the anti-burial rule.
- Executed on Coston2, mandate #11: five facilitator-sent x402 settlements, no receipts. Three filed uncommitted (docket 3), then the committed crossing (docket 5 > 4):
bondOf1 → 0.75. Crossing0x75d51613…, docs/DEPLOYMENTS.md. - Tests: 16 unit tests (
test/Erc20Outflow.t.sol), including a 512-run fuzz that splits and repeats the same logs across filings. The invariant handler now drives a third track on the same Vault: token moves with 1–3 logs each, filings listing all or some of them, and committed crossings. New invariant:invariant_tokenDocketIsTheSumOfItsFiledEvents. Campaign: 14 invariants × 1,500 runs × depth 200, 0 failures. 205 tests. - Stated in SPEC §10: the FDC indexes Ethereum, Flare and Songbird only. Base, where most x402 settles today, is outside every DELICTI challenge. So is RLUSD on XRPL.
Also deployed with v0.13 (merged after v0.12.0): kind-4 receipts, the §6.8 docket, the invariant tracks
- The invariant campaign now drives both judges on one Vault. Until now the fuzzer only called JudgeEvm, so every value invariant (the balance equals bonds + credits + unsettled remainders + open stakes; per mandate, posted = bonded + taken + withdrawn; verdicts never take more than the base) had only ever been checked on a Vault that one judge had touched. The handler now also opens exclusive outflow mandates, moves XRP in and out of their accounts (including slightly outside the window), and files runs of those moves on the §6.10 docket. It covers overlapping runs, uncommitted filings that must stay below the budget, and committed crossings, all interleaved with every EVM path on the same Vault. New invariant
invariant_docketIsTheSumOfWhatItFiled: after any sequence, the docket equals a from-scratch recount of the positive outflow over the transactions it has filed; no filing adds anything other than its new outflow; nothing outside the window is ever filed. A canary run confirmed that XRPL verdicts are reached within the first 10 runs. Campaign: 13 invariants × 1,500 runs × depth 200 (300,000 calls each), 0 failures. 188 tests. - §6.8 on a docket:
JudgeXrpl.fileBudgetPaymentsworks like §6.10's docket (record below the budget without commitment, convict on the committed crossing, skip already-filed payments, one receipt per payment across filings). The verifier's ~14-day memory no longer bounds a receipted XRPL case. Five tests. - XLS-56 Batch, measured on devnet: the XRP leaves in the inner transactions (own hashes, fee 0, unsigned,
ParentBatchID), not in the outer one. Whether the FDC verifier attests an inner transaction is unknown until the testnet enablesBatchV1_1. This is a possible §6.10 blind spot, stated in SPEC §10.tools/xrpl_testnet.py batchis the probe to rerun. - Mutation testing (mewt, Trail of Bits) on the v0.12 code: in the first 24 mutants, 23 were caught. The one that survived was the removal of the receipt-kind check on the XRPL payment path, a test gap rather than a code bug.
test_revert_receiptOfAnotherKindnow kills it, confirmed by re-applying the mutant. testFuzz_docketIsTheSumOverDistinctTransactions: however filings are ordered, batched and repeated, the docket equals the positive outflow over distinct transactions (512 runs).- Kind-4 receipts: an XRPL payment named by its transaction id (SPEC §4, §6.8). x402 on XRPL binds payments with
InvoiceID, which no FDC type returns, so memo-referenced receipts could never match a real x402-XRPL settlement.JudgeXrpl.challengeBudgetOverrunPaymentnow also accepts kind 4, matched onrequestBody.transactionId. Three tests. 180 tests. test/halmos/VaultMath.t.sol: bounded symbolic checks of the Vault's arithmetic (penalty bounded and monotone; the surety split conserves value). The conservation check does not finish at 600 s over 64-bit inputs; it is recorded, not claimed.- Tooling trap, found by mutation testing: the npm distribution of
forge(@foundry-rs/forge, a Node shim) exits 0 when tests fail, setUp failures included. The native binary exits 1. Every result in this repo's history was read from the output, not the exit code. Scripts now judge a run by its summary line (scripts/lib/green.sh).
Contracts, addresses and live transactions: README · docs/DEPLOYMENTS.md. Testnet only, unaudited.
v0.12.0 — the surety rule and the docket
Two economic holes closed by changing who is owed what, not by adding checks. Both ran live on Coston2.
The surety rule: a deposit compensates whoever its depositor names (SPEC §8.3). Before this release, the remainder of every verdict went to the principal, no matter who posted the collateral. A principal colluding with its own agent could stage an overrun and walk off with an insurer's deposit. Now a third party's plain post names itself as beneficiary, and postFor can name anyone. What colluders can still take from an outsider is that deposit's share of the challenger's reward: 3 of 30 in the test case, down from 30. Live, mandate #9: after settle, the insurer is owed its half of the remainder (0x639f368d…).
The docket: outflow cases outlive the verifier's memory (SPEC §6.10). The FDC XRP verifier only remembers about 14 days. Each proven transaction is now filed once. Below the budget a filing only records, with no commitment and no verdict. The filing that crosses the budget is the committed conviction. A copier who front-runs part of an honest filing cannot kill that filing or steal its reward. Live: two deeds filed early, then a crossing over only the two new ones.
177 tests. Invariant campaign: 12/12 × 1500 runs × depth 200 = 300,000 calls, clean. Stated and not solved: filing below the budget is unpaid.
v0.11.0 — the Vault and its judges; an agent convicted for a transaction it never signed
Live on Coston2 (2026-09-23). An agent's XRPL account declared that everything leaving it belongs to its mandate: at most 12 XRP of outflow, fees included. It paid 3 + 3 + 3 XRP and left an offer selling 5 XRP in the book, which the counterparty consumed with its own OfferCreate. Four FDC BalanceDecreasingTransaction proofs later, including one for the transaction the agent never signed, the bond was slashed by a sixth: 0xa0c2ab11…. No receipt was written at any point.
- Vault + judges (SPEC §8.2). The collateral, the books and the commit–reveal gate live in one
Vault.JudgeEvmandJudgeXrplhold no funds and are fixed at construction: no admin, no setter, no upgrade. All 146 tests from v0.10 pass with unchanged assertions. The 242-byte EIP-170 ceiling is gone. - §6.10 gross XRP outflow. The measure is chosen per mandate (
assetKey = "XRP/outflow"). Exclusivity is declared by the XRPL key (AgentRefs.proveExclusive). It covers payments, offers, escrow, AMM and fees. Issued currencies are not covered. - 169 tests. The invariant campaign ran 1500×200 = 300,000 calls, clean.
- Stated and not solved (SPEC §10): the FDC verifier remembers about 14 days, and principal–agent collusion against a third-party depositor.
v0.10.0 — 2026-09-22 — the first deeds judged on XRPL, and the tally judged at the time of the deed
The first deeds judged on XRPL. An XRPL account confirmed its own mandate, then paid 1 XRP five times under a 4-XRP budget. Five FDC Payment proofs later, a 25 % overrun took 25 % of the bond — 0xb6856090…62bcb89.
- SPEC §6.9's condition is met: FDC
BalanceDecreasingTransactionattested an XRPL deed that is not a payment — and not even the agent's own transaction (a counterparty took its resting offer).FdcVerification→true. - Audit of v0.9: two openings, both fixed with regression tests — an effector could erase the under-reporting case against itself after it became public (HIGH), and one deed could be counted many times in an agent's record (MEDIUM).
- 146 tests, 12 invariants; a 300,000-call campaign clean on the final v0.9 code.
Testnet only, not independently audited.
An adversarial pass over v0.9 (Slither at medium+, a 300,000-call invariant campaign on the final code, and a read of every path against the list of attack classes this repo has already suffered). Slither: the same three false-positive classes as v0.9, nothing new. The campaign: clean. The read found two openings, both reachable from outside the contracts, neither a Solidity bug — one a binding missing in time, one missing in scope. Both have a regression test in test/Audit.t.sol that fails against v0.9.
Executed on Coston2 (2026-09-22)
Deployed (addresses in the README and docs/DEPLOYMENTS.md). The XRPL path is no longer tests-only. scripts/xrpl-structuring.sh (new), mandate #6: the agent's XRPL account confirmed the mandate with a payment carrying AgentRefs.challengeFor(6) as its one 32-byte memo → AgentRefs.prove 0xa5c17d21…63cd8f; five payments of 1 XRP under a 4-XRP budget, five anchored kind-3 receipts, five FDC Payment proofs → reveal 0xb6856090222fb3083d425bb22126f88fd35e66f14641a8b03c2cd2c4862bcb89 (514,785 gas): bondOf 1 → 0.75. Fees (10 drops a payment) were not summed, as §6.8 says.
tools/xrpl_testnet.py (new) does the XRPL side — faucet accounts, payments with exactly one 32-byte memo, which is the only shape for which Payment reports a reference. The run is resumable: state goes to .run/ after every costly step, and RESUME=1 re-commits over the same deeds if the commitment has aged past COMMIT_TTL.
It needed that. The first runs sent the verifier transaction ids as 0x0x…, the refusals were read as indexer lag, and the first commitment aged out; the deeds, leaves and mandate were still good, so only the commitment was redone. Recorded here because the script's own comments had briefly blamed the verifier.
SPEC §6.9's condition is met, and the blind spot it named does not exist. A BalanceDecreasingTransaction proof for an OfferCreate — and not the agent's own: another account took the agent's resting offer — was requested (0xc5a32b7b…067b47, round 1461000) and verified by FdcVerification (true, spentAmount 9,000,000 drops). SPEC v0.7 rewrites §6.9 around what that means: a challenge over every XRP outflow of an exclusive XRPL account, with no receipt required, is v0.11.
Not executed live on this deployment: the §6.5 historical-tally fix and the per-agent corroboration key (both covered by test/Audit.t.sol), CorroborationLog in general, a second incremental verdict, pro-rata withdrawal after a slash.
The effector could destroy the case against itself, after seeing it (HIGH)
challengeUnderReportedSpend convicted on proven > meter.spent(mandateId), read at the moment of the reveal. But note() belongs to the effector, stays open while the mandate lives, and takes any amount — and the challenger is forced to announce the case first: FdcHub.requestAttestation carries the deeds' transaction hashes in the clear, and the reveal cannot land before that round finalises and commitLead (10 min) has elapsed (§6.7). So an effector colluding with the agent had ~13 minutes to note exactly what it had hidden, and the challenge then reverted TallyAgrees against a tally that had been true for about a minute. Commit–reveal does not help here: it protects who owns a reward, not whether a case exists at all. One transaction, and §6.5 was gone.
SpendMeternow keeps the tally as checkpoints(timestamp, total)— one per second, since several settlements in one block are one moment — and answersspentAt(mandateId, ts)by binary search, zero before the first note.Bond.meterGrace(immutable, 5 minutes in production) and the verdict readsspentAt(lastDeed + meterGrace),lastDeedbeing the newest deed among the supplied proofs. What the tally said when it should have said it is a fact; nothing written later can change it.- Why a grace at all, and why this one: the meter is written in the same payment path that reads it, seconds around the deed, so five minutes is two orders of magnitude of slack for an honest effector — and it is far below the earliest moment any challenger can reveal, which is the number that closes the race. Zero would convict an effector for a slow block. SPEC §10 states what the window still allows.
- Regressions:
test_effectorCannotCatchUpTheTallyOnceTheCaseIsPublic,test_effectorMayBeLateWithinTheGrace(the limit, stated),test_controlUntouchedTallyStillConvicts, plus two on the history itself.
One deed could be counted many times in an agent's public record (MEDIUM)
CorroborationLog keyed corroborated and deedRecorded per mandate, which is right for a mandate's own episode — but countOfAgent crosses mandates. Anyone may commit a mandate naming any agent, an agent may acknowledge its own, and the same FDC proof verifies under every one of them. So one real transaction could be entered into an agent's record as many times as someone was willing to pay gas for: a second mandate over the same window and source, one anchor, one call. §10 admitted this log cannot tell a deed from a wash — but a wash costs a transaction on the source chain, and this cost none. It matters because the next floor is a public score.
deedRecordedForAgent[agent][deedId], checked alongside the per-mandate keys. Each deed enters an agent's record once, whatever mandate it is claimed under.- Regression:
test_oneDeedIsCountedOncePerAgentAcrossMandates.
Checked and sound
_penalty arithmetic and its bounds; severity accumulation across kinds and buckets; pro-rata withdraw (rounding favours the contract, the last depositor out takes the rest); post refusing unacknowledged mandates, unproven agentRefs, foreign Bonds and slashed mandates; the commitment gate's three clauses and ClockDrift; fdcCost failing to zero rather than reverting on resolveAccusation's path; Deeds as the single definition of agreement. The one thing a depositor can still do is withdraw its share before a later verdict lands during the cooling window, which is inherent in a first-come withdrawal and is bounded by the window itself.
Slither after the fixes: the same false-positive classes plus two more of the same kind — uninitialized-state on SpendMeter._history (a mapping of arrays is written by push, which the detector does not follow) and incorrect-equality on h[n-1].at == block.timestamp (the deliberate "same second, same moment" collapse).
146 tests (was 140), 12 of them invariants.
v0.9.0 — 2026-09-19 — what the budget is made of, and who agreed to it
The mandate says what its budget is made of, the agent has to say yes, and a 25 % overrun costs 25 % of the bond — not all of it.
- Mandates pin
sourceId,assetKey,agentRefand their ownbond;acknowledge()is required before any collateral is accepted. The registry has no deployer and no admin key. - Proportional, cumulative slashing —
clamp(bond × overrun / budget, 10 %, bond); the challenger is reimbursed its FDC fees (read on-chain), then 10 %; depositors are repaid pro rata. - XRPL:
challengeBudgetOverrunPaymenton FDCPaymentproofs andAgentRefs.prove— tests only, not yet executed on-chain. CorroborationLog,Deeds,Verdict/DeedJudgedevents,BondLens— the data surface a public score needs.- 140 tests, 12 invariants; 300,000-call campaign; two accusation holes found and closed; Slither pass.
- Live on Coston2:
0x7c4c3094…8d8123→bondOf1 → 0.75. Copier refused:0xe736229a…e2ef27.
Testnet only, not audited. Full reasoning below.
Written for the three floors that are meant to stand on this one — a public score, a risk market, credentials issued on XRPL — and for the rule that none of them may require redeploying the core.
The mandate now says what its budget is made of
SPEC §10 admitted that the ERC-20 asset and the sourceId lived only in the off-chain envelope. That made a bond posted by anyone other than the principal worth less than it looked: an insurer could read how much a mandate allowed and not of what, and on the ERC-20 paths the challenger chose the asset in calldata.
Mandate.sourceId,Mandate.assetKey,Mandate.agentRef— the FDC source the deeds happen on, whatbudgetcounts (0= that source's native asset; on an EVM source, the ERC-20 address left-padded), and the agent's identity on a non-EVM source (FDC standard address hash). Passed tocommitas oneTermsstruct, appended to the end ofMandateso a reader compiled against the old nine-field tuple — flario's mandate gate — keeps decoding the prefix it knows.- Every challenge reads them from the mandate.
challengeBudgetOverrunERC20andchallengeUnderReportedSpendlost theirassetparameter; the native paths refuse a token mandate and the token path refuses a native one (WrongAsset), before any proof is verified. Every path compares the proof'ssourceIdwith the mandate's (WrongSource). - That last check closed a hole nobody had written down.
challengeUnderReportedSpendhas no leaves, so it had nothing to borrow asourceIdfrom, and it checked none. One key is one address on every EVM chain the FDC attests: a transfer by the agent on Sepolia would have been summed against a Coston2 tally that never promised to cover it.accuseUnanchoredDeedhad the same gap — exclusivity is a promise about one address on one chain. - Monotonic narrowing covers the unit. A child must keep its parent's
sourceIdandassetKey(ChangesParentAsset). Narrowing attenuates a quantity; a child in another asset is not a smaller share of the parent's budget, it is a different budget, andbudget <= parent.budgetwould be comparing drops with wei.agentRefandbondare the child's own: they describe the child, not the unit.
The agent has to say yes (acknowledge)
Not on the list for this release, and the reason pinning the asset was not enough. A principal writes the agent's address unilaterally, and principals may anchor. So a principal could name a stranger's busy address as "agent", anchor receipts for its ordinary transfers, and collect whatever a third party had posted as bond — and, once a public score exists, poison that stranger's contradiction rate for the price of gas. MandateRegistry.acknowledge(id) is callable only by the agent and is sticky; declareExclusive implies it; Bond.post refuses collateral for a mandate its agent has not acknowledged (NotAcknowledged). A score should ignore unacknowledged mandates for the same reason.
The registry has no deployer (Terms.bond)
Also not on the list, and the precondition for "no redeploy of the core". Until v0.8 the registry had one set-once bond, chosen by whoever deployed it. Every new challenge type therefore needed a new Bond, a new Bond needed a new registry, and a new registry orphaned every mandate ever committed — this release would have been the sixth time. Each mandate now names its own consequence contract. That contract can revoke that mandate and nothing else, which is a power its principal already holds, so nothing is delegated that was not already there. setBond, the global bond and the deployer are gone: the registry has no privileged key at all. Bond.post refuses a mandate that names a different Bond (NotThisBond), because collateral there could never be slashed — revokeByBond would revert every time — and would only look like a bond to a counterparty reading the chain.
Invariants and fuzzing — and what they found
The repo had 82 unit tests and no property-based ones. A unit test says "this attack, which we thought of, fails"; every hole this project has had was an ordering nobody had thought of. test/invariant/ drives the four contracts through a handler that can do everything a participant can — commit, delegate (deliberately unclamped, so most attempts to widen must be refused), acknowledge, post, act, anchor or stay silent, commit to a challenge, replay someone else's commitment bytes, reveal early, reveal on time, accuse, answer, resolve, revoke, withdraw, claim, wait — against an FDC that verifies everything, because the handler only builds proofs of deeds it really simulated. After every call:
- the Bond's balance equals bonds + credits + open accusation stakes, to the wei, and nothing ever leaves that did not come in;
claim()pays exactlyowed, zeroes it, and cannot be repeated on the same balance;- a live commitment keeps the timestamp of its first submission whoever replays it, and a spent one is gone;
- a mandate is never slashed for more than the bond it was slashed from; a slashed mandate is dead; a mandate dead for good never comes back and never anchors;
- a live child has a live parent all the way up, dies no later than its parent, and is never wider in budget, window or unit;
- every accusation whose window has closed can be closed by someone; an open accusation always has collateral behind it.
test_handlerReachesEveryDeepState exists because a handler is code and can be wrong in the boring direction — every call refused, every invariant vacuously true. The first version of this one was: a getter in an argument list ate the vm.prank and no accusation ever landed (the same pitfall claude/22 lists). test/Fuzz.t.sol adds two stateless if-and-only-if properties: a child mandate is accepted iff it only narrows, and the commit gate opens iff at + lead <= roundStart <= at + TTL and roundStart <= now. Default campaign 256 × 80 (~25 s, runs in CI); 1500 × 200 = 300,000 calls run clean before this commit.
Found by the campaign: an accuser's stake could be stranded for ever. resolveAccusation reverted AlreadySlashed when the mandate had been slashed by another path while the response window was open — two watchers accusing two silent deeds is enough. answerAccusation needs a leaf that by hypothesis does not exist, so nothing could ever close the second accusation and its 0.1 FLR stayed in the contract. Shrunk by the fuzzer to: accuse, under-reported-spend slash, wait, resolve → 0x37233762. Resolution now always closes, always returns the stake, and slashes only if there is still something to slash. Regression: test_secondAccusersStakeIsNotStrandedByTheFirstSlash.
Found while fixing it, not by the fuzzer: collateral could walk out from under an open accusation. The response window (24 h) is as long as the cooling window (24 h). An accusation filed in the last hour of the cooling window was still open when withdraw became legal; the principal withdrew, resolution reverted NothingToSlash, the agent walked, and the stake was stranded on top. withdraw now refuses while openAccusations[mandateId] != 0. The random campaign did not reach this ordering on its own, which is worth saying: the handler was taught it (lateAccusationThenWithdraw), and with the fix removed the invariant now fails within one default run. Regression: test_revert_withdrawWhileAnAccusationIsOpen. What this costs is stated in SPEC §10: an accuser can hold a bond for one response window per real, unanchored deed, at 0.1 FLR and one attestation each.
112 tests, 11 of them invariants.
Deeds on XRPL can be summed (challengeBudgetOverrunPayment, proveAgentRef)
Every cumulative challenge needed EVMTransaction proofs and compared sourceAddress with an EVM address. The only XRPL challenge was the negative one — a payment that did not happen. A deed actually done on XRPL could not be counted against a budget at all, which disqualified DELICTI as infrastructure for the ledger it is supposed to serve.
challengeBudgetOverrunPayment— positive FDCPaymentattestations against kind-3 leaves: the payment exists, succeeded, came from the account the mandate names (agentRef, standard address hash), inside the window, to the destination, for the amount and with the reference the receipt claims; the sum convicts. Commitment kind 6.receivedAmountis summed, notspentAmount. On XRPL the latter includes the fee. Counting it would let an agent be convicted by twelve drops while delivering exactly its budget; the EVM paths ignore gas for the same reason.- Leaves must be pairwise distinct, a check the EVM paths do not need: there the leaf's `ref...
v0.8.0 — 2026-09-14 — the reward belongs to whoever looked
Commit–reveal on every challenge: the 10 % belongs to whoever found the violation, not to whoever copied the calldata. A copier was refused on-chain (0xab529327…4da115, CommittedTooLate) and the honest challenger revealed two blocks later (0xdbf70a53…9a7628).
Every challenge so far paid its 10 % to whoever landed the transaction. That is not the same as paying whoever found the violation, and the difference is not academic: a challenge cannot be assembled in secret, because FdcHub.requestAttestation is an on-chain call carrying the deed's transaction hash or payment reference in the clear, minutes ahead of the reveal. A parasite watching FdcHub therefore learns of every case before it can be filed, copies the finished calldata out of the mempool and outbids the gas — paying for no monitoring and no analysis. The honest watcher pays for both. The equilibrium number of real watchers is zero, and a consequence layer nobody watches is theatre.
Bond.commitChallenge(bytes32)— a bare hash, leaking nothing:keccak256(abi.encode(challenger, mandateId, kind, deedsDigest, salt)). All five challenge entry points and the §6.4 accusation take asaltand spend that commitment once.resolveAccusationstays open to anyone, because the reward follows the accuser, not the caller.- The rule is about rounds, not about the request. The commitment must predate the start of the FDC voting round that produced the evidence — the lowest round among the supplied proofs, so that one freshly requested proof cannot launder a commitment made after the rest of the case was public.
commitLead(10 min, immutable). The obvious rule — commitment older than the round — looks sufficient and is not: a parasite that sees the victim's request land in round R commits inside R, requests its own attestation in R+1, and reveals against that, honestly. It then wins whenever the honest challenger's proof is the slow one, and measured Coston2 DA latency spans ~100–500 s, so that race is real. A lead wider than the latency spread makes the defence deterministic instead of a coin flip.COMMIT_TTL(1 h, constant). From the adversarial audit, and the more important half. Without an upper bound a commitment is a free permanent option: on three of the five paths the deed set is public and guessable — one transaction hash, one published leaf, or the canonical "every deed so far, ascending" — so anyone could pre-commit to cases nobody has found yet at oneSSTOREeach and copy a reveal months later. The TTL turns that option into rent. Constant rather than a constructor argument, because a deployer could otherwise set it just abovecommitLeadand make honest challenges against its own agents impossible to time.- A voting round cannot have begun in the future. Also from the audit.
roundStartTsmultiplies a round number that may be years old by the epoch length Flare reports now; if Flare lengthens the epoch or rebasesfirstVotingRoundStartTs, that product lands in the future and every commitment clears the lead test — the gate would stop existing, silently, with nothing reverting to say so. One comparison, fail-closed. - Replaying a commitment is a no-op.
commitChallengekeeps the earliest submission. Commitments are public calldata, so a refreshable timestamp would let a parasite grief a challenge it could not steal by replaying the victim's own bytes just before the reveal. - The voting-round clock is read live, via
ContractRegistry.getProtocolsV2()—firstVotingRoundStartTs/votingEpochDurationSecondsare declared onProtocolsV2Interface, not onIFlareSystemsManager. Never hardcoded. A constructor override exists for unit tests, the same shape as_fdcOverride; the live resolution is asserted against Coston2 on a fork. answerAccusationnow pinsleaf.kind, the one place a leaf was read without it. No exploit followed from the gap — the agent controls its own leaves either way — but an invariant present in one path and absent in its twin is how this repo has grown holes before.- Scripts:
scripts/lib/commit.sh, sourced by all six challenge scripts, which now run deeds → anchors → commit → wait outcommitLead→ request attestations → reveal. The digest and the commitment preimage are computed by the contract's own pure helpers (deedsDigest,commitmentFor) rather than re-encoded in shell, and the salt comes fromopenssl rand, not$RANDOM.scripts/structuring.sh SNIPE=1additionally submits the copied challenge with a late commitment and leaves the refusal on-chain as a reverted transaction — that refusal is the release. - SPEC v0.5: new §6.7 with the rule and the reasoning for each clause; §10 rewritten to state the two limits this does not close — a squatter paying rent forever can still hold a live commitment (the defence is economic, not cryptographic), and the agent, having the earliest knowledge of its own violation, can self-slash ahead of a real watcher.
- 81 tests (was 56). Slither at medium+: the same two false-positive classes as v0.7, nothing new.
- Executed on Coston2. Honest reveal
0xdbf70a53…9a7628(358,026 gas) → slashed; the copier's identical calldata with a commitment made 185 s after the evidence round opened,0xab529327…4da115, reverted with0xc9e9ac51=CommittedTooLate()after burning 271,152 gas on proof verification. Deployment and the full reading are in the README.
v0.7.0 — 2026-09-14 — the fast half
SpendMeter — structuring refused while it is still happening, plus the under-reported-spend challenge and the accusation loop (unanchored deed) executed on Coston2 in both directions.
Everything in DELICTI so far was evidence after the fact. An FDC attestation takes ~90 s per voting round and minutes end to end, so a structuring attack succeeded and was only punished later; the §7 brake could not see it, because it judges one call at a time and every slice is inside its own limit. The gap was never patience — it was arithmetic. Only a cumulative total can refuse the next slice.
SpendMeter.sol— the running tally an effector keeps against a mandate's budget.wouldExceed()/headroom()areeth_calls read before the deed, so the payment path gains no attestation latency;note()is oneSSTOREafter it. The principal declares which effectors may write (declareEffector), and a dead mandate cannot accrue spend. The meter deliberately records past the budget — refusing to record an overrun is a way of lying about it — and deliberately never slashes on its own: it is one witness (§5).Bond.challengeUnderReportedSpend— an effector can defeat the meter by not writing, and this is what makes that expensive. FDC proofs summing to more than the tally admits convict, with no anchored leaves required: the meter is witness 1 over the sequence, the proofs are witness 2 over the same sequence. Runs only on mandates that are both metered and exclusive (§6.4), because without exclusivity an outflow from the agent may be none of this mandate's business.scripts/spend-meter.sh—MODE=brake(four slices fit, the fifth is refused by oneeth_call, no FDC involved) andMODE=underreport(two of five settlements recorded, FDC proves five, slash).- SPEC v0.4: §7.1 (the meter), §6.5 (under-reported spend), and §10 updated to say plainly that real-time prevention now exists but only where an effector keeps the tally.
- 56 tests (was 42). Slither at medium+: three findings, all false positives.
Live on Coston2 (2026-09-14). Deployment: MandateRegistry 0x3b53a646E5450F4b525e30F2aA59be95AF77657b, AnchorLog 0xd5EECFAFE96fE9eec7E126F4B318DB139c9396bf, SpendMeter 0xD64465B95A1E83DC292AcB1e5b66dD416ADeA55C, Bond 0x3557Ae63bC3868165E605685b45506116E2CE1e6.
- Structuring refused in real time (mandate #1): four slices fit, the fifth rejected by
wouldExceed— no FDC round, no transaction, bond untouched. challengeUnderReportedSpend(mandate #2): tally said 0.02, the FDC proved 0.05 → slash0x53664b9f186353821ae8be87e75279d0de6619fd3326f3751974a616fb0b20ff(280,836 gas).- The §6.4 accusation loop, finally demonstrated in both directions on the same deployment: silence → slash
0x432ca347481f7a279e377b648c5ad2b776f871b38ed0068b515383d63e72490b(118,872 gas, mandate #5); answered in time → dismissed, accuser's stake forfeited,0x1bbcaf3f0a371facd17b8922d522e59917120f0055a2cca6954c3d38ba6ae48b(76,075 gas, mandate #4).