Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
019777e
feat(contract): payee rights transfer: payee reassigns future payout …
Joyful12-tech Oct 19, 2026
e20c968
Merge branch 'main' into feat/payee-rights-transfer-payee-reassigns-f…
Joyful12-tech Oct 20, 2026
ad749a4
Merge branch 'main' into feat/payee-rights-transfer-payee-reassigns-f…
Joyful12-tech Oct 21, 2026
205d8eb
Merge branch 'main' into feat/payee-rights-transfer-payee-reassigns-f…
Joyful12-tech Oct 21, 2026
7292742
Merge branch 'main' into feat/payee-rights-transfer-payee-reassigns-f…
Joyful12-tech Oct 23, 2026
9e9d407
Merge branch 'main' into feat/payee-rights-transfer-payee-reassigns-f…
Joyful12-tech Oct 24, 2026
2dd1868
fix(ci): repair transfer_payee integration after merging main
Joyful12-tech Oct 24, 2026
3232a5d
Merge branch 'main' into feat/payee-rights-transfer-payee-reassigns-f…
Joyful12-tech Oct 24, 2026
201bae7
Merge branch 'main' into feat/payee-rights-transfer-payee-reassigns-f…
Joyful12-tech Oct 26, 2026
f8e1655
Merge remote-tracking branch 'upstream/main' into feat/payee-rights-t…
Joyful12-tech Oct 27, 2026
fd5293d
test(snapshots): refresh transfer_payee snapshots for main's cached S…
Joyful12-tech Oct 27, 2026
dca9b03
feat(contract): bump CONTRACT_VERSION to 1.1.0 for transfer_payee
Joyful12-tech Oct 27, 2026
dfbadc7
Merge branch 'main' into feat/payee-rights-transfer-payee-reassigns-f…
Joyful12-tech Oct 27, 2026
9aea376
Merge branch 'main' into feat/payee-rights-transfer-payee-reassigns-f…
Joyful12-tech Oct 27, 2026
025814d
Merge branch 'main' into feat/payee-rights-transfer-payee-reassigns-f…
Joyful12-tech Oct 27, 2026
1df1206
fix(contract): keep failed releases recoverable (#154)
Ezekiel146 Oct 28, 2026
2fc41f8
ci(contract): contract-change fragments instead of per-PR version bum…
afeezorobsco-cyber Oct 28, 2026
7310cd9
chore(changelog): fold changelog.d fragments into ROADMAP.md (#269)
afeezorobsco-cyber Oct 28, 2026
2580628
feat(contract): payee rights transfer — transfer_payee reassigns the …
Joyful12-tech Oct 27, 2026
c2b6573
Merge the branch's existing remote history into the rebased head
Joyful12-tech Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 2 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,22 +75,10 @@ jobs:
with:
fetch-depth: 0

- name: Require a contract version bump when lib.rs changes
- name: Contract changes use a fragment, not a version bump
if: github.event_name == 'pull_request'
working-directory: .
run: |
if git diff --quiet "origin/${{ github.base_ref }}...HEAD" -- contracts/escrow/src/lib.rs; then
exit 0
fi

base_version=$(git show "origin/${{ github.base_ref }}:contracts/escrow/src/lib.rs" \
| sed -nE 's/^pub const CONTRACT_VERSION: &str = "([^"]+)";/\1/p')
head_version=$(sed -nE 's/^pub const CONTRACT_VERSION: &str = "([^"]+)";/\1/p' \
contracts/escrow/src/lib.rs)
if [ -z "$head_version" ] || { [ -n "$base_version" ] && [ "$base_version" = "$head_version" ]; }; then
echo "contracts/escrow/src/lib.rs changed without a CONTRACT_VERSION bump" >&2
exit 1
fi
run: node scripts/check-contract-change.mjs "origin/${{ github.base_ref }}"

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.96.0
Expand Down
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ the change belongs in `docforum-core` instead, not here.
- No PHI. No healthcare domain concepts (patient, doctor, referral, etc.)
anywhere in this repo — see `ARCHITECTURE_ESSENTIALS.md` hard rule 1.
- Contract changes need contract tests in the same PR, always.
- Never edit `CONTRACT_VERSION` or renumber an `Error` code. A PR that
changes `contracts/escrow/src/lib.rs` adds a fragment under
`contract-changes.d/`, and new `Error` variants use a provisional code
(issue number × 100 + n). A maintainer bumps the version and assigns final
codes. See `contract-changes.d/README.md`; CI enforces it.
- Testnet only, per `ARCHITECTURE_ESSENTIALS.md` hard rule 4, until an
explicit security-review milestone (ROADMAP.md Phase E4) is marked done.
- Update `ROADMAP.md` on every contribution — org-wide rule, applies here
Expand Down
2 changes: 2 additions & 0 deletions ARCHITECTURE_ESSENTIALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ refund(escrow_id, caller) -> result // IMPLEMENTED (Phase E2 + E4) — a rele
refund_with_approvals(escrow_id, approvers) -> result // IMPLEMENTED (Phase E5, issue #119) — same approval rules as release_with_approvals
settle_many(caller, ops: Vec<(escrow_id, Release|Refund)>) -> Vec<Result<(), Error>> // IMPLEMENTED (Phase E5, issue #29, ADR 0012) — caller authorizes once; each op gets exactly the checks of release/refund against that escrow's own releasers; best-effort, one result per op in input order (Err = untouched; same escrow twice → second NotFunded); a post-transfer failure (payee callback) reverts the whole batch; at most MAX_SETTLE_OPS = 20 (BatchTooLarge); not deployed on testnet yet
top_up(escrow_id, payer, amount) -> result // IMPLEMENTED (issue #26) — original payer only, funded state only; emits `top_up`
transfer_payee(escrow_id, current_payee, new_payee) -> result // IMPLEMENTED (Phase E5, issue #45) — current payee only, funded state only; new_payee may not be the contract address or the payer; rotation tool, not key-loss recovery
create_pooled_escrow(creator, payee, token, funding_target, condition_ref, releaser, funding_deadline) -> escrow_id // IMPLEMENTED (Phase E5, issue #113, ADR 0011) — pooled escrow; amount starts at 0, `funding_target` > 0 and a strictly-future `funding_deadline` are fixed at creation; not deployed on testnet yet
contribute(escrow_id, payer, amount) -> () // IMPLEMENTED (issue #113) — permissionless before the deadline, up to the target only; balance-delta verified (`transfer_in_exact`) and recorded per payer, accumulating across repeats
claim_refund(escrow_id, contributor) -> () // IMPLEMENTED (issue #113) — pull-based refund, contributor's own auth, only from a failed round (deadline passed, target unmet), pays exactly the recorded amount once (`AlreadyRefunded` after)
Expand Down Expand Up @@ -339,3 +340,4 @@ quickstart deployment.
## Roadmap / status
See `ROADMAP.md` in this repo. Live testnet deployment record:
`docs/testnet-deployments.md`.
A failed outbound token transfer during release returns `Undeliverable` and preserves the funded record, allowing the releaser to retry or use the refund path.
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ verification/release-engineering tracks.
entry as a new file in `changelog.d/`, in the same PR. Don't append to
the end of `ROADMAP.md`; that is what made every PR conflict. See
`changelog.d/README.md`.
- If you change the contract (`contracts/escrow/src/lib.rs`), add a fragment
under `contract-changes.d/` with the bump level and a one-line summary.
**Don't edit `CONTRACT_VERSION`**: a maintainer bumps it once per batch.
New `Error` variants use a provisional code (your issue number × 100 + n);
never renumber or remove an existing one. CI checks all three. See
`contract-changes.d/README.md`.
- Changes to escrow lifecycle semantics must also update the TLA+
specification under `spec/` and rerun TLC. That includes changes to
settlement auth, status transitions, timeouts, payee acceptance,
Expand Down
86 changes: 86 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ An opt-in creation method can require the payee to authorize `accept` before rel
- [x] 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 in `docs/threat-model.md`. **Not yet on testnet.**
- [x] Optional escrow-wide earliest release time (issue #43, ADR 0009); refund path remains independent.
- [x] Two-party releaser rotation (issue #23): current releaser and payer authorize; only funded single-releaser escrows can rotate. Contract and payee addresses are rejected.
- [x] Undeliverable payee handling (issue #55): a rejected outbound release transfer (deauthorised SAC trustline, rejecting contract payee) returns append-only `Error::Undeliverable = 35` and leaves the escrow `Funded`, so the releaser can retry or refund. Tests: `tests/undeliverable.rs`.

- [x] **OpenZeppelin Stellar library evaluation** (issue #118) —
`docs/adr/0006`: all six candidate modules checked against ADR 0001/0002
Expand Down Expand Up @@ -274,6 +275,25 @@ An opt-in creation method can require the payee to authorize `accept` before rel
(scenarios A–M) + 3 malicious-token contribution tests; existing
suites and snapshots unchanged. SDK `0.7.0 → 0.8.0` (additive).
**Not yet on testnet** — the live deployment predates it.

- [x] 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 is `Funded`. Only the
current payee may authorise this call — the payer and releaser have no
power to redirect funds via this path. `new_payee` may 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 a `pay_xfer` event. 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 in `contracts/escrow/tests/transfer_payee.rs` covering the
happy path, release/refund after transfer, chained transfers,
auth rejection for stranger/payer/releaser, and both rejected
`new_payee` targets. Rejected with `Error::InvalidNewPayee`.
Threat model
I1 updated; T11 added covering the new payee-key attack surface.
`ARCHITECTURE_ESSENTIALS.md` contract surface updated.
Closes issue #45.
## Phase E6 — SDK & integrator tooling
- [x] 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.
- [x] Automated npm release workflow (`.github/workflows/release-sdk.yml`),
Expand Down Expand Up @@ -928,3 +948,69 @@ An opt-in creation method can require the payee to authorize `accept` before rel
`scripts/test_settle_outcomes.py` run 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_many` now takes
`ops: Vec<(escrow_id, SettleAction)>` (`Release` | `Refund`) instead of
release-only `escrow_ids`, and `caller` authorizes the batch once (the
old version called `release` per id, so any batch of two or more failed
`Error(Auth, ExistingValue)`). Each op runs exactly the checks of
`release`/`refund` against that escrow's own releaser set, shared through
`release_checks`/`refund_checks` so the paths can't diverge. Best-effort,
decided in `docs/adr/0012`: one result per op in input order, `Err` means
untouched, the same escrow twice gives `NotFunded` on the second op, and
a failure after an op's transfer (a failing payee callback) reverts the
whole batch. Bound `MAX_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 in `docs/threat-model.md`. 13 tests in
`tests/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`
(and `create_escrow_release_time`) accept an optional `fee_recipient`/`fee_bps`
pair, fixed at creation, capped at 10 % and floor-rounded; `release` pays that
share to `fee_recipient` and the exact remainder to the payee, while refunds
still return the full amount. `get_escrow` exposes the pair, the `ESCROW`
event snapshot appends it as one nested pair, and `top_up` re-checks the fee
math as the amount grows. Decided in `docs/adr/0010`; new invariant I7 in the
threat model; new append-only error `InvalidFee = 35`. The SDK gains
`feeRecipient`/`feeBps` on `createEscrow` (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)`;
`liabilities` is the sum of `amount` over every `Funded` escrow in that
token, kept in a new persistent `DataKey::Liability(token)` entry
(appended last, so existing storage encodings are unchanged) whose TTL is
re-extended on every write. The counter is increased in
`transfer_in_exact` (the single inbound path for every creation entry
point, `top_up` and `contribute`) and decreased in `settle()` and
`claim_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 of `Funded` amounts and the
contract balance. Monitoring guidance in `docs/threat-model.md`. Existing
test snapshots change only by the new `Liability` storage 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.
`buildMultisigSetup` returns one atomic `setOptions` transaction (thresholds,
one operation per signer, master key weight zeroed last) and refuses any
configuration that would lock the account out; `dryRunMultisigSetup` and
`verifyMultisig` cover review and on-chain verification. Client-side only — no
contract change, so the generated bindings are unchanged.
- 2026-10-28 — Issue #55 (Phase E5): `release` now uses `try_transfer` for 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-only `Error::Undeliverable = 35` and the escrow stays `Funded` with
its balance intact, so the releaser can retry once the payee is reachable or
use the refund path. Refund transfers are unchanged. Tests in
`contracts/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 bumping `CONTRACT_VERSION`, which put every open contract PR into conflict
on each contract merge. New `Error` variants 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 with
`CONTRACT_VERSION` instead of a literal.
16 changes: 0 additions & 16 deletions changelog.d/29-batch-settlement.md

This file was deleted.

10 changes: 0 additions & 10 deletions changelog.d/30-protocol-fee-at-release.md

This file was deleted.

16 changes: 0 additions & 16 deletions changelog.d/39-per-token-liabilities.md

This file was deleted.

1 change: 1 addition & 0 deletions changelog.d/45-payee-rights-transfer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Issue #45 (Phase E5): added `transfer_payee(escrow_id, current_payee, new_payee)` — restricted to the current payee (require_auth), only while Funded; `new_payee` may not be the contract address or the payer. Rotation tool, not key-loss recovery. 12 tests in `contracts/escrow/tests/transfer_payee.rs`. New error `Error::InvalidNewPayee` (provisional code 4500, renumbered by the maintainer fold). Contract surface updated in `ARCHITECTURE_ESSENTIALS.md`; threat model I1 updated and T11 added. `CONTRACT_VERSION` is left for the maintainer fold, per `contract-changes.d/README.md`.
1 change: 0 additions & 1 deletion changelog.d/65-dual-sdk-builds.md

This file was deleted.

6 changes: 0 additions & 6 deletions changelog.d/77-multisig-releaser-helpers.md

This file was deleted.

8 changes: 8 additions & 0 deletions contract-changes.d/45-payee-rights-transfer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
bump: minor
---
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
is `Funded`, and `new_payee` may be neither the contract address nor the
payer. Emits a `pay_xfer` event and adds `Error::InvalidNewPayee` (provisional
code 4500, renumbered by the maintainer fold). Rotation tool, not key-loss
recovery; covered by `contracts/escrow/tests/transfer_payee.rs`.
Loading
Loading