You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
escrow and milestones are two entirely separate contract crates with entirely independent storage (contracts/escrow/src/types.rs vs contracts/milestones/src/types.rs — no shared DataKey namespace, no shared instance, no cross-contract call between them anywhere in either lib.rs). Both use a bare u64 issue_id as their unit of "which GitHub issue is this for" — escrow::fund(issue_id: u64, ...) (contracts/escrow/src/lib.rs:64-71) and milestones::allocate(milestone_id: u64, issue_id: u64, ...) (contracts/milestones/src/lib.rs:92) — with no shared registry, on-chain or otherwise, that either contract consults to check whether the other contract has already claimed the same issue_id.
Consequence: the exact same GitHub issue can be funded as a standalone bounty via escrow::fund(issue_id=555, ...)and simultaneously allocated a budget within some release milestone via milestones::allocate(milestone_id=X, issue_id=555, ...), with neither contract aware the other exists, let alone that both are backing the same underlying piece of work. Both can independently reach release/release_issue and pay out in full, for what is, off-chain, one single contribution being compensated twice by two different financial instruments on the same platform. Unlike the within-milestones double-allocation issue filed alongside this one (a single contract's storage-scoping gap), this is a system-level architecture gap: no amount of fixing either contract in isolation closes it, because the information needed to detect the collision — "is issue_id 555 already committed anywhere in the system?" — doesn't exist anywhere on-chain; it can currently only ever live in mergefi-backend's own, off-chain database, with the smart contracts providing zero defense-in-depth if that database is ever wrong, out of sync, or bypassed.
This directly touches the tradeoff the README's "Why three contracts instead of one" section explicitly discusses and accepts ("Independent upgrade/audit surface... you can fix and redeploy that contract without touching escrow funds that are mid-flight") without discussing this specific consequence of that independence: the same architectural choice that gives independent-upgrade benefits also means there is no natural place for a cross-contract "who has already claimed this issue" check to live, short of introducing genuine cross-contract calls between all three contracts (which the README elsewhere implies was deliberately avoided — "compute_split is duplicated... rather than shared via a common library crate," i.e. even code sharing was avoided in favor of independence, let alone live cross-contract calls).
Requirements
Evaluate architectural options and their tradeoffs explicitly:
A shared, cross-contract registry contract (a fourth, minimal contract whose only job is "claim issue_id X for contract Y," called by fund/allocate before proceeding) — closes the gap on-chain, but reintroduces cross-contract calls the current design has otherwise avoided, and creates a new single point of failure/upgrade coupling across all three contracts, in tension with the README's stated independence rationale.
A shared library crate exposing a common DataKey convention that each contract could, in principle, be extended to check against a common instance if one were designated as the "source of truth" — weaker (doesn't fully close the gap without also adding cross-contract calls) but lower-coupling.
Accept the gap on-chain and mitigate entirely at the backend layer, with the contracts' role limited to documenting the assumption plainly so it's a conscious, informed tradeoff rather than a silent one — likely the most consistent choice given the README's explicit preference for contract independence over shared coupling, but this should be a stated decision, not a default by omission.
Whichever direction is chosen, document it explicitly in the README's "Why three contracts instead of one" section, since that section currently makes the independence tradeoff sound costless and doesn't mention this consequence at all.
Acceptance Criteria
Architectural decision made and documented with real reasoning, in the README
If an on-chain registry is implemented: shared claim-checking mechanism added to both escrow::fund and milestones::allocate, with tests proving a cross-contract collision is now rejected
If the gap is accepted and pushed to the backend layer: the README's "Why three contracts" tradeoff section explicitly names this as a known, accepted limitation of the independent-contracts design, not left implicit
cargo test --workspace passes (if code changes are made)
Additional Notes
Confirmed via full read of both contracts/escrow/src/lib.rs and contracts/milestones/src/lib.rs: neither contains any Address/contract-id reference to the other, nor any cross-contract Env::invoke_contract call anywhere in either file — the two contracts are, today, entirely unaware of each other's existence at the code level.
Test sketch (if an on-chain fix is chosen): test_same_issue_id_rejected_across_escrow_and_milestones — this test would need to live somewhere that can construct both contract instances in one Env (neither existing test file does this today, since each is scoped to its own crate's #[cfg(test)] mod test) — likely requiring a new workspace-level integration test crate/directory that depends on both mergefi-escrow and mergefi-milestones, itself a small but real infrastructure addition worth calling out as part of this issue's scope.
Cross-references: the within-milestones double-funding issue filed alongside this one (the narrower, single-contract-scoped version of the same underlying concern — fixable independently and should probably land first, since it's self-contained, while this cross-contract issue is a larger, more architectural decision); README "Why three contracts instead of one" (the design rationale this issue's analysis directly extends); Extract compute_split into a shared crate with proof of behavioral equivalence #16 (compute_split shared-crate extraction — precedent for the "how much should these contracts share" question this issue also has to answer, just for a claim-registry instead of split-math logic).
Overview
escrowandmilestonesare two entirely separate contract crates with entirely independent storage (contracts/escrow/src/types.rsvscontracts/milestones/src/types.rs— no sharedDataKeynamespace, no shared instance, no cross-contract call between them anywhere in eitherlib.rs). Both use a bareu64 issue_idas their unit of "which GitHub issue is this for" —escrow::fund(issue_id: u64, ...)(contracts/escrow/src/lib.rs:64-71) andmilestones::allocate(milestone_id: u64, issue_id: u64, ...)(contracts/milestones/src/lib.rs:92) — with no shared registry, on-chain or otherwise, that either contract consults to check whether the other contract has already claimed the sameissue_id.Consequence: the exact same GitHub issue can be funded as a standalone bounty via
escrow::fund(issue_id=555, ...)and simultaneously allocated a budget within some release milestone viamilestones::allocate(milestone_id=X, issue_id=555, ...), with neither contract aware the other exists, let alone that both are backing the same underlying piece of work. Both can independently reachrelease/release_issueand pay out in full, for what is, off-chain, one single contribution being compensated twice by two different financial instruments on the same platform. Unlike the within-milestonesdouble-allocation issue filed alongside this one (a single contract's storage-scoping gap), this is a system-level architecture gap: no amount of fixing either contract in isolation closes it, because the information needed to detect the collision — "isissue_id555 already committed anywhere in the system?" — doesn't exist anywhere on-chain; it can currently only ever live inmergefi-backend's own, off-chain database, with the smart contracts providing zero defense-in-depth if that database is ever wrong, out of sync, or bypassed.This directly touches the tradeoff the README's "Why three contracts instead of one" section explicitly discusses and accepts ("Independent upgrade/audit surface... you can fix and redeploy that contract without touching escrow funds that are mid-flight") without discussing this specific consequence of that independence: the same architectural choice that gives independent-upgrade benefits also means there is no natural place for a cross-contract "who has already claimed this issue" check to live, short of introducing genuine cross-contract calls between all three contracts (which the README elsewhere implies was deliberately avoided — "compute_split is duplicated... rather than shared via a common library crate," i.e. even code sharing was avoided in favor of independence, let alone live cross-contract calls).
Requirements
issue_idX for contract Y," called byfund/allocatebefore proceeding) — closes the gap on-chain, but reintroduces cross-contract calls the current design has otherwise avoided, and creates a new single point of failure/upgrade coupling across all three contracts, in tension with the README's stated independence rationale.DataKeyconvention that each contract could, in principle, be extended to check against a common instance if one were designated as the "source of truth" — weaker (doesn't fully close the gap without also adding cross-contract calls) but lower-coupling.Acceptance Criteria
escrow::fundandmilestones::allocate, with tests proving a cross-contract collision is now rejectedcargo test --workspacepasses (if code changes are made)Additional Notes
contracts/escrow/src/lib.rsandcontracts/milestones/src/lib.rs: neither contains anyAddress/contract-id reference to the other, nor any cross-contractEnv::invoke_contractcall anywhere in either file — the two contracts are, today, entirely unaware of each other's existence at the code level.test_same_issue_id_rejected_across_escrow_and_milestones— this test would need to live somewhere that can construct both contract instances in oneEnv(neither existing test file does this today, since each is scoped to its own crate's#[cfg(test)] mod test) — likely requiring a new workspace-level integration test crate/directory that depends on bothmergefi-escrowandmergefi-milestones, itself a small but real infrastructure addition worth calling out as part of this issue's scope.milestonesdouble-funding issue filed alongside this one (the narrower, single-contract-scoped version of the same underlying concern — fixable independently and should probably land first, since it's self-contained, while this cross-contract issue is a larger, more architectural decision); README "Why three contracts instead of one" (the design rationale this issue's analysis directly extends); Extractcompute_splitinto a shared crate with proof of behavioral equivalence #16 (compute_split shared-crate extraction — precedent for the "how much should these contracts share" question this issue also has to answer, just for a claim-registry instead of split-math logic).