Repository navigation
feat(contract): payee rights transfer: payee reassigns future payout to a new address - #199
Conversation
…to a new address Closes DocForum#45 - Add transfer_payee(escrow_id, current_payee, new_payee) restricted to the current payee (require_auth), only while Funded. - Reject contract address and payer as new_payee (InvalidNewPayee = 8). - Emit pay_xfer event with (escrow_id, old_payee, new_payee). - 12 tests in contracts/escrow/tests/transfer_payee.rs covering: happy path, release/refund after transfer, chained transfers, auth rejection (stranger/payer/releaser), both rejected targets, and state checks (after release, after refund, unknown id). - Threat model: I1 updated to document transfer_payee scope, payee actor updated (new on-chain power), T11 added (payee key surface). - ROADMAP.md Phase E5 item added, changelog entry dated 2026-10-19. - ARCHITECTURE_ESSENTIALS.md and README.md contract surface updated. Design note: this is a rotation tool. A payee who has lost their key cannot use it — they cannot sign current_payee.require_auth(). Key loss remains an off-chain operational risk. Documented in T11.
|
@Joyful12-tech Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
|
@Joyful12-tech CI was green on this PR, but it now has merge conflicts with |
|
@Joyful12-tech This PR now has merge conflicts with git fetch upstream # upstream = https://github.com/DocForum/docforum-escrow.git
git rebase upstream/main
# resolve the conflicts, re-run the tests, then:
git push --force-with-leaseThe usual culprits are shared files like Automated message. It won't repeat unless the PR conflicts again after your next push. |
|
@Joyful12-tech CI is failing on this branch against the latest Soroban contract (test + wasm build) SDK bindings match contract WASM A Rust source file has an unclosed This PR also conflicts with |
|
@Joyful12-tech CI is failing on this branch against the latest Soroban contract (test + wasm build) Reproducible WASM across checkout paths, SDK bindings match contract WASM
|
|
@Joyful12-tech This PR now has merge conflicts with git fetch upstream # upstream = https://github.com/DocForum/docforum-escrow.git
git rebase upstream/main
# resolve the conflicts, re-run the tests, then:
git push --force-with-leaseThe usual culprits are shared docs like Automated message. It won't repeat unless the PR conflicts again after your next push. |
|
@Joyful12-tech This PR now has merge conflicts with git fetch upstream # upstream = https://github.com/DocForum/docforum-escrow.git
git rebase upstream/main
# resolve the conflicts, re-run the tests, then:
git push --force-with-leaseThe usual culprits are shared docs like Automated message. It won't repeat unless the PR conflicts again after your next push. |
|
@Joyful12-tech CI is failing on this branch against the latest Reproducible WASM across checkout paths, SDK bindings match contract WASM Soroban contract (test + wasm build)
This PR also conflicts with |
…uture Resolve conflicts and repair the botched conflict resolutions this branch carried from earlier merges: - lib.rs: keep both transfer_payee (this branch) and the pooled-escrow helpers (main). Remove the duplicate DisallowedToken enum variant this branch had re-introduced (=7 alongside main's =8) and give InvalidNewPayee the next free discriminant, 35. Add the missing closing brace of transfer_payee. - docs/threat-model.md: drop four generations of duplicated I1-I4 invariant rows this branch had accumulated; take main's latest rows and add the transfer_payee sentence to I1. Deduplicate the actor table and fold the payee-transfer capability into main's payee row. Renumber this branch's payee-key threat from T11 (taken by main) to T20; fix its error-code reference to 35. - ARCHITECTURE_ESSENTIALS.md / README.md / ROADMAP.md: take main's versions and re-apply only the genuine issue DocForum#45 additions (the surface line, the summary mention, the roadmap item and changelog entry) instead of the stale pre-main blocks this branch had resurrected.
Three CI jobs failed on this branch because the feature was written against a pre-merge main and never re-integrated: - Regenerate sdk/src/generated/contract-client.ts from the merged contract WASM with the pinned stellar CLI 28.1.0: the contract gained transfer_payee and error 35 (InvalidNewPayee), so the committed bindings no longer matched (issue DocForum#121 CI job). - Update the create_escrow call in tests/transfer_payee.rs to the current signature (notify_payee + refund_to arguments). - Commit the 12 Soroban test snapshots for the transfer_payee tests, which were never added (issue DocForum#120 snapshot-drift CI job). Verified locally: cargo test (all suites incl. proptest model with PROPTEST_CASES=64), snapshot drift clean, wasm build + artifact, releaser-account tests and wasm, byte-identical rebuild via check-wasm-reproducibility.sh, check-generated-bindings.sh OK, resource baseline OK, and sdk typecheck/build/offline/milestones/ unit/runtime suites all pass.
|
fix CI failure |
|
@Joyful12-tech CI is failing. The Soroban contract job fails at "Fail if committed test snapshots changed": Run Please fix and push. |
…ransfer-payee-reassigns-future
…tatus entry After merging main, cargo test writes the cached ["Status", id] ledger entry (status-read optimisation) into the transfer_payee test snapshots, which were generated before that change landed. All 12 files gain exactly one entry, matching main's own snapshots.
|
@Joyful12-tech CI still fails at the "Require a contract version bump when lib.rs changes" step: This PR changes |
CI (from DocForum#153) requires a CONTRACT_VERSION bump whenever lib.rs changes against the base branch. transfer_payee is a new public function, so bump the minor version and update the version_reports_the_contract_semver assertion to match.
|
@Joyful12-tech This PR now has merge conflicts with git fetch upstream # upstream = https://github.com/DocForum/docforum-escrow.git
git rebase upstream/main
# resolve the conflicts, re-run the tests, then:
git push --force-with-leaseThe usual culprits are shared docs like Automated message. It won't repeat unless the PR conflicts again after your next push. |
|
@Joyful12-tech This PR is behind |
|
@Joyful12-tech This PR now has merge conflicts with git fetch upstream # upstream = https://github.com/DocForum/docforum-escrow.git
git rebase upstream/main
# resolve the conflicts, re-run the tests, then:
git push --force-with-leaseThe usual culprits are shared docs like Automated message. It won't repeat unless the PR conflicts again after your next push. |
* fix(contract): keep failed releases recoverable * fix(contract): reserve unique undeliverable error code * fix(contract): reserve unique undeliverable error code * test: enable revocation in undeliverable SAC fixture * test(contract): refresh undeliverable payee snapshots The status-read optimisation on main keeps a separate DataKey::Status entry next to the escrow record. Regenerate the two undeliverable payee snapshots so they include it: Funded after the rejected release, and Released after the retry. * docs(roadmap): record undeliverable payee handling (DocForum#55) * test(releaser-account): refresh snapshot for status-read optimisation Picked up from merging main's status-read change (DocForum#260): releaser-account's own fixture now also has a separate DataKey::Status entry. Not touched by this PR's change; regenerated by running its test suite. --------- Co-authored-by: Ezekiel146 <320391431+Ezekiel146@users.noreply.github.com> Co-authored-by: Pheelz <afeezorobsco@gmail.com>
|
@Joyful12-tech The latest merge from Both sides' lines were kept instead of resolved. Keep one |
…ps (DocForum#268) Nine open PRs each bumping CONTRACT_VERSION meant every contract merge put all the others into conflict, and each picked the same 'next' version. - contract-changes.d/: a PR that changes lib.rs adds a fragment (bump level plus summary) and leaves CONTRACT_VERSION alone. New Error variants use a provisional code (issue x 100 + n) so two PRs can't pick the same number. - scripts/check-contract-change.mjs replaces the 'Require a contract version bump' CI step: fragment required, no version edits, error codes append-only, new codes provisional. A fold PR (deletes fragments) may bump and renumber. - scripts/fold-contract-changes.mjs: maintainer fold. Bumps the version once by the largest level, assigns final error codes, writes a changelog.d entry, deletes the fragments. - version test compares with CONTRACT_VERSION instead of a literal. - AGENTS.md and CONTRIBUTING.md document the rule. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
@Joyful12-tech This PR now has merge conflicts with git fetch upstream # upstream = https://github.com/DocForum/docforum-escrow.git
git rebase upstream/main
# resolve the conflicts, re-run the tests, then:
git push --force-with-leaseThe usual culprits are shared docs like Automated message. It won't repeat unless the PR conflicts again after your next push. |
|
@Joyful12-tech Heads-up: #268 just changed how contract PRs work in this repo. Your PR changes
Correction to my earlier comment here: I pointed you at This should also make your next merge from |
…m#269) Ran scripts/fold-changelog.mjs during the 2026-10-28 PR sweep. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…release destination to a new address Only the current payee may authorise this; 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. Adds contract-changes.d/45-payee-rights-transfer.md (bump: minor) instead of editing CONTRACT_VERSION, per contract-changes.d/README.md, and gives Error::InvalidNewPayee the provisional code 4500 (issue DocForum#45 x 100) for the maintainer fold to renumber. Regenerates the SDK bindings from the rebuilt WASM and adds changelog.d/45-payee-rights-transfer.md for issue DocForum#45. Co-authored-by: Codebuff <noreply@codebuff.com>
The branch was rebased onto upstream/main (contract-change fragment instead of a CONTRACT_VERSION bump, InvalidNewPayee on provisional code 4500), which rewrote its history. Adding the previous remote tip as a second parent keeps that history reachable so the update can be pushed without a force push, while the tree stays the rebased content: upstream/main plus the transfer_payee change only. The superseded side carried an older copy of this change (a garbled README paragraph, an edit to the maintainer-owned ROADMAP status summary, and a CONTRACT_VERSION bump), all of which the rebased content replaces. Co-Authored-By: Codebuff <noreply@codebuff.com>
Malware scan: passed, with notes for the reviewerNothing malicious was detected in
|
The last merge of main (DocForum#169/DocForum#199) left the branch uncompilable and its tests failing: - `Error` lost `Paused`, `AlreadyPaused` and `NotPaused` while `lib.rs` still returns them (E0599). Re-added with the provisional codes 3500-3502 that contract-changes.d/README.md asks for, since main already spent 35 (`InvalidFee`) and 36 (`Undeliverable`); the maintainer fold assigns the final numbers. README, ADR 0011, ROADMAP, the threat model and the changelog fragment no longer claim 36-38. - `use soroban_sdk::{...}` was spliced into itself, so every imported name was defined twice (E0252). - main's newer `tests/transfer_payee.rs` and `tests/undeliverable.rs` still registered the contract with the old one-argument constructor, so all 14 of their tests panicked ("invalid number of input arguments: 2 expected, got 1"). They now pass a pause admin and their snapshots are regenerated. - Added the required `contract-changes.d/35-emergency-pause.md` fragment (`bump: minor`); `CONTRACT_VERSION` stays at main's 2.3.0, as the new CI rule requires. `cargo test` passes for every suite in contracts/escrow (pause 23, create_escrow 7, release_refund 8, timeout_refund 17, top_up 3, cancel 18, payee_acceptance 3, pooled 19, settle_many 13, liabilities 14, protocol_fee 17, quorum 19, transfer_payee 12, undeliverable 2, token_allowlist 2, token_edge_cases 1, malicious_token 7, sponsored_refund 16, releaser_rotation 3, release_callback 6, time_locked_release 5, model 4). Generated with Codebuff 🤖 Co-Authored-By: Codebuff <noreply@codebuff.com>
* feat(contract): add a deposit-only emergency pause (ADR 0005) There was no on-chain way to slow down a live bug: the only defence was telling integrators to stop. This adds a narrow lever instead — a single custodian set by the contract's constructor can block `create_escrow` and nothing else, so new exposure stops while every existing escrow can still be released, refunded, or timeout-refunded. Blocking outbound paths would have turned a possible loss into a certain lock, which is why the pause deliberately does not read the flag there (and the 16 new tests prove it). Records the decision in ADR 0005, adds the role to the threat model (T11, invariant I6), and regenerates/bumps the SDK to 0.4.0 for the new pause/unpause/is_paused/get_pause_admin surface. Closes #35 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> * feat(contract): add a deposit-only emergency pause (ADR 0011) Adds a constructor-set custodian that can pause every deposit path (create_escrow and its variants, plus top_up) and nothing else, so new exposure stops while existing escrows can still be released, refunded or timeout-refunded. Pause/unpause are custodian-only, non-idempotent, and emit Paused/Unpaused events; is_paused/get_pause_admin are read-only. Rebased onto current main: the constructor is merged with the existing token allowlist (__constructor(allowed_tokens, pause_admin)), top_up is gated as the ADR requires of any deposit path, error codes are 26-28 to avoid colliding with existing codes, and the ADR is renumbered to 0011 so it does not clash with the protocol-fee ADR. * fix: reconcile emergency pause with pooled escrows from main The last merge of main (#257 pooled escrows, #193/#248 SDK work) left the branch broken: - contract: `DataKey::Pooled`/`Contribution` had been spliced into the `Unpaused` event struct; restore them to `DataKey`. - contract: `Paused`/`AlreadyPaused`/`NotPaused` collided with `NotPooled`/`FundingDeadlinePassed`/`FundingTargetReached` (26-28). Main's codes keep their on-chain values; the pause codes are appended as 35/36/37 (docs, ADR 0011, README, threat model, ROADMAP, changelog updated). - contract: gate the new pooled deposit paths (`create_pooled_escrow`, `contribute`) on the pause; `claim_refund` and `release` stay ungated. Three new tests in tests/pause.rs cover this. - tests: main's new tests (cancel, pooled, model, malicious_token) registered the contract with the old one-argument constructor; pass a pause admin. Snapshots regenerated with `cargo test`. - sdk: regenerate src/generated/contract-client.ts with the pinned stellar CLI 28.1.0 (it contained a broken hand merge); close the unterminated `getPauseAdmin` method in src/index.ts; drop the duplicate `test:unit` script key in package.json. - ci: the Go SDK local-network deploy passes `--pause_admin`. * ci(sdk-go): pin stellar/quickstart to the digest main last passed with The 2026-10-26 quickstart:latest (core/RPC v30) fails the pinned Stellar CLI 28.1.0's first simulation, `stellar contract asset deploy --asset native`, with HostError: Error(Context, InternalError). It fails the same way on unrelated PRs, before the escrow contract (and its constructor args, which match __constructor(allowed_tokens, pause_admin)) is touched. Pin to the digest from main's last green run (39607326690). * test(fuzz): seed lifecycle corpus with the pause-admin constructor crash CI's cargo-fuzz crash (input AQEAAAgBAA==, [1, 1, 0, 0, 8, 1, 0]) was the harness registering the escrow with main's one-argument constructor; every input hit 'invalid number of input arguments: 2 expected, got 1'. The harness now passes (allowed_tokens, pause_admin). Keeping the input in the corpus replays it on every PR and nightly run. * chore: bump CONTRACT_VERSION to 2.1.0 Emergency pause is a backward-compatible feature on top of 2.0.0 (#209). Satisfies the CI rule requiring a CONTRACT_VERSION bump whenever contracts/escrow/src/lib.rs changes. * chore: bump CONTRACT_VERSION to 2.2.0 main is now at 2.1.0 (#242), so the PR head must move past it for the 'Require a contract version bump when lib.rs changes' CI check. * fix(sdk-go): send fee_recipient/fee_bps on create_escrow and create_escrow_release_time #209 added two optional trailing args (fee_recipient, fee_bps) to create_escrow and create_escrow_release_time. The Go SDK still sent the old argument count, so the integration job failed with Error(WasmVm, UnexpectedSize) / MismatchingParameterLen. Send None for both (no protocol fee) until the Go SDK exposes the fee; create_escrow_with_releasers is unchanged. * test(releaser-account): refresh snapshot for per-token liability entry (#242) * feat(contract): settle_many is never blocked by the emergency pause; bump to 2.3.0 Follow-up to the upstream main merge (#254). Per ADR 0011 the pause stops new deposits only and never blocks outbound paths, so settle_many's release and refund ops ignore the pause flag exactly like single release/refund (invariant I8). Documents this in lib.rs, README and the threat model, adds pause tests showing settle_many release, refund and a mixed batch all succeed while paused (with their snapshots), bumps CONTRACT_VERSION to 2.3.0 (main is 2.2.0) and applies cargo fmt. * fix(contract): repair the merge of main that broke the emergency pause The last merge of main (#169/#199) left the branch uncompilable and its tests failing: - `Error` lost `Paused`, `AlreadyPaused` and `NotPaused` while `lib.rs` still returns them (E0599). Re-added with the provisional codes 3500-3502 that contract-changes.d/README.md asks for, since main already spent 35 (`InvalidFee`) and 36 (`Undeliverable`); the maintainer fold assigns the final numbers. README, ADR 0011, ROADMAP, the threat model and the changelog fragment no longer claim 36-38. - `use soroban_sdk::{...}` was spliced into itself, so every imported name was defined twice (E0252). - main's newer `tests/transfer_payee.rs` and `tests/undeliverable.rs` still registered the contract with the old one-argument constructor, so all 14 of their tests panicked ("invalid number of input arguments: 2 expected, got 1"). They now pass a pause admin and their snapshots are regenerated. - Added the required `contract-changes.d/35-emergency-pause.md` fragment (`bump: minor`); `CONTRACT_VERSION` stays at main's 2.3.0, as the new CI rule requires. `cargo test` passes for every suite in contracts/escrow (pause 23, create_escrow 7, release_refund 8, timeout_refund 17, top_up 3, cancel 18, payee_acceptance 3, pooled 19, settle_many 13, liabilities 14, protocol_fee 17, quorum 19, transfer_payee 12, undeliverable 2, token_allowlist 2, token_edge_cases 1, malicious_token 7, sponsored_refund 16, releaser_rotation 3, release_callback 6, time_locked_release 5, model 4). Generated with Codebuff 🤖 Co-Authored-By: Codebuff <noreply@codebuff.com> * fix(sdk,contract): regenerate bindings and pass a pause admin in events.rs The merge of main (#238, lifecycle events) left two jobs failing: - `sdk/src/generated/contract-client.ts` was hand-mangled: the `/**` that opens the `transfer_payee` doc block was lost (TS1131), and the file predated the provisional pause error codes. It is now exactly what the pinned stellar CLI (28.1.0, scripts/stellar-cli-version) generates from the current contract WASM, so "SDK bindings match contract WASM" passes too. Never hand-edited. - main's `contracts/escrow/tests/events.rs` still registered the contract with the one-argument constructor, so its 13 tests panicked with `Error(WasmVm, InvalidAction)` ("2 expected, got 1"). All five `env.register` sites now pass a pause admin, and their snapshots are regenerated for the `PauseAdmin`/`Paused` instance entries. `cargo test` passes for every suite in contracts/escrow (events 13, pause 23, create_escrow 7, release_refund 8, timeout_refund 17, top_up 3, cancel 18, payee_acceptance 3, pooled 19, settle_many 13, liabilities 14, protocol_fee 17, quorum 19, transfer_payee 12, undeliverable 2, token_allowlist 2, token_edge_cases 1, malicious_token 7, sponsored_refund 16, releaser_rotation 3, release_callback 6, time_locked_release 5, model 4). Generated with Codebuff 🤖 Co-Authored-By: Codebuff <noreply@codebuff.com> * fix(sdk): regenerate bindings with the pinned Rust toolchain The bindings committed with the merge were generated from a WASM built with the sandbox's stable Rust (1.99), but the repo pins 1.96 in rust-toolchain.toml and CI builds with it. The compiler changes the order of the ContractSpec entries in the WASM, so the committed file had the PayeeRefunded event spec in a different slot than CI's regeneration and "SDK bindings match contract WASM" failed. Rebuilt the WASM with the pinned 1.96.0 toolchain and regenerated with stellar CLI 28.1.0 as scripts/check-generated-bindings.sh does, so the diff is only that one entry moving; the script now reports OK. Generated with Codebuff 🤖 Co-Authored-By: Codebuff <noreply@codebuff.com> --------- Co-authored-by: Codebuff Agent <noreply@codebuff.com> Co-authored-by: Azintar <Azintar@users.noreply.github.com> Co-authored-by: Azintar <342361479+Azintar@users.noreply.github.com> Co-authored-by: afeez <afeezorobsco@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Implements the Phase E5 payee rights transfer feature from issue #45.
Adds
transfer_payee(escrow_id, current_payee, new_payee)— lets thecurrent payee reassign the release destination to a new address while the
escrow is
Funded. Only the payee can authorise this; the payer andreleaser have no power to redirect funds via this path (invariant I1 is
preserved).
What changed
contracts/escrow/src/lib.rsError::InvalidNewPayee = 8: emitted whennew_payeeis thecontract address (funds would be permanently locked) or the payer
(would let a colluding payee redirect a release to the payer,
bypassing refund auth).
transfer_payee(escrow_id, current_payee, new_payee):current_payee.require_auth()current_payee == data.payee(Unauthorized if not)Funded(NotFunded if not)new_payee(InvalidNewPayee)pay_xfereventcontracts/escrow/tests/transfer_payee.rs(new, 12 tests)docs/threat-model.mdtransfer_payeeonly the payee can call, not payer/releaserROADMAP.mdtransfer_payeeitem added (closes [Phase E5] Payee rights transfer: payee reassigns future payout to a new address #45)ARCHITECTURE_ESSENTIALS.mdandREADME.mdtransfer_payeeentryDesign decisions
this — they cannot sign
current_payee.require_auth(). This isdocumented explicitly in the function's doc comment and in T11.
redirecting a release to the payer (turning it into a de-facto
payer refund that bypasses refund auth). Rejected with
InvalidNewPayee.sign to claim a payout, so funds would be locked permanently.
payout-claim reassignment, not an undo.
Test output
(CI will confirm this — Rust toolchain not available locally in this environment.)
Closes #45