Update this file on every contribution that starts/completes/blocks an item below.
Status: Phase E1 done. Phase E2 done and live on testnet. Phase E3's create_escrow/get_status/release/refund wrapping done; SDK distribution decided (issue #5 closed — GitHub Release tarball, ADR 0003). Phase E4 (security review, issue #6) in progress: threat model drafted (docs/threat-model.md) and the timeout-refund item closed (issue #11 — ADR 0004 + implementation, live on testnet); independent review not yet done — blocking for any mainnet use. Phase E5 (contract features) now includes the OZ-library evaluation (ADR 0006, no library adopted) and the optional in-contract M-of-N releaser quorum (ADR 0007, SDK 0.5.0) — implemented, tested and pending a testnet redeploy. Phase E6 (npm publishing) in progress: automated sdk-v* release workflow with provenance, changesets, and a superseding ADR landed; the one-time human npm org/trusted-publishing setup is still pending, so sdk-v0.3.0 is not yet on npm. Fee estimation and fee-bump sponsorship helpers (issue #70) are implemented in the SDK, 0.5.0. Phase E7 (verification/release engineering) started: committed Soroban test snapshots with a CI drift check (issue #120) and a CI check that generated SDK bindings match the contract WASM (issue #121).
An opt-in creation method can require the payee to authorize accept before release. Existing escrows keep the default behavior, and refunds remain available without acceptance. Acceptance is recorded in the escrow data.
- Soroban project scaffold (
contracts/escrow), builds to wasm (cargo build --target wasm32v1-none --release) — 4 passing tests incontracts/escrow/tests/create_escrow.rs. - Deploy to testnet as a no-op — done 2026-09-14. Contract
CABSYY5FZGCCZ3UTBQGC7S357D2FUFLUQUUGJCCFHKGEJZVIFK4SIS2Z, verified live (not just deployed):get_statuson an unknown id correctly errors, and a realcreate_escrowcall locked 0.5 testnet XLM and reported"Funded"back. Full record, including the transaction links:docs/testnet-deployments.md. Closes issue #1. -
create_escrow— locks funds (calls the SEP-41 token contract'stransfer), assigns a sequentialescrow_id, stores payer/payee/amount/ opaquecondition_ref, statusFunded. Rejects non-positive amounts. Proven on testnet, not just in local tests — see above. -
get_status— read-only status query, errors on unknown id. Proven on testnet, not just in local tests — see above.
-
release— restricted to the escrow'sreleaser(set atcreate_escrowtime, decision recorded indocs/adr/0002— a per-escrow address, not a single contract-level admin), moves funds to payee, only whileFunded. Closes issue #2. -
refund— same auth/state rule, returns funds to payer. Closes issue #3. - Full test suite in
contracts/escrow/tests/release_refund.rs: 7 tests — release happy path, release unauthorized, double-release, refund happy path, refund unauthorized, refund-after-release, release-after-refund. 11/11 tests passing repo-wide (4 existing + 7 new); wasm release build still succeeds. - Redeployed to testnet with the new (Phase E2)
create_escrowsignature — done 2026-09-15. ContractCAXESEEAJILFFCN3PH2HHNMWAAHLEB64UTJHZ6IP7C5BBICT3S3VODSM, supersedes the Phase E1-only deployment (kept as history, not edited). Verified live: a realrelease(funds moved to payee), a realrefund(funds returned to payer), and a real unauthorized-caller rejection (Error(Contract, #3)) — not just the local test suite. Full record:docs/testnet-deployments.md.
-
@docforum/escrow-sdkwrappingcreate_escrow/get_status/release/refund. Closes issue #4. Originally scoped to wrap onlycreate_escrow/get_status(release/refunddidn't exist yet when #4 was written) — since Phase E2 landed in the same work session, wrapping all four now avoided shipping an SDK immediately stale relative to the contract it wraps.sdk/src/generated/contract-client.ts— machine-generated viastellar contract bindings typescript --wasm ...against the local wasm build (not the live deployment, so it isn't stale relative to source). Never hand-edit; regenerate on contract signature changes.sdk/src/index.ts— the actual public surface: anEscrowClientclass accepting aKeypairdirectly as a signer (converted internally by@stellar/stellar-sdk'sKeypairSigner) — no manual XDR-signing plumbing exposed to consumers (e.g.docforum-core'spaymentsmodule).
- Decided npm vs. GitHub Packages — closes issue #5. Interim:
consumed via a tarball attached to a tagged GitHub Release
(
sdk-v0.1.0), not GitHub Packages — that registry requires auth even for public packages, which would breaknpm cifor any external Wave contributor's fork. (A git-subdirectory dependency was tried first as a zero-infrastructure option but doesn't actually work against this npm version — see ADR revision note.) Real npm registry publish is the long-term goal once publishing credentials exist (an out-of-band human step, same category as the Drips Wave application itself). Verified end-to-end: installed and imported successfully fromdocforum-core/backend. Seedocs/adr/0003. - Integration test against testnet from the SDK itself, not just the
contract's own test suite.
sdk/tests/testnet-integration.test.ts— 3 tests, all passing against the live Phase E2 deployment: full release lifecycle, full refund lifecycle, unauthorized-caller rejection. Deliberately not wired intonpm test/CI (npm run test:integrationonly) — depends on testnet + Friendbot funding liveness, a different reliability profile than this org's other "integration" tests (e.g.docforum-core's self-contained embedded- Postgres booking test). CI runstypecheck+buildonly for the SDK.
- External or community review before any non-testnet deployment. Tracked as issue #6.
- Documented threat model in
docs/. Part of issue #6. In progress: draft indocs/threat-model.md(self-written by the implementer, so it does not count as the review). Stays unchecked until an independent reviewer has gone through it. - Timeout refund path when the releaser key is lost — issue #11.
Decided in
docs/adr/0004(optionalrefund_afterset at creation, after which anyone may refund, still only topayer;releaseis never gated by it; alternatives compared and payee implications for T8 recorded), implemented incontracts/escrow/src/lib.rswith 17 tests incontracts/escrow/tests/timeout_refund.rscovering every edge case the issue lists. Breakingcreate_escrowchange → SDK bindings regenerated,@docforum/escrow-sdk0.3.0 (migration notes in ADR 0004), redeployed to testnet and verified live (docs/testnet-deployments.md, 2026-10-08 entry). Closes issue #11. The threat model's §6 checklist item for this is checked; the operational half of T2 (key backup / multisig releaser) remains open. - Balance-delta check on
create_escrow+ malicious mock-token tests (issue #12). The contract's token balance must rise by exactlyamountacross the funding transfer or the escrow is rejected withBalanceMismatch(error code 6) — a lying transfer, fee-on-transfer, or rebasing token would otherwise createFundedescrows backed by nothing (threat model T5). Three test-only mock tokens intests/malicious_token.rs: lying transfer, short delivery (debits 300, delivers 270), and one that re-entersreleasefrom insidetransfer(T4). The re-entry test pinned the actual host behavior: Soroban rejects re-entry as an unrecoverable context error ("Contract re-entry is not allowed"), aborting the whole invocation — fail-closed. Post-creation clawback remains possible by design and is documented in T5 as accepted residual risk. Deployed and verified with a real fee-on-transfer rejection on testnet; seedocs/testnet-deployments.md. Closes issue #12. - Redeploy current upstream
mainto testnet with live create, release, refund, timeout-refund, andBalanceMismatchverification (issue #109). The current contract ID and transaction links are indocs/testnet-deployments.md; prior deployment records remain as history. - Releaser-key compromise runbook and race-to-settle tooling (issue #94).
docs/runbooks/releaser-compromise.md(linked from threat model T1) and the resumablescripts/settle-outcomes.pyare in; the 50+ escrow testnet drill has not been run or recorded yet. - Rest of the
docs/threat-model.md§6 pre-mainnet checklist, scoped into issues: #8 write status before transfer + reject contract-self addresses (Trivial), #9 events (Medium — done, seecontracts/escrow/tests/events.rsandARCHITECTURE_ESSENTIALS.md § Events), #10 storage TTL (Medium), #15 integrator trust-model docs (Trivial). - Related, not in the checklist: #13
get_escrowfull-record read (Medium), #14 offline SDK unit tests in CI (Medium).
- Optional M-of-N Soroban custom releaser account (issue #50):
contracts/releaser-account/verifies distinct Ed25519 signatures, restricts authorization to onerelease/refundcall on its configured escrow, and requires the current threshold for nonce-bound signer and threshold changes. Design is indocs/adr/0005-custom-releaser-account.mdand the T1 update indocs/threat-model.md. Not deployed to testnet; this does not change the escrow contract or make mainnet use permissible.
- Pick a maximum length (64 bytes) based on observed integrator usage
(UUIDs, short labels; existing test refs far shorter), document the
rationale, and reject longer refs with a new error code
(
ConditionRefTooLong = 7) at creation — closes issue #57. - Update the SDK to validate the same bound client-side with a shared
constant (
MAX_CONDITION_REF_LENGTH), so callers fail fast instead of paying network fees for a rejected transaction. - Update T10 in
docs/threat-model.mdwith the bound and rationale, and updateREADME.md+ARCHITECTURE_ESSENTIALS.mdto surface the new contract/SDK surface. Contract tests added incontracts/escrow/tests/create_escrow.rs(hard rule 2). - Redeployed to testnet with
ConditionRefTooLongon 2026-10-20; lifecycle andBalanceMismatchverification are recorded indocs/testnet-deployments.md.
-
Batch release and refund (issue #29,
docs/adr/0012):settle_many(caller, ops: Vec<(escrow_id, Release|Refund)>), best-effort with one result per op, per-op auth against each escrow's own releasers, at mostMAX_SETTLE_OPS= 20 (measured against the 16 KiB events budget). T1 response guidance updated. Tests:contracts/escrow/tests/settle_many.rs. Not yet on testnet. -
Per-token liability accounting and an on-chain solvency view (issue #39):
solvency(token) -> (balance, liabilities); the counter moves in the same invocation as every value-moving state change and its TTL is extended on each write. Property test:contracts/escrow/tests/liabilities.rs. Monitoring guidance indocs/threat-model.md. Not yet on testnet. -
Optional escrow-wide earliest release time (issue #43, ADR 0009); refund path remains independent.
-
Two-party releaser rotation (issue #23): current releaser and payer authorize; only funded single-releaser escrows can rotate. Contract and payee addresses are rejected.
-
Undeliverable payee handling (issue #55): a rejected outbound release transfer (deauthorised SAC trustline, rejecting contract payee) returns append-only
Error::Undeliverable = 35and leaves the escrowFunded, so the releaser can retry or refund. Tests:tests/undeliverable.rs. -
OpenZeppelin Stellar library evaluation (issue #118) —
docs/adr/0006: all six candidate modules checked against ADR 0001/0002 and the "no hidden admin" rule. Pausable, Upgradeable and the access/role modules are rejected (each adds a contract-level role, and a pause switch would let its holder block every release and timeout refund); smart-account/multisig-account modules stay out of scope; no stable OZ release supports this contract'ssoroban-sdkmajor (0.7.2 requires^26.1.0, this contract pins 27.0.6). Adopted: the audited checks-effects-interactions ordering (T4) implemented directly insettle()— status written before the outbound transfer, with a rollback test (tests/malicious_token.rs::failed_payout_leaves_the_escrow_funded). Library pinning/update policy and the wasm/instruction-cost comparison recorded in the ADR. Not yet on testnet. -
In-contract M-of-N releaser quorum (issue #119) —
docs/adr/0007: optionalcreate_escrow_with_releasers(releasers, threshold)(1–10 addresses, threshold 1..=len, no duplicates) plusrelease_with_approvals/refund_with_approvals. Approvals are collected in one call (each approver authorizes that exact invocation) rather than accumulated, so there is no approval state to clear and nothing to replay; duplicates are rejected and never count twice.EscrowDatanow storesreleasers+threshold(single-releaser escrows are stored as 1-of-1, entry points unchanged);release/refundon a quorum escrow returnThresholdNotMet. Timeout refunds stay open to anyone. New append-only errors 13–17. 19 tests intests/quorum.rs; single-releaser suites unchanged and green. Breaking read shape → SDK 0.5.0, bindings regenerated. Not yet on testnet — the live deployment predates it. -
Payer cancel before the payee accepts (issue #27) —
docs/adr/0010:cancel(escrow_id, payer), allowed only whileFunded, on an escrow withrequires_acceptance, and before the payee'saccept. Reuses #149'srequires_acceptance/accepted_atandsettle()(checks-effects-interactions), so funds go to the storedrefund_to(#158) and I1 is unchanged. New terminalEscrowStatus::Cancelled; no new error codes. Tests:tests/cancel.rs, including both orderings of accept vs cancel in one ledger, sponsoredrefund_to, andtop_upinteraction. SDKclient.cancel(...); bindings regenerated; 0.6.0 → 0.7.0. Not yet on testnet — the live deployment predates it. -
Sponsored funding: a funder distinct from the refund recipient — issue #28. ADR first:
docs/adr/0009answers the two questions the issue poses — a separate refund destination is compatible with invariant I1 (it is a second address stored at creation and read, never supplied, at payout), and it is authorised by the funder in their existingcreate_escrowsignature (Soroban auth covers all arguments; the recipient only receives and need not sign). Implementation: optionalrefund_tooncreate_escrow,create_escrow_with_acceptanceandcreate_escrow_with_releasers(defaultpayer, so omitted = unchanged), stored inEscrowData, used by every refund path (releaser and timeout share one transfer; payee-initiated voluntary refunds (issue #40, ADR 0010) also use the same storedrefund_todestination), rejected with newError::InvalidRefundTo = 21if it is the contract itself. Tests: 16 incontracts/escrow/tests/sponsored_refund.rs— releaser refund, timeout refund (stranger, funder, exactly at the deadline), no authority from being funder/recipient, release unaffected, default unchanged, contract-self rejected, funder auth required, recipient never signs. I1 wording updated indocs/threat-model.md(plus actors, T1, T2, T8, new T11 and §6). Breaking change:create_escrowgains a parameter andEscrowDataa field;@docforum/escrow-sdk0.5.0 → 0.6.0 (bindings regenerated, never hand-edited; addsrefundTooncreateEscrowandgetEscrow). The current contract was deployed on testnet 2026-10-20 and the SDK tests target it. Publishing thesdk-v0.6.0release tarball per ADR 0003 remains pending. -
Immutable deploy-time token allowlist (issue #36):
__constructor(allowed_tokens)stores approved addresses;create_escrowrejects unlisted tokens with append-only error code 7. An empty list preserves allow-all behavior. Tests cover listed, unlisted, and empty-list cases. Threat model, deployment guidance, and contract API docs updated. Deployed and verified with the immutable testnet allowlist on 2026-10-20. Closes issue #36. -
Optional release callback (issue #53): both single-releaser and quorum creation require payee authorization when
notify_payeeis true; release callson_escrow_releasedafter transfer and rolls back atomically on failure. Re-entry tests cover release/refund/create attempts. ADR 0005 and threat-model T11 document the decision. Deployed on testnet 2026-10-20. -
Optional protocol fee taken at release, with recipient and bps fixed at creation — issue #30.
docs/adr/0010: an optionalfee_recipient/fee_bpspair (atomic — a half-set pair is rejected), capped at 10 % (MAX_FEE_BPS = 1_000), taken off the top at release —⌊amount × fee_bps ÷ 10 000⌋tofee_recipient, the exact remainder topayee(floor rounding, so fee + payout always equal the storedamount) — and never onrefund, which still returns the fullamount.fee_recipientmay not be the escrow contract itself. Both fields are readable fromget_escrowbefore funds are locked.create_escrowandcreate_escrow_release_timetake the pair; the acceptance and M-of-N entry points remain fee-free.top_upgrows the escrowed amount and therefore the bps fee, re-checking the fee math before its transfer. New append-onlyError::InvalidFee = 35, new invariant I7, 17 tests intests/protocol_fee.rs. Breaking read shape and creation signature → SDK minor changeset and regenerated bindings. Not yet on testnet — the live deployment predates the fee. -
Emergency pause that can only stop new deposits — issue #35.
docs/adr/0011: onepause_adminaddress fixed at deployment by__constructor(allowed_tokens, pause_admin), which maypause/unpausea flag that blocks every deposit path —create_escrowand its variants (they sharecreate_escrow_inner),top_up, and the pooledcreate_pooled_escrow/contribute— and nothing else.release,refund, the timeout refund and pooledclaim_refundnever read it, so a pause can never lock existing funds (invariant I8, threat model T12). Alternatives rejected in the ADR: permissionless pause (trivial griefing), auto-expiring pause (silently lapses mid-incident), token allowlist (a different problem), and doing nothing. New error codesPaused,AlreadyPausedandNotPaused, with codes 38–40 (contract 2.5.0);Paused/Unpausedevents carry the custodian. 20 tests incontracts/escrow/tests/pause.rs, including release, releaser refund, stranger timeout refund, and release-after-deadline while paused, plus a year-long pause leaving nothing stuck,top_upblocked, and pooled contributions blocked while pooled claims and releases still pay out. SDK minor changeset withpause/unpause/isPaused/getPauseAdminand regenerated bindings. Not yet redeployed to testnet — the current deployment predates the constructor and cannot pause. -
Redeploy to testnet with the new constructor (
allowed_tokens,pause_admin) and verifypause/unpauselive, including a release while paused. -
Decide the mainnet custodian: a Stellar multisig account (2-of-3) held in a KMS/HSM — no contract change needed, it is the deploy-time
pause_adminargument (threat model T12). -
Pooled escrows funded by multiple payers (issue #113) —
docs/adr/0011: open contributions until an explicitfunding_target(fixed contributor list rejected — see ADR table), bounded by afunding_deadline. New entry pointscreate_pooled_escrow/contribute/claim_refund/get_pooled/get_contribution;EscrowDataand all existing entry points unchanged (pooled records live under two appended storage keys).contributeis permissionless before the deadline, delta-verified via the existingtransfer_in_exact, recorded per payer (repeats accumulate), and rejects overfunding.releaseadditionally requiresfunded == target, so partial pools never release. A missed deadline opens pull-based refunds: each contributor claims their exact recorded amount once, independently — no contributor iteration anywhere, no contributor cap by design (ADR 0011 "Resource limits"). Pushrefund/refund_with_approvals/cancel/top_upreject pooled escrows (append-only errors 26–34). Threat model: I1–I4 generalized, new T16–T19 + traceability rows. Tests: 18 intests/pooled.rs(scenarios A–M) + 3 malicious-token contribution tests; existing suites and snapshots unchanged. SDK0.7.0 → 0.8.0(additive). Not yet on testnet — the live deployment predates it. -
Payee rights transfer (issue #45):
transfer_payee(escrow_id, current_payee, new_payee)lets the current payee reassign the release destination to a new address while the escrow isFunded. Only the current payee may authorise this call — the payer and releaser have no power to redirect funds via this path.new_payeemay not be the contract's own address (would permanently lock funds) or the payer (would let a colluding payee redirect a release to the payer, bypassing refund auth). Emits apay_xferevent. This is a rotation tool: a payee who has already lost their key cannot use it (they cannot sign). Key loss remains an operational risk the payee must mitigate off-chain. 12 tests incontracts/escrow/tests/transfer_payee.rscovering the happy path, release/refund after transfer, chained transfers, auth rejection for stranger/payer/releaser, and both rejectednew_payeetargets. Rejected withError::InvalidNewPayee. Threat model I1 updated; T11 added covering the new payee-key attack surface.ARCHITECTURE_ESSENTIALS.mdcontract surface updated. Closes issue #45.
- Escrow lifecycle snapshots and a configurable SubQuery indexer with a typed, paginated SDK query client (issue #87). Operators provide the deployed contract and starting ledger; no hosted indexer is assumed.
- Automated npm release workflow (
.github/workflows/release-sdk.yml), triggered by ansdk-v*tag: builds, typechecks, tests, runsnpm publish --provenance, and attaches the tarball to the GitHub Release (kept for existing consumers per ADR 0003). Uses npm trusted publishing (OIDC,id-token: write) so no long-lived token sits in secrets; refuses to publish if the tag doesn't matchpackage.json's version. Closes issue #16. - Adopted changesets (
.changeset/,@changesets/cli) for version bumps and changelogs;sdk/package.jsongainedchangeset/version/releasescripts andpublishConfigwith provenance. - Superseding ADR written:
docs/adr/0005supersedes ADR 0003's interim tarball-only decision now that npm publishing is going live. - One-time human steps (npm org creation, trusted-publisher setup for
the release workflow) — documented in
CONTRIBUTING.md; not yet done, sosdk-v0.3.0is not yet published to npm. - Fee estimation and fee-bump sponsorship helpers (issue #70).
estimate({ source, method, args })simulates a call and returnsresourceFee(with a 20% buffer over the simulated minimum, rounded up),inclusionFee,total, and the rawminResourceFee, in stroops.feeBump(signedXdr, sponsor, { maxFee, baseFee? })wraps a signed envelope and chargesbaseFee × (inner operations + 1); it throwsMaxFeeExceededErrorbefore building anything when that exceedsmaxFee, so a fee spike can never silently overshoot the cap.guardSponsorship/assertSponsorableinspect every operation and every contract-address auth entry, rejecting foreign contract calls, non-invocation operations, foreign auth targets, and already-bumped envelopes while allowing the account auth entries a legitimate escrow call carries. 17 offline unit tests insdk/tests/fees.test.ts(wired into CI asnpm run test:unit) plus a live-testnet estimate/fee-bump case intestnet-integration.test.ts; additive SDK surface,0.4.0 → 0.5.0. Closes issue #70. - React hooks package (issue #126) —
packages/escrow-react, published name@docforum/escrow-react. TanStack Query v5 peer.useEscrow(id)pollsgetEscrowand stops atReleased/Refunded;useEscrowActions(signer)invalidates the query after a settlement and resolves wallet rejection as{ status: "rejected" };useEscrowTimelinecounts down torefundAfterwith a documented ledger-vs-local clock caveat. Hooks only — no UI. 14 React Testing Library tests with a fake client and a dedicated CI job. Demo inexamples/web-demo/. - Stellar Wallets Kit adapter (issue #127) —
WalletsKitSigneron the@docforum/escrow-sdk/signers/wallets-kitsubpath, implementing the SDK's SEP-43Signersurface. The kit is an optional peer dependency and is injected, so there is no runtime import and server bundles stay wallet-free. TypedWalletNetworkMismatchErrorbefore signing; user rejection returned as SEP-43-4rather than thrown. 12 offline fake-kit tests. - Go client SDK (issue #128) —
sdk-go/, modulegithub.com/DocForum/docforum-escrow/sdk-go.CreateEscrow,GetStatus,GetEscrow,Release,Refund(+TopUpand approval paths), typed contract errors,big.Inti128 amounts, offline unit tests and an opt-in quickstart integration suite with its own CI workflow. - OpenTelemetry instrumentation hooks (issue #129) — optional
instrumentation/onEventonEscrowClient, spans forsimulate,sign,submitandconfirm. No tracer/observer means no clock read and no span allocation. Attributes carry contract id, function, escrow id, transaction hash and retry count; addresses are opt-in only (includeAddresses: true). 7 offline tests assert span production and the disabled path. - Token-aware amount parsing and formatting (issue #81) —
sdk/src/amounts.ts:getTokenInfo(token)readsdecimals,symbolandnamefrom the SEP-41 token contract (cached per contract address);parseAmountandformatAmountconvert between human strings and rawbigintwith strict validation (no floats/exponents, no excess fractional precision, no negatives, locale-independent). Property tests assertformat(parse(x)) == normalise(x). Used in the web demo and SDK docs. - Network presets with an explicit mainnet opt-in guard (issue #67) —
networks.testnet,networks.futurenet,networks.localpresets carry an RPC URL, network passphrase and known contract ids. Constructing a client with the mainnet passphrase throws unlessallowMainnet: trueis passed; custom networks (private quickstart) remain possible. With a preset (orverifyNetwork: true) the RPC'sgetNetworkpassphrase is verified lazily, once, before the first network call (never in the constructor, cached per client), so a customrpcUrlcannot bypass the guard. Offline unit tests cover each guard. Additive SDK surface (minor changeset). - SDK lifecycle amendments (issue #84). Done:
topUp,accept,cancel(nowEscrowSigner),createEscrow({ requiresAcceptance }), typedEscrowPreconditionErrorchecks before submission, and aprepareLifecycleCall→signLifecycleCall→submitLifecycleCallflow for parties on different machines. SDK0.7.0 → 0.8.0. Waiting on the contract:extendRefundAfter(#41) andrefundByPayee(#40) need their contract functions merged first.
- Contract version and reproducible build provenance exposed in the contract interface and WASM metadata (issue #52).
- Storage and resource optimisation guided by benchmarks (issue #98) —
get_statusreads a dedicated persistent status value instead of decodingEscrowData. Benchmarks showed that movingNextIdto persistent storage raised creation costs, so it remains in instance storage. The added status record improvesget_statuswhile adding writes to creation and settlement. Storage layout changed, so deploy as a new testnet contract ID under ADR 0005 and let existing escrows settle on their original contract. - Coverage-guided contract fuzzing (issue #99) —
contracts/escrow/fuzz/runs randomized create/release/refund sequences with ledger timestamp jumps against the Soroban test environment and checks payout, status, and balance invariants after each operation. CI runs a short cargo-fuzz pass on pull requests and a longer nightly pass, uploading crash artifacts. The committed seed corpus starts the lifecycle and timeout paths. No contract or SDK behavior changed. - Reproducible WASM builds and deployment verification (issue #91) ?
Rust 1.96.0 and Stellar CLI 28.1.0 are pinned; the pinned
rust:1.96.0-bookwormcontainer compares release WASM hashes from two checkout paths after remapping repository paths.contract-v*releases publish the WASM and SHA-256 checksum.scripts/verify-deploymentfetches deployed WASM from testnet and compares it with a local build; README documents third-party verification. Nostellar contract optimizestep is used. The current testnet deployment predates this source, so the verifier reports the expected hash mismatch. Contract tests/build and the two-path comparison passed locally. - PR malware scan —
.github/workflows/pr-malware-scan.yml+.github/scripts/pr-malware-scan.cjs, on every PR push. Flags editor/devcontainer autorun tasks, npm install hooks, typosquatted packages and Actions, non-registry dependency sources, payloads disguised as fonts/images, base64/whitespace obfuscation, exfiltration endpoints, reverse shells and miners. HIGH findings fail theMalware scancheck, label the PRsecurity-reviewand @mention the author; one comment per PR, updated in place. Reads the PR as data only (never checks out or runs it). Added 2026-10-20 after a VS Code autorun payload (escrow #163) and a typosquatted action (escrow #176). Manual backfill (Actions → PR malware scan → Run workflow,prs: all) scans PRs opened before the scanner and records aMalware scancommit status, which satisfies the required check. - Nightly testnet escrow lifecycle soak (issue #108) —
.github/workflows/nightly-testnet-soak.ymlruns the SDK live integration suite nightly with generated identities, transient Friendbot retries, release/refund paths, and a short timeout refund. Quorum coverage remains skipped until the documented testnet contract supports it. Failures update one tracking issue in place and classify contract-missing resets, infrastructure failures, and SDK test failures. Manual dispatch includes a forced-failure reporting check. - Commit Soroban test snapshots, review event/storage diffs in PRs
(issue #120) —
test_snapshots/is no longer git-ignored; everycargo testwrites one snapshot per test (events, auth, ledger/storage) and CI fails if the committed files change without being committed. The snapshots were reviewed once on the initial commit. Documented event-capture caveat: the SDK clears the event buffer at the start of every top-level invocation, so a snapshot only shows the events of a test's last contract call —tests/top_up.rs::top_up_emits_an_eventdeliberately ends on the emitting call so the mechanism is visible in a diff. Determinism verified by running the suite twice against the committed files (no diff). - CI check that generated SDK bindings match the contract WASM
(issue #121) —
scripts/check-generated-bindings.shplus thesdk-bindingsCI job: builds the WASM, regenerates with the Stellar CLI version pinned inscripts/stellar-cli-version(28.1.0, release tarball checksum pinned too), and fails with regeneration instructions ifsdk/src/generated/contract-client.tsdiffers. Detects both a stale binding after a contract change and a hand edit; CI never commits the regeneration. Instructions insdk/src/generated/README.md; the previously stale committed bindings (missingtop_up, errors 6–8) were regenerated here. - PR conflict notifier —
.github/workflows/pr-conflict-notifier.yml+.github/scripts/pr-conflict-notifier.cjs. On every push tomain(and every 3h) it labels open PRs that conflict withmainasneeds-rebaseand @mentions the author once per push with rebase steps; the label clears itself when the conflict is resolved. Runs frommainonly and never checks out PR code. - Property-based state-machine tests with proptest (issue #64).
Pure-Rust reference model of the escrow state machine in
contracts/escrow/tests/model.rs— no Soroban types, intentionally simple enough to review independently. A proptest state machine generates operation sequences (create/release/refund/timeout-refund) and compares contract results and stored state against the model after every step, including auth outcomes (who may call what, when). Shrunk failures print a readable operation sequence. CI runs a bounded case count (PROPTEST_CASES); a nightly workflow runs a larger count. Closes issue #64. - TLA+ escrow state-machine model (issue #112) —
spec/Escrow.tla+spec/Escrow.cfgmodel the actual escrow lifecycle, status transitions, timeout refund behavior, payee acceptance, and same-ledger race windows while explicitly omitting unsupported disputes/milestone logic. TLC verification passed against the committed model. - Scripted, deterministic deployment with records (issue #89) —
scripts/deploybuilds or takes the WASM, checks its hash, uploads, deploys with the constructor allowlist, confirms the on-chain WASM, runs scripted live verification (create, release, refund, three rejection cases) and records the result indeployments.json,docs/testnet-deployments.mdand the SDK (knownContractId,knownNetworkConfig). Refuses a dirty tree and mainnet;--dry-runtargets a local quickstart.
- Any healthcare-specific logic (see README).
- Fiat on/off-ramp, KYC — that's
docforum-core's concern if/when it builds general payments (see prior architecture discussion), not this repo's.
New entries go in
changelog.d/, one file per PR — seechangelog.d/README.md. Maintainers fold them in here withnode scripts/fold-changelog.mjs. Please don't append to this section directly.
- 2026-10-19 — Issue #70 (Phase E6): added
estimate,feeBump,guardSponsorshipandassertSponsorableto the SDK. Estimation runs through the client's RPC and reports the resource fee with a buffered 20% headroom, the inclusion fee, the total, and the unbuffered minimum; the buffered figure rounds up so an estimate is never a strop short. Fee-bumping refused to guess like the original branch did: it builds the fee-bump withTransactionBuilder.buildFeeBumpTransaction, chargesbaseFee × (inner operations + 1)(the inner fee is refunded), and checks the cap before building, throwingMaxFeeExceededErrorinstead of overshooting. The sponsorship guard inspects every operation and every contract-address auth entry — an early draft compared account auth addresses to the contract and would have rejected legitimate payer/releaser entries — and unwraps (then rejects) an already-bumped envelope. 17 offline unit tests insdk/tests/fees.test.ts, wired into CI asnpm run test:unit, plus an estimate/fee-bump case against the live testnet deployment; additive surface,0.4.0 → 0.5.0. README,ARCHITECTURE_ESSENTIALS.mdand this roadmap updated. Closes issue #70. - 2026-10-20 — Added the Phase E7 nightly testnet soak workflow for issue #108, with transient Friendbot retries, fresh test identities, timeout refund coverage, failure classification, de-duplicated issue reporting, and a manual forced-failure path.
- 2026-10-19 — Required payee authorization when
notify_payeeis enabled, and extended the callback to quorum escrows after #203 introduced M-of-N releasers. Updated ADR 0005, T11, API docs and generated bindings. - 2026-10-20 — Phase E5 + E7 batch (issues #118, #119, #120, #121).
#118: ADR 0006 evaluates OpenZeppelin's Stellar contracts library;
pause/upgradeable/access are rejected (contract-level roles, and a pause
switch would gate releases and timeout refunds), no stable release
supports
soroban-sdk27, so no crate is adopted — the audited checks-effects-interactions ordering is applied directly instead (settle()writes status before transferring; rollback test added). Dependency pinning/update policy + measured wasm/CPU comparison in the ADR (wasm 24,516 → 29,338 bytes; settlement paths +1.3–1.6%). #119: ADR 0007 adds an optional in-contract M-of-N releaser set:create_escrow_with_releasers,release_with_approvals,refund_with_approvals; one-call approvals (no stored state, nothing to replay), duplicates rejected,MAX_RELEASERS = 10, errors 13–17;EscrowDatanow carriesreleasers+threshold(single-releaser escrows store 1-of-1 and keep usingrelease/refund). 19 new tests; 57 contract tests green. SDK 0.4.0 → 0.5.0 (breaking read shape), withcreateQuorumEscrow/releaseWithApprovals/refundWithApprovals, offline validation tests, and regenerated bindings. Not yet on testnet. #120: committedtest_snapshots/for every test (57 files) and a CI drift check; documented how to update them intentionally and the last-invocation-only event-capture caveat. #121: pinned the Stellar CLI (scripts/stellar-cli-version, 28.1.0 + tarball checksum), addedscripts/check-generated-bindings.shand thesdk-bindingsCI job; regenerating against the current WASM revealed the committed bindings were stale (missingtop_up,create_*docs and errors 6–8) — now fixed. CI's offline SDK step also runs the new quorum validation tests (npm run test:offline). Note on numbering: Phase E6 above referencesdocs/adr/0005(npm publishing), but that file is not in this tree — 0006 and 0007 are used by this batch to avoid overwriting it if it lands later. - 2026-10-17 — Phase E5 started: bounded
condition_refto 64 bytes at creation (new errorConditionRefTooLong = 7), added contract test, added SDK client-side validation (MAX_CONDITION_REF_LENGTH), updated T10, README and ARCHITECTURE_ESSENTIALS.md. Not yet redeployed to testnet. Closes issue #57. - 2026-10-18 — Fixed PR #183's contract CI after merging newer
main: restored theIntoValimport, assignedCallbackFailederror code 8 afterDisallowedTokencode 7, supplied the callback opt-in and allowlist constructor arguments in affected tests, and regenerated the SDK binding from the merged contract spec.cargo testand the WASM release build pass locally. - 2026-10-16 — Implemented the immutable deploy-time token allowlist for issue #36. Contract tests/docs updated; awaiting a new testnet deployment before claiming the protection is live.
- 2026-10-17 — Issue #63 (Phase E6): added SDK
getEventspagination and typed lifecycle-event decoding, preserving unknown topic variants for forward compatibility. Added cursor-resumable serial polling with handler back-pressure and typed RPC-retention errors. Committed Soroban XDR event fixtures cover decoding, filters, unknown topics, cursor resume, retention errors, and stopping. Bumped the additive SDK surface from 0.3.0 to 0.4.0. SDK typecheck/build and contract tests/build pass; lifecycle events remain unavailable until issue #9 changes the contract. - 2026-10-18 — Phase E7 (issue #64): added a pure-Rust reference
model of the escrow state machine (
contracts/escrow/tests/model.rs) and proptest state-machine tests comparing contract results and stored state against the model after every generated operation, including auth outcomes. CI runs a bounded case count; a nightly workflow runs a larger one. - 2026-09-09 — Repo created as part of the DocForum 3-repo org split.
- 2026-10-09 — Phase E6 started: added
.github/workflows/release-sdk.yml(tag-triggeredsdk-v*build/test/npm publish --provenance+ GitHub Release tarball, OIDC trusted publishing, tag/version guard), adopted changesets, wrotedocs/adr/0005superseding ADR 0003, and documented the one-time npm org/trusted-publishing human steps inCONTRIBUTING.md. npm publish itself still pending those human steps. Closes issue #16. - 2026-09-14 — Phase E1 implemented:
create_escrow/get_statusonsoroban-sdk27.0.6, 4 tests passing, wasm build verified. Testnet deploy intentionally left undone (needs a funded identity + CLI this environment doesn't have) — do not mark E1 fully done until that lands. - 2026-09-14 — Phase E1 fully closed: deployed to testnet
(
CABSYY5FZGCCZ3UTBQGC7S357D2FUFLUQUUGJCCFHKGEJZVIFK4SIS2Z) via thestellarCLI, and verified live with a realcreate_escrowcall (0.5 testnet XLM, native SAC) followed byget_statusconfirming"Funded". Seedocs/testnet-deployments.md. Issue #1 closed. - 2026-09-14 — Added
.github/ISSUE_TEMPLATE/bug_report.md(identical copy across all three org repos — seedocforum-core's changelog for the sourcing note: a user-supplied bug-report structure plus drips.network's "Creating Meaningful Issues" guide). - 2026-09-14 — Replaced the placeholder
ci.yml(echo "TODO",pull_request-only, never once run — every commit so far went straight tomain) with a real workflow:cargo test+cargo build --target wasm32v1-none --releaseagainst the actual contract incontracts/escrow/, triggered on both push tomainand PRs, with a final check that the wasm artifact actually exists. SDK build/test intentionally left out of CI —sdk/package.json'sbuild/testscripts are stillecho TODOplaceholders (issue #4), nothing real to validate yet; add them to this workflow when #4 lands. Done ahead of a Drips Wave application: a reviewer or prospective contributor landing on this repo should see it actually exercising its own tests, not a stub. - 2026-09-15 — General workspace-audit fixes (applied identically across
all three org repos, see
docforum-core's changelog for the full rationale): addedCONTRIBUTING.md(human onboarding —AGENTS.mdis agent-facing, and this repo's contract moves real funds, so it also notes reporting security issues privately rather than as a public issue, ahead of the planned Phase E4 review); enabled branch protection onmain(real CI check + 1 approval required to merge, force-push/ deletion disabled,enforce_adminsleftfalseso the maintainer isn't blocked). Topics were already added in the prior session. - 2026-09-15 — Implemented Phase E2:
release()andrefund(). Auth model decision recorded indocs/adr/0002— a per-escrowreleaseraddress, set atcreate_escrowtime, rather than a single contract-level admin (reasoning: no single point of control, matches who actually knows when to release/refund, stays generic per ADR 0001, multi-tenant ready). This changedcreate_escrow's signature (addedreleaser: Address) — a breaking change from the Phase E1 signature already live on testnet; that deployment record stays as accurate history, not retroactively edited. 7 new tests intests/release_refund.rscovering both functions' happy paths, unauthorized-caller rejection, double-release, refund-after-release, and release-after-refund. 11/11 tests passing, wasm release build verified.ARCHITECTURE_ESSENTIALS.mdcontract-surface table updated. Closes issues #2 and #3. Not yet done: redeploying to testnet with the new signature — Phase E2 is proven in the local test suite only so far, not live. - 2026-09-15 — Redeployed to testnet with the Phase E2 signature
(
CAXESEEAJILFFCN3PH2HHNMWAAHLEB64UTJHZ6IP7C5BBICT3S3VODSM, supersedes the Phase E1-only deployment) and verifiedrelease/refundlive: real fund transfers on both paths, plus a real unauthorized-caller rejection (Error(Contract, #3)) confirmed via the contract's own diagnostic events, not just a local assertion. Closes the "not yet done" gap from the previous entry. Full record:docs/testnet-deployments.md. - 2026-09-15 — Built the TypeScript SDK (
@docforum/escrow-sdk), closing issue #4:sdk/src/generated/contract-client.ts(machine-generated from the local wasm build viastellar contract bindings typescript) plus a hand-writtenEscrowClientinsdk/src/index.tswrapping all four contract functions — expanded from #4's original create_escrow/get_status-only scope since Phase E2 landed in the same session, so there was no reason to ship an immediately-stale SDK. Accepts aKeypairdirectly as a signer (no manual XDR signing exposed). 3 real tests insdk/tests/testnet-integration.test.ts, all passing against the live Phase E2 deployment — full release lifecycle, full refund lifecycle, unauthorized-caller rejection — satisfying Phase E3's "integration test against testnet from the SDK itself" item. Deliberately kept out ofnpm test/CI (npm run test:integrationonly, CI runstypecheck+build) given its dependency on live testnet + Friendbot funding, a different reliability profile than this org's other self-contained integration tests.ARCHITECTURE_ESSENTIALS.mdupdated with the SDK's actual public surface. - 2026-09-15 — Closed issue #5: decided not GitHub Packages (its npm
registry requires auth even for public packages — would break
npm cifor any external contributor's fork, exactly what the issue warned against). First tried an npm git-subdirectory dependency (github:DocForum/docforum-escrow#path:sdk) as a zero-infrastructure option — tested it againstdocforum-coreand it failed (npm 10.8.2 doesn't support the subdirectory selector the way assumed). Replaced with a tarball attached to a real tagged GitHub Release (sdk-v0.1.0, vianpm pack+gh release create) — verified working end-to-end: installed and imported successfully fromdocforum-core/backendwith zero auth. Added aprepare: npm run buildscript tosdk/package.json(unused by this specific approach, but harmless to keep for a future git- or workspace-based route). Real npm registry publish remains the long-term goal, blocked on publishing credentials this environment doesn't have. Seedocs/adr/0003. - 2026-09-15 — SDK v0.2.0 (breaking, per hard rule 3):
createEscrow,release, andrefundnow also return the transaction's hash ({ escrowId, txHash }/{ txHash }) instead of discarding it — needed bydocforum-core's payments module to persiststellarTxHash. All 3 live-testnet tests updated and still passing. Republished as GitHub Releasesdk-v0.2.0(see ADR 0003's release process);docforum-coreupdated to depend on the new tarball URL. - 2026-09-15 — Fixed two real bugs in
docs/adr/0003that brokedocforum-core's VitePress docs-site build entirely ("Element is missing end tag" — VitePress parses markdown through a Vue template compiler, which can misread certain angle-bracket placeholder text as an unclosed HTML tag): a placeholder phrase with a space inside angle brackets, and — found only after the first fix still didn't build — two separate bracketed placeholders inside one inline code span. Both rephrased to avoid angle-bracket placeholder syntax entirely rather than guessing at a safe form. Caught, and actually fixed for real (not just believed fixed), while checking the live docs site reflects this session's changes rather than assuming it did — verified by buildingdocforum-core's docs site locally against this exact file, twice, before trusting it. - 2026-09-26 — Updated the stale top status line (still said issue #5 was open; it closed 2026-09-15).
- 2026-10-01 — Drafted
docs/threat-model.mdfor Phase E4 (issue #6): invariants, actors, ten threats with severity, SDK and consumer integration notes, and a proposed pre-mainnet checklist. No contract changes. Self-written, so E4 stays open pending independent review. - 2026-10-08 — Closed issue #11 (Phase E4: timeout refund path when the
releaser key is lost). ADR first:
docs/adr/0004picks the threat model's candidate — an optionalrefund_afterunix timestamp set atcreate_escrow, after which anyone mayrefund, always only back topayer— and compares it against a privileged unlock role, releaser rotation, and doing nothing; it also spells out what the deadline means for payees (T8): the commitment expires,get_escrowlets them read it, and the new risk is bounded to "delayed then reversed", never redirected. Implementation:refund_after: Option<u64>added tocreate_escrow(rejects a deadline not strictly in the future — newError::InvalidRefundAfter = 5),refundaccepts any caller once the deadline has passed (state rules unchanged: stillFundedonly),releasedeliberately not gated by the deadline (the releaser keeps its right; whoever lands first wins, the loser getsNotFunded), plus a read-onlyget_escrowso the deadline is inspectable on-chain. With norefund_after, behaviour is exactly what it was before. Tests: 17 new intests/timeout_refund.rscovering every edge case issue #11 lists (release in the same ledger the timeout passes — both orders; deadline already past at creation; already released/refunded before the timeout; no deadline at all) — 28/28 passing repo-wide, wasm release build verified. Breaking change:create_escrow's signature andEscrowData's layout both changed, so the contract was redeployed to testnet (CAHQ4J3T23WSIBBG6SI6EAYR5Q2HIPKG5YMC4SMZBOTYA6MYK2VADEV5, supersedes the Phase E2 deployment, kept as history) and verified live with a real non-releaser timeout refund after the deadline, a real release after the deadline, and real rejections for a pre-deadline stranger refund (#3), a past deadline at creation (#5) and a second refund (#4) — full record with tx links indocs/testnet-deployments.md. SDK: bindings regenerated from the new wasm (never hand-edited),@docforum/escrow-sdk0.2.0 → 0.3.0 (breaking, per hard rule 3; same 0.x-minor-for-breaking convention as 0.1.0 → 0.2.0), adding optionalrefundAfteroncreateEscrowand agetEscrow()accessor; migration steps fordocforum-coreare in ADR 0004. All 5 SDK integration tests pass against the new deployment (npm run test:integration, still kept out of CI).ARCHITECTURE_ESSENTIALS.md,README.mdanddocs/threat-model.md(§1 invariants, T2, T8, §6) updated. Not done: publishing thesdk-v0.3.0release tarball per ADR 0003 — that's the follow-up step beforedocforum-corecan consume it. - 2026-10-08 — Fixed
mainfailing to compile after #17 and #18 merged minutes apart: both added an error variant numbered 5, and the merge kept #18'sInvalidRefundAfter = 5and dropped #17'sBalanceMismatch. Restored it asBalanceMismatch = 6(5 is already live on testnet asInvalidRefundAfter), and addedrefund_after: Noneto #17's mock-token tests, written before #18 changedcreate_escrow's signature. 31/31 contract tests pass, wasm build succeeds. Not yet done: the current testnet deployment (source7a595d8) predates #17, so it doesn't include the balance-delta check — needs a redeploy. - 2026-10-08 — Restored two sets of ROADMAP entries lost in merge
conflict resolution: #17's (issue #12, balance-delta check — Phase E4
item and its 2026-10-07 changelog entry, summarised here: contract
rejects escrows whose token balance doesn't rise by exactly
amount, three malicious mock-token tests, re-entry confirmed fail-closed), and #16's (links to the threat-model issues #8–#10 and #13–#15; #11 and #12 were opened and closed in between). #16 merged with an empty diff because its branch's merge frommainkeptmain's ROADMAP.md wholesale. - 2026-10-20 — Added the PR conflict notifier workflow (see the CI item above): maintainers no longer need to manually ask authors to rebase after each merge.
- 2026-10-20 — Hardened the PR automation against transient GitHub API errors. Today a single 500/503 response aborted a whole conflict-notifier run, so no PR after it got labelled. Both
pr-conflict-notifier.ymlandpr-malware-scan.ymlnow setretries: 3onactions/github-script, so 5xx responses are retried. The notifier also isolates each PR: a PR whose API calls still fail is skipped with a warning, listed as "API error, retried next run" in the run summary, and the remaining PRs are processed normally. - 2026-10-16 — Phase E5, issue #28: sponsored funding. ADR 0009 first
(separate refund destination is compatible with I1; the funder
authorises it in the creation signature). Then optional
refund_tooncreate_escrow(defaultpayer), used by every refund path, withError::InvalidRefundTo = 21for the contract's own address. 16 new tests intests/sponsored_refund.rs; 47/47 contract tests pass repo-wide, wasm release build succeeds. SDK bindings regenerated;@docforum/escrow-sdk0.5.0 → 0.6.0 (breaking). I1 wording, actors, T1, T2, T8, new T11 and §6 in the threat model,README.mdandARCHITECTURE_ESSENTIALS.mdupdated. Not done: testnet redeploy and thesdk-v0.6.0release tarball. - 2026-10-20 — Issue #109: deployed current upstream main to Stellar testnet and verified
create, release, refund, timeout refund, and a real
BalanceMismatchrejection using the fee-on-transfer test token. Updated the SDK testnet contract ID and deployment documentation; previous deployment records remain as history. The SDK 0.6.0 release tarball and notification todocforum-coreissue #32 still require authenticated release access. - 2026-10-20 — Contributing: changelog entries now go in
changelog.d/(one new file per PR) instead of being appended to this section, which used to put most open PRs into merge conflict on every merge. Maintainers fold them in withnode scripts/fold-changelog.mjs. The contract surface now lives only inARCHITECTURE_ESSENTIALS.md(theREADME.mdsection is a summary that links there), with the few README-only details moved across, and itsrefund_toADR reference and timeout-refund destination corrected. Also dropped the hard-coded test count from the README, and documented the "edit only your own lines" and "regenerate, don't hand-merge,contract-client.ts" rules inAGENTS.mdandCONTRIBUTING.md. The conflict notifier now points authors atchangelog.d/. - 2026-10-21 — Phase E6 integrator tooling (issues #126, #127, #128, #129).
#127:
EscrowClientnow accepts a SEP-43Signeranywhere it accepted aKeypair; addedWalletsKitSigneron the@docforum/escrow-sdk/signers/wallets-kitsubpath with an injected, optional Wallets Kit peer and no runtime import of it. Wrong-network signing throwsWalletNetworkMismatchError; user rejection is a normal SEP-43-4outcome. #129: optionalinstrumentation/onEventwithsimulate/sign/submit/confirmspans and retry counts; addresses opt-in only; the disabled path does no timing work. SDK0.5.0 → 0.6.0(additive). #126: new@docforum/escrow-reactpackage withuseEscrow,useEscrowActions,useEscrowTimeline, React Query caching and polling that stops at terminal statuses; 14 RTL tests and a CI job. #128: newsdk-go/client with typed contract errors,big.Inti128 amounts, offline tests and an opt-in quickstart integration workflow. Addedexamples/web-demo/, docs and roadmap entries. Closes #126, #127, #128, #129. Go SDK:GetEscrownow decodesrefund_toas the contract'sAddress(ScvAddress) instead of a string, and the create helpers encodeRefundToas an address-backed option. Fixes the quickstart integration job'sexpected string ScVal, got ScValTypeScvAddressfailure. - 2026-10-21 — Issue #27 (Phase E5): payer
cancelbefore the payee accepts, ADR 0010.cancel(escrow_id, payer)works only whileFunded, on an escrow created with the acceptance gate, and beforeaccept; funds go to the storedrefund_to. New terminalEscrowStatus::Cancelled, no new error codes. SDKclient.cancel(), bindings regenerated,@docforum/escrow-sdk0.6.0 → 0.7.0. Not yet on testnet. - 2026-10-21 — Issue #91 (Phase E7): pinned Rust and Stellar CLI versions; added remapped WASM builds and a two-checkout-path CI check in a pinned Rust container; added
contract-v*WASM and SHA-256 release assets, a testnet-only deployment verifier, and third-party verification instructions. Contract tests/build and two-path comparison pass locally; live testnet fetch returns the recorded older hash as expected. - 2026-10-21 — Malware scan:
.vue,.svelte,.astro,.less,.sass,.go,go.modandgo.sumfiles are now scanned as code. Before this, a Vue single-file component was flagged HIGH as "disguised code" (JavaScript inside a file with a non-code extension), which wrongly blocked docforum-core #207. The same line rules still apply to these files. - 2026-10-21 — Malware scan: closed three gaps found in the malicious docforum-web #137,
which only had three of its nine problems flagged. Code pushed off-screen by
leading whitespace is now flagged (
api.jshid its payload behind 315 tabs). Apackage.jsonscript such asdevorbuildthat now also runsnode <file>(or bun, deno, sh, bash, python) alongside its original command is flagged asscript-hijack. Theglobal['!'] = …marker of the loader used in escrow #163 and web #137 is flagged asknown-payload. The whole-line heuristics no longer stop the per-line rules from running on the same file. - 2026-10-21 — Malware scan: TLA+ specifications (
.tla) are now scanned as text, like other source files. Before this, a TLA+ spec was flagged HIGH as "disguised code", which wrongly blocked docforum-escrow #225. Every line rule still applies to.tlafiles. - 2026-10-22 — ### Added
sdk/src/factory.ts:predictInstanceAddress(factoryId, salt, networkPassphrase?)derives the contract address a factory'sdeploy_instancewill create, offline, using the CAP-0046 preimage (deployer address + salt + network id).FactoryClient(deployInstance,getInstance,forIntegrator) for a factory that exposesdeploy_instance(integrator, salt)andget_instance(integrator), plusrandomSalt().EscrowClient.forIntegrator(factoryClient, integrator)builds a client bound to an integrator's instance.sdk/tests/factory.test.tspins a golden derivation vector, and the live factory suites only run whenTESTNET_FACTORY_IDpoints at a deployed factory (none ships in this repo yet). Closes #205.
- 2026-10-22 — Issue #224: fixed the nightly testnet soak failures in the TypeScript SDK.
authorizeRelease,authorizeRefundandsubmitRelayedwere documented in the README but never implemented; they now simulate the settlement, collect only the releaser's short-lived auth entry, and let a separate relayer sign the envelope and submit it.getEscrownow falls back to decodingget_escrowby field name when the generated positional decoder meets a testnet deployment that predates a newerEscrowDatafield such asrelease_not_before. Added offline coverage insdk/tests/relayed.test.tsandsdk/tests/escrow-data.test.ts, and wired both intonpm run test:offline. No contract change; the generated bindings are unchanged. - 2026-10-22 — Issue #71 (Phase E6): added a milestone (tranche) surface to the SDK —
createMilestoneEscrow,releaseMilestone,refundMilestone,getMilestones, plusvalidateMilestoneSchedule/MilestoneScheduleErrorandMilestoneEscrowPartialError. The deployed contract is single-tranche and the native multi-tranche entry point is still the Phase E5 contract work (issue #21), so a schedule is modelled client-side as one ordinary escrow per tranche, tagged<conditionRef>#m<i+1>/<n>. No contract change and no hand-edit ofsdk/src/generated/contract-client.ts— the generated bindings are byte-identical to a fresh generation from the current WASM, which is what theSDK bindings match contract WASMCI check verifies.sdk/package.jsongainstest:milestones, run by CI; the schedule arithmetic, the ref tagging, the per-tranche settlement calls and the partial-failure state are all covered by offline tests. - 2026-10-22 — Issue #74 (Phase E5): added batched escrow creation and settlement. The
contract gains
create_escrows(payer, items)(atomic,items.len()<=MAX_BATCH_SIZE= 50) andsettle_many(caller, escrow_ids)(per-opResultin input order), and theBatchTooLargeerror is appended at discriminant 25 so every existingErrorvalue keeps its on-chain number. The SDK gainscreateEscrows(items)/settleMany(ops): they chunk toMAX_BATCH_SIZE, map results back to the caller's input order, throw aBatchPartialFailureErrorcarrying every per-item result when anything fails, and accept{ resumeFrom }so a retry only sends what failed. The batching is implemented in the wrapper by looping over the existing singularcreate_escrow/release/refundcalls, so no per-batch auth or state semantics are introduced on the client.sdk/src/generated/was regenerated with the pinned Stellar CLI 28.1.0. - 2026-10-22 — Added a contract test for
release_not_beforeequal to the creation time (valid, and releasable straight away). The first nightly mutation-testing run found that replacing<with<=in that creation check (src/lib.rs:594) changed no test result. The new test fails on that mutant. - 2026-10-23 — Issue #113 (Phase E5): pooled escrows funded by multiple payers with
pull-based pro-rata refunds, decided in
docs/adr/0011(open contributions until an explicit funding target vs a fixed contributor list — open contributions chosen). New contract entry pointscreate_pooled_escrow,contribute,claim_refund,get_pooled,get_contribution; append-only errors 26–34;EscrowDataand every existing entry point unchanged, so single-payer behavior is untouched and all pre-existing test snapshots are byte-identical.releasenow requiresfunded == targeton pooled escrows; push refund/cancel/top_up are rejected for them (NotSupportedForPooled); a missed funding deadline opens per-contributorclaim_refund(exact recorded amounts, once each, no contributor iteration — no cap by design). Threat model I1–I4 generalized plus new T16–T19. SDK: additive wrappers (createPooledEscrow/contribute/claimRefund/getPooled/getContribution) and regenerated bindings,0.7.0 → 0.8.0(minor). Not yet deployed to testnet — the live deployment predates it. - 2026-10-24 — Issue #101 (Phase E4): added the formal-verification spike for invariants
I1–I3 and the timeout rule.
certora/escrow.specstates each property against the current settlement entry points andErrordiscriminants, anddocs/formal-verification.mdrecords the scope, assumptions, out-of-scope areas and current status (specified, not yet machine-checked — Certora Sunbeam needs a licensed toolchain, so there is no CI job). The threat model links I1–I3 to their properties. No contract behaviour or SDK surface changed. - 2026-10-24 — Issue #110 (Phase E7): added the
Futurenet compatibility checksworkflow (.github/workflows/futurenet-compatibility.yml) — manualworkflow_dispatchplus a weekly schedule, never onpull_request, so PRs are never blocked on futurenet. It builds the current WASM, installs the Stellar CLI pinned inscripts/stellar-cli-version(release tarball, sha256 verified), deploys to futurenet with a generated identity, and runs the futurenet SDK integration suite (npm run test:futurenet, scoped bysdk/vitest.futurenet.config.ts). Futurenet resets are handled gracefully, and per-protocol reports live underdocs/futurenet-compatibility-reports/. No contract or SDK surface change. - 2026-10-24 — Issue #111 (Phase E4): added the audit-readiness material for external
reviewers.
SECURITY.mddocuments private reporting, triage and disclosure;docs/audit/records the reviewed commit, the in- and out-of-scope files, and reproducible test/build results;CONTRIBUTING.mdexplains how contributors report privately and respect the reviewed scope; the threat model gains a clearly-labelled planned-features section (T12) and states that milestones, disputes, splits and attestations are not implemented yet. No contract, SDK or CI change. - 2026-10-24 — Issue #50 (Phase E5): documented the optional M-of-N custom releaser account in
ARCHITECTURE_ESSENTIALS.md,docs/threat-model.md(T1) andROADMAP.md. The account contract already onmain(contracts/releaser-account/, ADR 0005) is unchanged; this PR resolves the duplicate add/add conflicts by keepingmain's newer implementation. No escrow contract change, so generated bindings are unaffected. Not deployed to testnet. - 2026-10-24 — Issue #84 (Phase E6): SDK
topUp,acceptandcreateEscrow({ requiresAcceptance });cancelnow takes anyEscrowSigner. All three check preconditions client-side first and throwEscrowPreconditionError(InvalidAmount,NotFunded,Unauthorized,AcceptanceNotConfigured,AlreadyAccepted); passcheckPreconditions: falseto skip the read. NewprepareLifecycleCall/signLifecycleCall/submitLifecycleCallpass a call between signers as JSON.getEscrownow returnsrequiresAcceptanceandacceptedAt. No contract change, bindings unchanged.@docforum/escrow-sdk0.7.0 → 0.8.0.extendRefundAfterandrefundByPayeefollow once #41 and #40 land. - 2026-10-24 — Issue #89 (Phase E7): added
scripts/deploy, one command for deploy → verify → record. It builds or takes the WASM, checks its SHA-256, uploads and deploys it with the constructor allowlist, confirms the on-chain WASM matches, runs a scripted live verification (create, release, refund; stranger release, double release and zero-amount rejections), and records the result indeployments.json,docs/testnet-deployments.mdandsdk/src/deployments.generated.ts. It refuses a dirty git tree and any network except testnet;--dry-runruns against a local quickstart without touching tracked files. The SDK addsknownContractId,knownNetworkConfig,latestDeploymentandparseDeploymentswith no filesystem access, so it still runs in browsers, Deno and Bun. No contract change. - 2026-10-24 — Issue #98 (Phase E7): added a Soroban resource profile for the 13 functions
in the resource baseline. A dedicated status record reduces
get_statusCPU instructions by 41% and memory by 52% in the measured native test environment, while increasing create and settlement costs due to the extra persistent write. MovingNextIdto persistent storage was rejected because it raised create costs further. The status key changes storage layout and requires redeployment under a new testnet contract ID per ADR 0005. - 2026-10-24 — Refreshed the TypeScript SDK's lockfile to clear high-severity advisories in its dependency tree:
@stellar/stellar-sdk16.3.0 → 16.4.1 (which pins axios 1.18.0 → 1.20.0, fixing the prototype-pollution, header-injection, ReDoS and SSRF advisories),follow-redirects1.16.0 → 1.16.1 andsmol-toml1.8.0 → 1.9.1. Lockfile-only and within the existing ranges, so consumers of the published package already resolve these versions. - 2026-10-26 — Issue #81 (Phase E6): added token-aware amount helpers to the SDK
(
sdk/src/amounts.ts).getTokenInfo(token)readsdecimals,symbolandnamefrom the SEP-41 token contract and caches them per address;parseAmount('12.5', decimals)andformatAmount(bigint, decimals)convert with strict validation — floats/exponents (1e3), excess fractional precision (0.00000001at 7 decimals), thousands separators and negatives (including-0) are rejected rather than rounded. Property tests coverformat(parse(x)) == normalise(x)and run in CI vianpm run test:unit. Additive SDK surface, no contract change, so the generated bindings are unchanged. The web demo now shows amounts viaformatAmount. - 2026-10-26 — Issue #99 (Phase E7): added a cargo-fuzz lifecycle target for randomized create, release, refund and ledger-time sequences, including malicious token mocks, with invariant checks after each operation. CI runs short pull-request fuzzing and a longer nightly pass, and uploads crash artifacts. No contract or SDK behavior changed.
- 2026-10-27 — The contract now exposes its semantic version and embeds the source revision and locked Soroban SDK version in WASM metadata. Reproducible builds use an explicit CI SHA and mark local builds unknown.
- 2026-10-27 — Issue #67 (Phase E6): added SDK network presets (
networks.testnet,networks.futurenet,networks.local) carrying an RPC URL, network passphrase and known contract ids, plus an explicit mainnet opt-in guard: constructing anEscrowClientwith the mainnet passphrase throwsMainnetNotAllowedErrorunlessallowMainnet: true. When a preset is selected (orverifyNetwork: trueis passed) the client verifies the RPC'sgetNetworkpassphrase lazily, once, before its first network call, and throwsNetworkMismatchErroron a mismatch; it never runs in the constructor, the result is cached per client and a failed check is not cached. With a preset only, an RPC withoutgetNetwork(an offline test double) is skipped; an explicit request makes that an error, andverifyNetwork: falseturns the check off. Clients without a preset make no extra requests. Custom networks still work, and plain-HTTP RPC (local quickstart) needsallowHttp, whichnetworks.localsets. No contract change, so the generated bindings are unchanged. - 2026-10-27 — Issue #94 (Phase E4): added
docs/runbooks/releaser-compromise.md(detection signals, decision tree, roles, race-to-settle steps), linked from threat model T1, andscripts/settle-outcomes.py. The script validates the integrator's outcome file, never chooses an outcome, runs the operator's own submit command per escrow, and records each successful transaction hash in a JSON-lines state file (<outcomes>.state.jsonl, rewritten atomically) so a re-run after a crash, rate limit or fee spike skips settled escrows and retries the rest. Stdlib only, Python 3.9+, with offline tests inscripts/test_settle_outcomes.pyrun by a new CI job. The 50+ escrow testnet drill is still to be run and recorded in the runbook. No contract or SDK change. - 2026-10-27 — Issue #29 (Phase E5): batch release and refund.
settle_manynow takesops: Vec<(escrow_id, SettleAction)>(Release|Refund) instead of release-onlyescrow_ids, andcallerauthorizes the batch once (the old version calledreleaseper id, so any batch of two or more failedError(Auth, ExistingValue)). Each op runs exactly the checks ofrelease/refundagainst that escrow's own releaser set, shared throughrelease_checks/refund_checksso the paths can't diverge. Best-effort, decided indocs/adr/0012: one result per op in input order,Errmeans untouched, the same escrow twice givesNotFundedon the second op, and a failure after an op's transfer (a failing payee callback) reverts the whole batch. BoundMAX_SETTLE_OPS= 20, measured: 656 bytes of events per release against the 16,384-byte events budget (13,120 at 20). Existing error codes unchanged (BatchTooLarge= 25 reused). T1 response guidance indocs/threat-model.md. 13 tests intests/settle_many.rs. Bindings regenerated (SDK patch). Not yet deployed to testnet. - 2026-10-27 — Issue #30 (Phase E5): optional protocol fee taken at release.
create_escrow(andcreate_escrow_release_time) accept an optionalfee_recipient/fee_bpspair, fixed at creation, capped at 10 % and floor-rounded;releasepays that share tofee_recipientand the exact remainder to the payee, while refunds still return the full amount.get_escrowexposes the pair, theESCROWevent snapshot appends it as one nested pair, andtop_upre-checks the fee math as the amount grows. Decided indocs/adr/0010; new invariant I7 in the threat model; new append-only errorInvalidFee = 35. The SDK gainsfeeRecipient/feeBpsoncreateEscrow(minor changeset) and regenerated bindings. Not yet redeployed to testnet. - 2026-10-27 — Issue #39 (Phase E5): per-token liability accounting and an on-chain
solvency view. New read-only
solvency(token) -> (balance, liabilities);liabilitiesis the sum ofamountover everyFundedescrow in that token, kept in a new persistentDataKey::Liability(token)entry (appended last, so existing storage encodings are unchanged) whose TTL is re-extended on every write. The counter is increased intransfer_in_exact(the single inbound path for every creation entry point,top_upandcontribute) and decreased insettle()andclaim_refund, always in the same invocation as the state change.tests/liabilities.rs: 14 tests, including a proptest that runs random sequences over every state-changing entry point and checks, after each step, that liabilities equal the per-token sum ofFundedamounts and the contract balance. Monitoring guidance indocs/threat-model.md. Existing test snapshots change only by the newLiabilitystorage entry. SDK:client.getSolvency(token)and regenerated bindings (minor). Not yet deployed to testnet. - 2026-10-27 — The SDK now publishes ESM and CommonJS builds with matching declarations and a browser-compatible ESM entry. CI smoke checks exercise Node imports and a browser bundle.
- 2026-10-27 — Phase E6 (issue #247): added multisig releaser setup helpers to the SDK.
buildMultisigSetupreturns one atomicsetOptionstransaction (thresholds, one operation per signer, master key weight zeroed last) and refuses any configuration that would lock the account out;dryRunMultisigSetupandverifyMultisigcover review and on-chain verification. Client-side only — no contract change, so the generated bindings are unchanged. - 2026-10-28 — Issue #55 (Phase E5):
releasenow usestry_transferfor the payout. If the token rejects the transfer (for example a SAC payee whose trustline was deauthorised, or a contract payee that rejects the funds), the call returns the new append-onlyError::Undeliverable = 35and the escrow staysFundedwith its balance intact, so the releaser can retry once the payee is reachable or use the refund path. Refund transfers are unchanged. Tests incontracts/escrow/tests/undeliverable.rs; generated SDK bindings regenerated for the new error code. - 2026-10-28 — Contract changes now use fragments in
contract-changes.d/instead of each PR bumpingCONTRACT_VERSION, which put every open contract PR into conflict on each contract merge. NewErrorvariants use provisional codes (issue number × 100 + n) until the maintainer fold (scripts/fold-contract-changes.mjs) bumps the version once per batch and assigns final codes. CI's "Contract changes use a fragment, not a version bump" step (scripts/check-contract-change.mjs) replaces the old "Require a contract version bump" step. The version test now compares withCONTRACT_VERSIONinstead of a literal. - 2026-10-28 — Issue #40 (Phase E5): the stored payee can now voluntarily refund a funded escrow with
refund(escrow_id, payee). The payout still goes only to the storedrefund_to(ADR 0009), and the call emits a distinctpayee_refundedevent (ADR 0010). Pooled escrows reject it throughreject_pooled. Bindings regenerated, and the TLA+ model gained aPayeeRefundaction. - 2026-10-28 — Issue #45 (Phase E5): added
transfer_payee(escrow_id, current_payee, new_payee)— restricted to the current payee (require_auth), only while Funded;new_payeemay not be the contract address or the payer. Rotation tool, not key-loss recovery. 12 tests incontracts/escrow/tests/transfer_payee.rs. New errorError::InvalidNewPayee(code 37). Contract surface updated inARCHITECTURE_ESSENTIALS.md; threat model I1 updated and T11 added.CONTRACT_VERSIONis left for the maintainer fold, percontract-changes.d/README.md. - 2026-10-28 — Issue #9 (Phase E4, T7): cover the contract's full-state lifecycle events on create, release, refund, and cancel, keyed by escrow_id. Error paths emit nothing. Added contract tests in
contracts/escrow/tests/events.rs, documented the indexer-compatible payload inARCHITECTURE_ESSENTIALS.md § Events, and marked T7 mitigated indocs/threat-model.md. - 2026-10-30 — Contract 2.4.0 (was 2.3.0), folded from 2 change(s):
- Issue #40: allow the stored payee to authorize a voluntary full refund of a funded single-payee escrow to its stored payer destination, and emit a distinct
payee_refundedevent. - Issue #45 (Phase E5): new
transfer_payee(escrow_id, current_payee, new_payee)entry point — only the current payee may authorize it, only while the escrow isFunded, andnew_payeemay be neither the contract address nor the payer. Emits apay_xferevent and addsError::InvalidNewPayee(code 37). Rotation tool, not key-loss recovery; covered bycontracts/escrow/tests/transfer_payee.rs. Final error codes: Error::InvalidNewPayee 4500 -> 37.
- Issue #40: allow the stored payee to authorize a voluntary full refund of a funded single-payee escrow to its stored payer destination, and emit a distinct
- 2026-10-31 — Issue #35 (Phase E5): deposit-only emergency pause. The contract's
__constructornow also takes apause_adminaddress; only that custodian can callpause/unpause, and while paused every deposit path fails withPausedbefore any funds move — including pooledcreate_pooled_escrowandcontribute(Paused, plusAlreadyPaused/NotPausedfor the transition guards — codes 38–40).release,refund, the timeout refund and pooledclaim_refundnever read the flag, so a pause can stop new exposure but never lock escrowed funds (invariant I8, threat model T12,docs/adr/0011). The SDK gainspause/unpause/isPaused/getPauseAdmin(minor changeset) and regenerated bindings. This is a deployment change: the live testnet contract predates the constructor and cannot pause until it is redeployed. - 2026-10-31 — Contract 2.5.0 (was 2.4.0), folded from 1 change(s):
- Issue #35 (Phase E5): deposit-only emergency pause (ADR 0011). The constructor gains a second argument (
pause_admin) and the contract gainspause/unpause/is_paused/get_pause_adminentry points plus thePaused/Unpausedevents and thePaused,AlreadyPaused,NotPausederrors (codes 38–40). Every deposit path is gated —create_escrowand its variants,top_up, and pooledcreate_pooled_escrow/contribute— whilerelease,refund, the timeout refund, pooledclaim_refundand bothsettle_manyops never read the flag, so a pause can never lock funds already escrowed. Final error codes: Error::Paused 3500 -> 38, Error::AlreadyPaused 3501 -> 39, Error::NotPaused 3502 -> 40.
- Issue #35 (Phase E5): deposit-only emergency pause (ADR 0011). The constructor gains a second argument (
- 2026-11-01 — Contract 2.5.1 (was 2.5.0), folded from 1 change(s):
create_escrowsnow works for more than one item:payerauthorizes once for the whole batch instead of once per item, which aborted withError(Auth, ExistingValue).MAX_BATCH_SIZEdrops from 50 to 16, because 24 items already exceed Mainnet's 16,384-byte contract events limit (docs/adr/0012).
- 2026-11-01 —
create_escrowsnow works for batches of more than one item, and stays atomic.MAX_BATCH_SIZEdrops from 50 to 16 in the contract and the SDK, because 24 items exceed Mainnet's contract events limit. See the amended consequences indocs/adr/0012-batch-settlement-best-effort.md. - 2026-11-01 — Added contract tests for the five mutants the nightly mutation run missed: the
create_escrowsbatch limit, a pooledcondition_refof exactly 64 bytes, no fee transfer when the fee is zero, andsettle_manyat exactlyrelease_time. Tests only, so no contract change. - 2026-11-01 — Restored the SDK's batch helpers (
createEscrows,settleMany,BatchPartialFailureError,MAX_BATCH_SIZEand the related types), which #248 removed by accident when it merged from a stale branch, and puttests/batch.test.tsback intest:offlineso CI catches this if it happens again. No contract change, so the generated bindings are unchanged. - 2026-11-01 — Nightly soak: failures are now classified correctly. The old check matched bare
words such as "network", which
tests/network-presets.test.tsalways printed, so every failure was reported asINFRASTRUCTURE_FAILURE. Added aDEPLOYMENT_OUTDATEDresult for a contract whose function arguments no longer match the SDK. The live suite now reads its contract id fromdeployments.jsonthroughknownContractId("testnet"), or fromTESTNET_CONTRACT_ID. - 2026-11-03 — Redeployed contract 2.5.1 to testnet as
CAANBNO7UXU24RVORQSGV33VJ7ZPL4UQKQNGPOSCSDDLISQ35SH62Y6Ewithscripts/deploy(source52a6a39), so the deployment now matches the SDK'screate_escrowand has the emergency pause. All six scripted live checks passed;pause_adminisGDJBRSDH4OARYUYDUDUXYTELZQMIJVZBLL4H2CDQFOHMNKNGE4ILUM7T. The SDK testnet preset, README andfees.test.tsnow use the new id. Exercisingpause/unpauselive, including a release while paused, is still open under Phase E5. docforum-core stays on its own older contract and SDK 0.2.0 for now. - 2026-11-04 — Issue #103 (Phase E4): added
tests/reentry_intermediate.rs, which proves re-entry through an intermediate contract fails closed forrelease,refundandcreate_escrow, including against a different escrow id. No contract change. The threat model's T4 section now records the result, replacing a coverage claim that had no test behind it. - 2026-11-04 — Issue #52 (Phase E5): the SDK can now read the contract's version on first use and check it against
>=2.0.0 <3.0.0, the range that has thecreate_escrowarguments it sends. Older deployments with noversion()are reported asnulland handled without an opaque error.docs/testnet-deployments.mdnow records each deployment's version. - 2026-11-05 — Raised the time limit for
sdk/tests/testnet-integration.test.tsfrom 60 to 120 seconds, because the longest live tests were timing out on a slow testnet and failing the nightly soak. Other SDK tests keep their 60-second limit. - 2026-11-11 — Fixed the weekly Futurenet compatibility check, which had failed since the contract gained a constructor: its deploy step now passes
allowed_tokens(empty, so any token is allowed) andpause_admin(the run's own funded identity).