Skip to content

feat(contract): pull-payment escrow — deposit(), per-member claim(), and claimable-balance accounting with TTL management #121

Description

@martinvibes

Context

distribute pushes tokens to every member inside one invocation. That means the payer funds N cross-contract transfer calls, one bad member address aborts the entire payroll run (a frozen or clawback-enabled trustline is enough), and the payer must be online with the full balance at exactly the right moment. Real payroll systems use pull payments.

Scope

  1. deposit(id, from, amount) — pull amount into the contract address in a single transfer, then credit each member's claimable balance using the same basis-point floor-plus-dust math as base::utils::distribute_amounts.
  2. claim(id, member)member.require_auth(), transfers the full accrued balance out, zeroes the entry. Add claim_to(id, member, to) for members paying out to a different address.
  3. Viewsclaimable_balance(id, member) -> i128 and total_escrowed(id) -> i128.
  4. Snapshot semantics — a deposit credits the member set as of that deposit. Later member changes must never retroactively move already-credited funds. Document it in rustdoc and prove it in a test: deposit, swap the whole member list, and confirm the original members can still claim their exact amounts.
  5. Invariant — for every group, the sum of all claimable balances equals total_escrowed, and the sum across groups never exceeds the contract's token balance. Expose an internal accounting helper the tests can assert on.
  6. Storage and archival — new keys DataKey::Claimable(BytesN<32>, Address) and DataKey::Escrowed(BytesN<32>). Every write must extend_ttl with named threshold/extend-to constants, because a member who claims eleven months later must not find an archived entry. Document the chosen TTL policy and its rent cost.
  7. distribute keeps working unchanged — escrow is additive, not a replacement.

Acceptance criteria

  • Claiming with a zero balance returns a new NothingToClaim error; double-claim returns the same on the second call.
  • Claim by a non-member returns MemberNotFound and moves nothing.
  • deposit on a group with no members returns EmptyMembers and does not take custody of the tokens.
  • Dust: after 100 deposits of an amount that divides unevenly across 7 members, sum(claimable) == sum(deposits) exactly — no leaked stroops.
  • Fuzz a random sequence of deposits and claims and assert the accounting invariant after every step.
  • TTL test: advance the ledger past the default persistent-entry TTL with env.ledger().with_mut(...), then claim successfully.
  • Reentrancy note in the PR: state must be zeroed before the token transfer, and a test must assert the write ordering.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third CampaigncontractSmart Contract (Soroban/Rust)functionsFunction implementationtestsTesting and test coverageutilsUtility functions

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions