Skip to content

feat(escrow): enforce liability floor on terminal dust sweep#295

Open
Vvictor-commits wants to merge 1 commit into
Liquifact:mainfrom
Vvictor-commits:security/dust-sweep-liability-floor
Open

feat(escrow): enforce liability floor on terminal dust sweep#295
Vvictor-commits wants to merge 1 commit into
Liquifact:mainfrom
Vvictor-commits:security/dust-sweep-liability-floor

Conversation

@Vvictor-commits
Copy link
Copy Markdown

@Vvictor-commits Vvictor-commits commented May 28, 2026

Closes #267


Summary

Implements the on-chain invariant check: sweep_terminal_dust can no longer reduce the contract balance below outstanding investor liabilities in cancelled escrows.

Problem

In a cancelled escrow (status 4), investors reclaim principal via refund(). Nothing previously prevented the treasury from calling sweep_terminal_dust and draining tokens still owed to investors who hadn't refunded yet.

Also fixes: EscrowError enum, ensure, and fail helpers were accidentally removed from HEAD — the codebase would not compile.

Changes

escrow/src/lib.rs

  • Restored EscrowError enum, ensure, fail (accidentally deleted from HEAD)
  • Added EscrowError::SweepExceedsLiabilityFloor = 42
  • Added DataKey::DistributedPrincipal — additive key (absent ⇒ 0, backward-compatible per ADR-007)
  • Updated refund() — increments DistributedPrincipal atomically before token transfer
  • Updated sweep_terminal_dust() — in cancelled (status 4) only, asserts:
    outstanding = funded_amount - distributed_principal
    balance - sweep_amt >= outstanding
    
  • Added get_distributed_principal() read-only accessor

escrow/src/tests/external_calls.rs

Six new tests covering: all-refunded allows dust, no-refunds blocks sweep, partial refund allows only surplus, sweep eating outstanding blocked, zero funded_amount edge case, counter accumulation across refunds.

docs/adr/ADR-006-dust-sweep-and-token-safety.md

Updated with liability floor decision, scoping rationale, and test table.

Security notes

  • Floor scoped to status 4 only — settled/withdrawn disbursement is off-chain
  • saturating_sub / saturating_add throughout — overflow-safe
  • Checks-effects-interactions order preserved in refund()
  • All existing sweep tests verified unaffected

- Restore EscrowError enum, ensure/fail helpers accidentally removed from HEAD
- Add EscrowError::SweepExceedsLiabilityFloor = 42
- Add DataKey::DistributedPrincipal: running total of principal returned via refund()
- refund(): increment DistributedPrincipal atomically before token transfer
- sweep_terminal_dust(): in cancelled (status 4) escrows, assert
  balance - sweep_amt >= funded_amount - distributed_principal
  so the sweep can never pull funds owed to unredeemed investors
- Add get_distributed_principal() read-only accessor
- 6 new tests in escrow/src/tests/external_calls.rs covering:
  all-refunded allows dust, no-refunds blocks sweep, partial refund
  allows only surplus, sweep eating outstanding is blocked, zero
  funded_amount edge case, counter accumulation across refunds
- Update ADR-006 with liability floor decision and test table

Fixes: sweep_terminal_dust liability floor (issue referenced in task)
Refs: ADR-006, docs/escrow-security-checklist.md
@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented May 28, 2026

@Vvictor-commits 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! 🚀

Learn more about application limits

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reconcile contract token balance with funded_amount to bound sweep_terminal_dust blast radius

2 participants